diff --git a/.gitignore b/.gitignore index 3a342fd7..9dca76e1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,5 @@ npm-debug.log *.log -/plugins/vorlon/**/*.css tsconfig.json /Plugins/obj **/node_modules @@ -8,11 +7,11 @@ sync.bat *.suo /.vs/ bin/ -GLE id�es.txt +**/control.css sync.bat -Plugins/Vorlon/plugins/remoteDebugging.zip .settings/launch.json *.dat Server/public/stylesheets/style.css -vorlon /Server/public/stylesheets/style.css +/DeploymentTools/deployment-package.zip +DeploymentTools/deployment-package.zip \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index ee84dc00..08c4a076 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,9 +19,11 @@ // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. "runtimeExecutable": null, // Optional arguments passed to the runtime executable. - "runtimeArgs": ["--nolazy"], + "runtimeArgs": [ + "--nolazy" + ], // Environment variables passed to the program. - "env": { }, + "env": {}, // Use JavaScript source maps (if they exist). "sourceMaps": false, // If JavaScript source maps are enabled, the generated code is expected in this directory. @@ -35,6 +37,58 @@ // Port to attach to. "port": 5858, "sourceMaps": false + }, + { + // not working since latest vs code and electron versions :-( + "name": "Launch desktop App", + "type": "node", + "program": "desktop/app/background.js", + "stopOnEntry": false, + "args": [ + "--dev" + ], + "cwd": ".", + "runtimeExecutable": "desktop/node_modules/electron-prebuilt/dist/electron.exe", + "env": {} + }, + { + // not working since latest vs code and electron versions :-( + "name": "Launch node.js sample", + "type": "node", + "program": "client samples/nodejs/app.js", + "stopOnEntry": false, + "args": [ + "--nolazy" + ], + "cwd": ".", + "runtimeExecutable": null, + "env": {} + }, + { + // Name of configuration; appears in the launch configuration drop down menu. + "name": "Launch w/TypeScript", + // Type of configuration. Possible values: "node", "mono". + "type": "node", + // Workspace relative or absolute path to the program. + "program": "Server/server.ts", + // Automatically stop program after launch. + "stopOnEntry": false, + // Command line arguments passed to the program. + "args": [], + // Workspace relative or absolute path to the working directory of the program being debugged. Default is the current workspace. + "cwd": ".", + // Workspace relative or absolute path to the runtime executable to be used. Default is the runtime executable on the PATH. + "runtimeExecutable": null, + // Optional arguments passed to the runtime executable. + "runtimeArgs": [ + "--nolazy" + ], + // Environment variables passed to the program. + "env": {}, + // Use JavaScript source maps (if they exist). + "sourceMaps": true, + // If JavaScript source maps are enabled, the generated code is expected in this directory. + "outDir": null } ] -} +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..802a46dd --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,20 @@ +// Available variables which can be used inside of strings. +// ${workspaceRoot}: the root folder of the team +// ${file}: the current opened file +// ${fileBasename}: the current opened file's basename +// ${fileDirname}: the current opened file's dirname +// ${fileExtname}: the current opened file's extension +// ${cwd}: the current working directory of the spawned process + +{ + "version": "0.1.0", + "command": "gulp", + "isShellCommand": true, + "tasks": [ + { + "taskName": "default", + "isBuildCommand": true, + "showOutput": "always" + } + ] +} diff --git a/DeploymentTools/Dockerfile b/DeploymentTools/Dockerfile new file mode 100644 index 00000000..516b130b --- /dev/null +++ b/DeploymentTools/Dockerfile @@ -0,0 +1,29 @@ +# use the node argon image as base +FROM node:argon + +# Set the Vorlon.JS Docker Image maintainer +MAINTAINER Julien Corioland (Microsoft, DX) + +# update apt get and install unzip +RUN apt-get -qq update && apt-get -qqy install unzip + +# Create the application directory +RUN mkdir -p /usr/src/vorlonjs + +# Set app root as working directory +WORKDIR /usr/src/vorlonjs + +# Send the app content to the container +COPY deployment-package.zip /usr/src/vorlonjs + +# Extract the archive +RUN unzip deployment-package.zip -d /usr/src/vorlonjs + +# Remove the archive +RUN rm deployment-package.zip + +# Expose port 1337 +EXPOSE 1337 + +# Run Vorlon.JS +CMD ["npm", "start"] \ No newline at end of file diff --git a/DeploymentTools/build-docker-image.cmd b/DeploymentTools/build-docker-image.cmd new file mode 100644 index 00000000..0930d409 --- /dev/null +++ b/DeploymentTools/build-docker-image.cmd @@ -0,0 +1,28 @@ +@ECHO OFF + +IF "%1"=="" GOTO :usage +IF "%2"=="" GOTO :usage +IF "%3"=="" GOTO :usage +IF "%4"=="" GOTO :usage +IF "%5"=="" GOTO :usage + +@ECHO "SET DOCKER_HOST TO %1" +SET DOCKER_HOST=%1 + +@ECHO "BUILD DOCKER IMAGE" +docker --tls --tlscacert="%2\ca.pem" --tlscert="%2\cert.pem" --tlskey="%2\key.pem" build -t jcorioland/vorlonjs:0.2 . + +@ECHO "LOG INTO DOCKER HUB" +docker --tls --tlscacert="%2\ca.pem" --tlscert="%2\cert.pem" --tlskey="%2\key.pem" login --username="%3" --password="%4" --email="%5" https://index.docker.io/v1/ + +@ECHO "PUSH IMAGE INTO DOCKER HUB" +docker --tls --tlscacert="%2\ca.pem" --tlscert="%2\cert.pem" --tlskey="%2\key.pem" push jcorioland/vorlonjs:0.2 + +@ECHO "LOG OUT FROM DOCKER HUB" +docker --tls --tlscacert="%2\ca.pem" --tlscert="%2\cert.pem" --tlskey="%2\key.pem" logout + +GOTO :eof + +:usage +@ECHO Usage: %0 ^ ^ ^ ^ +EXIT /B 1 \ No newline at end of file diff --git a/DeploymentTools/deployment-template.json b/DeploymentTools/deployment-template.json new file mode 100644 index 00000000..ffd2b94d --- /dev/null +++ b/DeploymentTools/deployment-template.json @@ -0,0 +1,286 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingPlanName": { + "type": "string", + "minLength": 1 + }, + "siteName": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard", + "Premium" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverfarms", + "location": "[resourceGroup().location]", + "tags": { + "displayName": "HostingPlan" + }, + "properties": { + "name": "[parameters('hostingPlanName')]", + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/sites", + "location": "[resourceGroup().location]", + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "Website" + }, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "properties": { + "name": "[parameters('siteName')]", + "serverFarm": "[parameters('hostingPlanName')]" + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]", + "type": "Microsoft.Insights/autoscalesettings", + "location": "East US", + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "AutoScaleSettings" + }, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "properties": { + "profiles": [ + { + "name": "Default", + "capacity": { + "minimum": 1, + "maximum": 2, + "default": 1 + }, + "rules": [ + { + "metricTrigger": { + "metricName": "CpuPercentage", + "metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "timeGrain": "PT1M", + "statistic": "Average", + "timeWindow": "PT10M", + "timeAggregation": "Average", + "operator": "GreaterThan", + "threshold": 80.0 + }, + "scaleAction": { + "direction": "Increase", + "type": "ChangeCount", + "value": 1, + "cooldown": "PT10M" + } + }, + { + "metricTrigger": { + "metricName": "CpuPercentage", + "metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "timeGrain": "PT1M", + "statistic": "Average", + "timeWindow": "PT1H", + "timeAggregation": "Average", + "operator": "LessThan", + "threshold": 60.0 + }, + "scaleAction": { + "direction": "Decrease", + "type": "ChangeCount", + "value": 1, + "cooldown": "PT1H" + } + } + ] + } + ], + "enabled": false, + "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]", + "targetResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('ServerErrors ', parameters('siteName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', parameters('siteName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]": "Resource", + "displayName": "ServerErrorsAlertRule" + }, + "properties": { + "name": "[concat('ServerErrors ', parameters('siteName'))]", + "description": "[concat(parameters('siteName'), ' has some server errors, status code 5xx.')]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]", + "metricName": "Http5xx" + }, + "operator": "GreaterThan", + "threshold": 0.0, + "windowSize": "PT5M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('ForbiddenRequests ', parameters('siteName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', parameters('siteName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]": "Resource", + "displayName": "ForbiddenRequestsAlertRule" + }, + "properties": { + "name": "[concat('ForbiddenRequests ', parameters('siteName'))]", + "description": "[concat(parameters('siteName'), ' has some requests that are forbidden, status code 403.')]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]", + "metricName": "Http403" + }, + "operator": "GreaterThan", + "threshold": 0, + "windowSize": "PT5M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('CPUHigh ', parameters('hostingPlanName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "CPUHighAlertRule" + }, + "properties": { + "name": "[concat('CPUHigh ', parameters('hostingPlanName'))]", + "description": "[concat('The average CPU is high across all the instances of ', parameters('hostingPlanName'))]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "metricName": "CpuPercentage" + }, + "operator": "GreaterThan", + "threshold": 90, + "windowSize": "PT15M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "LongHttpQueueAlertRule" + }, + "properties": { + "name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]", + "description": "[concat('The HTTP queue for the instances of ', parameters('hostingPlanName'), ' has a large number of pending requests.')]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "metricName": "HttpQueueLength" + }, + "operator": "GreaterThan", + "threshold": 100.0, + "windowSize": "PT5M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Insights/components", + "location": "Central US", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', parameters('siteName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]": "Resource", + "displayName": "AppInsightsComponent" + }, + "properties": { + "applicationId": "[parameters('siteName')]" + } + } + ] +} diff --git a/DeploymentTools/dev-deployment-template-parameters.json b/DeploymentTools/dev-deployment-template-parameters.json new file mode 100644 index 00000000..18dfa12f --- /dev/null +++ b/DeploymentTools/dev-deployment-template-parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "value": "vorlonjs-dev" + }, + "hostingPlanName": { + "value": "vorlonjs-dev-hp" + }, + "sku" :{ + "value" : "Basic" + }, + "workerSize" : { + "value" : "0" + } + } +} \ No newline at end of file diff --git a/DeploymentTools/post-deployment-template.json b/DeploymentTools/post-deployment-template.json new file mode 100644 index 00000000..cb2a7122 --- /dev/null +++ b/DeploymentTools/post-deployment-template.json @@ -0,0 +1,65 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingPlanName": { + "type": "string", + "minLength": 1 + }, + "siteName": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard", + "Premium" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/sites", + "location": "[resourceGroup().location]", + "properties": { + "name": "[parameters('siteName')]" + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ], + "properties": { + "webSocketsEnabled": true, + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot\\Server", + "preloadEnabled": true, + "virtualDirectories": null + }] + } + } + ] + } + ] +} diff --git a/DeploymentTools/preprod-deployment-template-parameters.json b/DeploymentTools/preprod-deployment-template-parameters.json new file mode 100644 index 00000000..aebf284b --- /dev/null +++ b/DeploymentTools/preprod-deployment-template-parameters.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "value": "vorlonjs-preprod" + }, + "hostingPlanName": { + "value": "vorlonjs-preprod-hp" + }, + "sku" :{ + "value" : "Basic" + }, + "workerSize" : { + "value" : "0" + } + } +} \ No newline at end of file diff --git a/DeploymentTools/production-deployment-template-parameters.json b/DeploymentTools/production-deployment-template-parameters.json new file mode 100644 index 00000000..afbfc7d0 --- /dev/null +++ b/DeploymentTools/production-deployment-template-parameters.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "siteName": { + "value": "vorlonjs-production" + }, + "hostingPlanName": { + "value": "vorlonjs-production-hp" + }, + "slotName":{ + "value": "staging" + }, + "sku" :{ + "value" : "Standard" + }, + "workerSize" : { + "value" : "0" + } + } +} \ No newline at end of file diff --git a/DeploymentTools/production-deployment-template.json b/DeploymentTools/production-deployment-template.json new file mode 100644 index 00000000..a7fa47ec --- /dev/null +++ b/DeploymentTools/production-deployment-template.json @@ -0,0 +1,323 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingPlanName": { + "type": "string", + "minLength": 1 + }, + "siteName": { + "type": "string", + "minLength": 1 + }, + "slotName": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard", + "Premium" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('hostingPlanName')]", + "type": "Microsoft.Web/serverfarms", + "location": "[resourceGroup().location]", + "tags": { + "displayName": "HostingPlan" + }, + "properties": { + "name": "[parameters('hostingPlanName')]", + "sku": "[parameters('sku')]", + "workerSize": "[parameters('workerSize')]", + "numberOfWorkers": 1 + } + }, + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/sites", + "location": "[resourceGroup().location]", + "tags": { + "[concat('hidden-related:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "Website" + }, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "properties": { + "name": "[parameters('siteName')]", + "serverFarm": "[parameters('hostingPlanName')]" + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "type": "slots", + "name" : "[parameters('slotName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ], + "properties": { + "name":"[concat(parameters('siteName'), '(', parameters('slotName'), ')')]" + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites/slots', parameters('siteName'), parameters('slotName'))]" + ], + "properties": { + "webSocketsEnabled": true, + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot", + "preloadEnabled": false, + "virtualDirectories": null + }] + } + } + ] + }] + }, + { + "apiVersion": "2014-04-01", + "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]", + "type": "Microsoft.Insights/autoscalesettings", + "location": "East US", + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "AutoScaleSettings" + }, + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "properties": { + "profiles": [ + { + "name": "Default", + "capacity": { + "minimum": 1, + "maximum": 2, + "default": 1 + }, + "rules": [ + { + "metricTrigger": { + "metricName": "CpuPercentage", + "metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "timeGrain": "PT1M", + "statistic": "Average", + "timeWindow": "PT10M", + "timeAggregation": "Average", + "operator": "GreaterThan", + "threshold": 80.0 + }, + "scaleAction": { + "direction": "Increase", + "type": "ChangeCount", + "value": 1, + "cooldown": "PT10M" + } + }, + { + "metricTrigger": { + "metricName": "CpuPercentage", + "metricResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "timeGrain": "PT1M", + "statistic": "Average", + "timeWindow": "PT1H", + "timeAggregation": "Average", + "operator": "LessThan", + "threshold": 60.0 + }, + "scaleAction": { + "direction": "Decrease", + "type": "ChangeCount", + "value": 1, + "cooldown": "PT1H" + } + } + ] + } + ], + "enabled": false, + "name": "[concat(parameters('hostingPlanName'), '-', resourceGroup().name)]", + "targetResourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('ServerErrors ', parameters('siteName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', parameters('siteName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]": "Resource", + "displayName": "ServerErrorsAlertRule" + }, + "properties": { + "name": "[concat('ServerErrors ', parameters('siteName'))]", + "description": "[concat(parameters('siteName'), ' has some server errors, status code 5xx.')]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]", + "metricName": "Http5xx" + }, + "operator": "GreaterThan", + "threshold": 0.0, + "windowSize": "PT5M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('ForbiddenRequests ', parameters('siteName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', parameters('siteName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]": "Resource", + "displayName": "ForbiddenRequestsAlertRule" + }, + "properties": { + "name": "[concat('ForbiddenRequests ', parameters('siteName'))]", + "description": "[concat(parameters('siteName'), ' has some requests that are forbidden, status code 403.')]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]", + "metricName": "Http403" + }, + "operator": "GreaterThan", + "threshold": 0, + "windowSize": "PT5M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('CPUHigh ', parameters('hostingPlanName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "CPUHighAlertRule" + }, + "properties": { + "name": "[concat('CPUHigh ', parameters('hostingPlanName'))]", + "description": "[concat('The average CPU is high across all the instances of ', parameters('hostingPlanName'))]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "metricName": "CpuPercentage" + }, + "operator": "GreaterThan", + "threshold": 90, + "windowSize": "PT15M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]", + "type": "Microsoft.Insights/alertrules", + "location": "East US", + "dependsOn": [ + "[concat('Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]": "Resource", + "displayName": "LongHttpQueueAlertRule" + }, + "properties": { + "name": "[concat('LongHttpQueue ', parameters('hostingPlanName'))]", + "description": "[concat('The HTTP queue for the instances of ', parameters('hostingPlanName'), ' has a large number of pending requests.')]", + "isEnabled": false, + "condition": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.ThresholdRuleCondition", + "dataSource": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleMetricDataSource", + "resourceUri": "[concat(resourceGroup().id, '/providers/Microsoft.Web/serverfarms/', parameters('hostingPlanName'))]", + "metricName": "HttpQueueLength" + }, + "operator": "GreaterThan", + "threshold": 100.0, + "windowSize": "PT5M" + }, + "action": { + "odata.type": "Microsoft.Azure.Management.Insights.Models.RuleEmailAction", + "sendToServiceOwners": true, + "customEmails": [] + } + } + }, + { + "apiVersion": "2014-04-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Insights/components", + "location": "Central US", + "dependsOn": [ + "[concat('Microsoft.Web/sites/', parameters('siteName'))]" + ], + "tags": { + "[concat('hidden-link:', resourceGroup().id, '/providers/Microsoft.Web/sites/', parameters('siteName'))]": "Resource", + "displayName": "AppInsightsComponent" + }, + "properties": { + "applicationId": "[parameters('siteName')]" + } + } + ] +} diff --git a/DeploymentTools/production-post-deployment-template.json b/DeploymentTools/production-post-deployment-template.json new file mode 100644 index 00000000..b867d5e5 --- /dev/null +++ b/DeploymentTools/production-post-deployment-template.json @@ -0,0 +1,100 @@ +{ + "$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "hostingPlanName": { + "type": "string", + "minLength": 1 + }, + "siteName": { + "type": "string", + "minLength": 1 + }, + "slotName": { + "type": "string", + "minLength": 1 + }, + "sku": { + "type": "string", + "allowedValues": [ + "Free", + "Shared", + "Basic", + "Standard", + "Premium" + ], + "defaultValue": "Free" + }, + "workerSize": { + "type": "string", + "allowedValues": [ + "0", + "1", + "2" + ], + "defaultValue": "0" + } + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "name": "[parameters('siteName')]", + "type": "Microsoft.Web/sites", + "location": "[resourceGroup().location]", + "properties": { + "name": "[parameters('siteName')]" + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ], + "properties": { + "webSocketsEnabled": true, + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot\\Server", + "preloadEnabled": false, + "virtualDirectories": null + }] + } + }, + { + "apiVersion": "2014-06-01", + "type": "slots", + "name" : "[parameters('slotName')]", + "location": "[resourceGroup().location]", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites', parameters('siteName'))]" + ], + "properties": { + "name":"[concat(parameters('siteName'), '(', parameters('slotName'), ')')]" + }, + "resources": [ + { + "apiVersion": "2014-06-01", + "type": "config", + "name": "web", + "dependsOn": [ + "[resourceId('Microsoft.Web/Sites/slots', parameters('siteName'), parameters('slotName'))]" + ], + "properties": { + "webSocketsEnabled": true, + "virtualApplications": [ + { + "virtualPath": "/", + "physicalPath": "site\\wwwroot\\Server", + "preloadEnabled": false, + "virtualDirectories": null + }] + } + } + ] + } + ] + }] +} diff --git a/DeploymentTools/production-preview-slot-deletion.ps1 b/DeploymentTools/production-preview-slot-deletion.ps1 new file mode 100644 index 00000000..588a5214 --- /dev/null +++ b/DeploymentTools/production-preview-slot-deletion.ps1 @@ -0,0 +1 @@ +Remove-AzureWebsite -Name vorlonjs-production -Slot staging -Force -ErrorAction SilentlyContinu \ No newline at end of file diff --git a/Plugins/Vorlon/plugins/babylonInspector/control.css b/Plugins/Vorlon/plugins/babylonInspector/control.css new file mode 100644 index 00000000..3c16e074 --- /dev/null +++ b/Plugins/Vorlon/plugins/babylonInspector/control.css @@ -0,0 +1,124 @@ +#babylonInspector-displayer { + position: absolute; + display: none; + left: 300px; + top: 100px; + width: 300px; + height: 300px; +} + +.tree-node { + margin-left: 20px; +} + +.tree-node-button { + display: inline-block; + cursor: pointer; + width: 16px; + height: 16px; +} + +.tree-node-content { + display: inline-block; + margin-left: 5px; + margin-right: 5px; +} + +.tree-node-features { + display: inline-block; + margin-left: 15px; +} + +.tree-node-features * { + display: inline-block; + margin-left: 15px; +} + +.tree-node-hidden { + display: none; +} + +.tree-node-type-icon { + display: inline-block; + width: 16px; + height: 16px; +} + +.tree-node-type-icon-mesh { + background-image: url("img/mesh.png"); +} + +.tree-node-type-icon-light { + background-image: url("img/light.png"); +} + +.tree-node-type-icon-camera { + background-image: url("img/camera.png"); +} + +.tree-node-type-icon-skeleton { + background-image: url("img/skeleton.png"); +} + +.tree-node-features-element { + display: inline-block; +} + +.tree-node-color-sample { + width: 12px; + height: 12px; + margin-left: 5px; + border: 1px solid; +} + +.tree-node-texture-thumbnail { + width: 20px; + height: 20px; +} + +.tree-node-texture-view { + width: 300px; + height: 300px; +} + +.tree-node-animation-button { + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-position: center; +} + +.tree-node-animation-button-play { + background-image: url("img/play.png"); +} + +.tree-node-animation-button-pause { + background-image: url("img/pause.png"); +} + +.tree-node-animation-button-stop { + background-image: url("img/stop.png"); +} + +.tree-node-animation-button-stop-pressed { + background-image: url("img/stop_pressed.png"); +} + +.tree-node-spot-mesh-button { + width: 16px; + height: 16px; + background-repeat: no-repeat; + background-position: center; +} + +.tree-node-spot-mesh-button-on { + background-image: url("img/spot_on.png"); +} + +.tree-node-spot-mesh-button-off { + background-image: url("img/spot_off.png"); +} + +.clickable { + cursor: pointer; +} \ No newline at end of file diff --git a/Plugins/Vorlon/plugins/babylonInspector/control.html b/Plugins/Vorlon/plugins/babylonInspector/control.html index 93d13be6..bdd01665 100644 --- a/Plugins/Vorlon/plugins/babylonInspector/control.html +++ b/Plugins/Vorlon/plugins/babylonInspector/control.html @@ -1,5 +1,4 @@
-
-
\ No newline at end of file + diff --git a/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.client.ts b/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.client.ts index 9ce2e67e..a680d912 100644 --- a/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.client.ts +++ b/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.client.ts @@ -763,12 +763,14 @@ module VORLON { * as the user switches between clients on the dashboard. */ public refresh(): void { - if (this.engine) { - this._sendScenesData(); - } else { - this.engine = this._getBabylonEngine(); - this.scenes = this.engine.scenes; - this._sendScenesData(); + if(typeof BABYLON !== 'undefined'){ + if (this.engine) { + this._sendScenesData(); + } else { + this.engine = this._getBabylonEngine(); + this.scenes = this.engine.scenes; + this._sendScenesData(); + } } } @@ -776,18 +778,17 @@ module VORLON { * Start the clientside code : initilization etc */ public startClientSide(): void { - if(!BABYLON.Engine.isSupported()) { + if(typeof BABYLON !== 'undefined' && !BABYLON.Engine.isSupported()) { //error } else { + //document.addEventListener("DOMContentLoaded", () => { + this.engine = this._getBabylonEngine(); + if (this.engine) { + this.scenes = this.engine.scenes; + this.refresh(); + } + //}); } - - //document.addEventListener("DOMContentLoaded", () => { - this.engine = this._getBabylonEngine(); - if (this.engine) { - this.scenes = this.engine.scenes; - this.refresh(); - } - //}); } /** @@ -837,10 +838,13 @@ module VORLON { * @private */ private _findMesh(meshName : string, sceneID : string) { - var id : number = +sceneID; - var scene = this.engine.scenes[id]; - var mesh = scene.getMeshByName(meshName); - return mesh; + if(typeof BABYLON !== 'undefined'){ + var id : number = +sceneID; + var scene = this.engine.scenes[id]; + var mesh = scene.getMeshByName(meshName); + return mesh; + } + return null; } /** @@ -848,7 +852,7 @@ module VORLON { * @private */ private _sendScenesData() { - if (this.scenes) { + if (typeof BABYLON !== 'undefined' && this.scenes) { var scenesData = this._dataGenerator.generateScenesData(this.scenes); this.sendToDashboard({ messageType: 'SCENES_DATA', @@ -864,7 +868,7 @@ module VORLON { */ private _getBabylonEngine() { for (var member in window) { - if (window[member] instanceof BABYLON.Engine) { + if (typeof BABYLON !== 'undefined' && window[member] instanceof BABYLON.Engine) { return window[member]; } } diff --git a/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.dashboard.ts b/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.dashboard.ts index aa3537cf..86101043 100644 --- a/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.dashboard.ts +++ b/Plugins/Vorlon/plugins/babylonInspector/vorlon.babylonInspector.dashboard.ts @@ -221,8 +221,10 @@ module VORLON { private _createColorSample(colorHex) { var colorSample = document.createElement('div'); colorSample.className = "tree-node-color-sample"; - colorSample.style.backgroundColor = colorHex; - colorSample.style.borderColor = this._isClearColor(colorHex) ? '#000000' : '#ffffff'; + if (colorHex) { + colorSample.style.backgroundColor = colorHex; + colorSample.style.borderColor = this._isClearColor(colorHex) ? '#000000' : '#ffffff'; + } return colorSample; } diff --git a/Plugins/Vorlon/plugins/domExplorer/control.less b/Plugins/Vorlon/plugins/domExplorer/control.less index 7b0a705d..9e8e3f5f 100644 --- a/Plugins/Vorlon/plugins/domExplorer/control.less +++ b/Plugins/Vorlon/plugins/domExplorer/control.less @@ -20,9 +20,7 @@ height: 100%; overflow: hidden !important; } - - - + .panel-left, .panel-right { height: 100%; @@ -532,16 +530,27 @@ .nodeAttribute { padding-left: 0.5em; - color: @quotesColor; - + color: @quotesColor; + span.attr-name { color: @attributeNameColor; } - + + span.link-hovered{ + text-decoration: underline; + } + span.attr-value { - color: @attributeValueColor; + color: @attributeValueColor; } } + + .colored-square{ + display: inline-flex; + height: 10px; + width: 10px; + border: 1px #000 solid; + } .styleLabel, .attributeName { display: inline-block; @@ -614,13 +623,14 @@ body .b-m-mpanel { cursor: pointer; border: none; } + /*.ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { background: #CCC; } - - + .ui-menu-item { padding: 10px; font-size: 10pt; }*/ } + \ No newline at end of file diff --git a/Plugins/Vorlon/plugins/domExplorer/vorlon.domExplorer.dashboard.ts b/Plugins/Vorlon/plugins/domExplorer/vorlon.domExplorer.dashboard.ts index f3799601..d9d7de8f 100644 --- a/Plugins/Vorlon/plugins/domExplorer/vorlon.domExplorer.dashboard.ts +++ b/Plugins/Vorlon/plugins/domExplorer/vorlon.domExplorer.dashboard.ts @@ -410,7 +410,7 @@ }, nodeStyle(data: any){ console.log("dashboard receive node style", data); - var plugin = this; + var plugin = this; plugin.setNodeStyle(data.internalID, data.styles); } } @@ -820,13 +820,15 @@ $('.b-m-mpanel').remove(); $("#" + parentElementId).contextmenu(option); } + nodeValue.addEventListener("contextmenu",() => { if (nodeValue.contentEditable != "true" && nodeName.contentEditable != "true") menu.bind(this)("value"); }); - nodeValue.addEventListener("click",() => { - this.parent.plugin.makeEditable(nodeValue); - }); + nodeValue.addEventListener("click",(e) => { + if(!this.uriCheck("click", nodeValue, e)) + this.parent.plugin.makeEditable(nodeValue); + }); nodeName.addEventListener("click",() => { this.parent.plugin.makeEditable(nodeName); }); @@ -834,8 +836,8 @@ if (nodeValue.contentEditable != "true" && nodeName.contentEditable != "true") menu.bind(this)("name"); }); - nodeValue.addEventListener("blur",() => { - sendTextToClient.bind(this)(nodeName.innerHTML, nodeValue.innerHTML, nodeValue); + nodeValue.addEventListener("blur",() => { + sendTextToClient.bind(this)(nodeName.innerHTML, nodeValue.innerHTML, nodeValue); }); nodeName.addEventListener("blur",() => { sendTextToClient.bind(this)(nodeName.innerHTML, nodeValue.innerHTML, nodeName); @@ -851,8 +853,32 @@ evt.preventDefault(); sendTextToClient.bind(this)(nodeName.innerHTML, nodeValue.innerHTML, nodeValue); } + }); + nodeValue.addEventListener("mousemove",(e) => { + this.uriCheck("mousemove", nodeValue, e); }); + nodeValue.addEventListener("mouseout",(e) => { + $(nodeValue).removeClass("link-hovered"); + }); } + + uriCheck(triggerType: string, node, e) { + if (e != null && e.ctrlKey) { + var urlPattern = /(\w+):\/*([^\/]+)([a-z0-9\-@\^=%&;\/~\+]*)[\?]?([^ \#]*)#?([^ \#]*)/i; + if (urlPattern.test(node.innerText)) { + switch(triggerType){ + case "click": open(node.innerText); + case "mousemove": $(node).addClass("link-hovered"); + default: return true; + } + return true; + } + } + else{ + $(node).removeClass("link-hovered"); + } + return false; + } render() { var node = new FluentDOM("SPAN", "nodeAttribute", this.parent.headerAttributes); @@ -899,6 +925,9 @@ for (var index = 0; index < styles.length; index++) { var style = styles[index]; var splits = style.split(":"); + // ensure that urls are not malformed after the split. + if(splits[2] !== undefined && splits[2].indexOf('//') > -1) + splits[1] += ":" + splits[2]; this.styles.push(new DomExplorerPropertyEditorItem(this, splits[0], splits[1], this.internalId)); } // Append add style button @@ -907,8 +936,7 @@ this.plugin.styleView.appendChild(e.target); }); } - } - + } } export class DomExplorerPropertyEditorItem { @@ -920,17 +948,24 @@ this.name = name; this.value = value; if (generate) - this._generateStyle(name, value, internalId, editableLabel); + this._generateStyle(name, value, internalId, editableLabel); } private _generateStyle(property: string, value: string, internalId: string, editableLabel = false): void { + console.debug(property + value); var wrap = document.createElement("div"); wrap.className = 'styleWrap'; var label = document.createElement("div"); label.innerHTML = property; label.className = "styleLabel"; - label.contentEditable = "false"; + label.contentEditable = "false"; var valueElement = this._generateClickableValue(label, value, internalId); - wrap.appendChild(label); + wrap.appendChild(label); + if(property.indexOf("color") != -1){ + var square = document.createElement("span"); + square.className = "colored-square"; + square.style.backgroundColor = value; + wrap.appendChild(square); + } wrap.appendChild(valueElement); this.parent.plugin.styleView.appendChild(wrap); @@ -1000,7 +1035,6 @@ }); return valueElement; } - } export interface LayoutStyle { diff --git a/Plugins/Vorlon/plugins/interactiveConsole/vorlon.interactiveConsole.client.ts b/Plugins/Vorlon/plugins/interactiveConsole/vorlon.interactiveConsole.client.ts index 7e9c857b..102f252c 100644 --- a/Plugins/Vorlon/plugins/interactiveConsole/vorlon.interactiveConsole.client.ts +++ b/Plugins/Vorlon/plugins/interactiveConsole/vorlon.interactiveConsole.client.ts @@ -107,9 +107,12 @@ for (var i = 0, l = messages.length; i < l; i++) { var msg = messages[i]; if (typeof msg === 'string' || typeof msg === 'number') { - resmessages.push(msg); + resmessages.push(msg); } else { - if (msg == window || msg == document) { + if (!Tools.IsWindowAvailable){ + resmessages.push(this.inspect(msg, msg, 0)); + } + else if(msg == window || msg == document) { resmessages.push('VORLON : object cannot be inspected, too big...'); } else { resmessages.push(this.inspect(msg, msg, 0)); @@ -168,13 +171,14 @@ public startClientSide(): void { this._cache = []; this._pendingEntries = []; + var console = Tools.IsWindowAvailable ? window.console : global.console; // Overrides clear, log, error and warn - this._hooks.clear = Tools.Hook(window.console, "clear",(): void => { + this._hooks.clear = Tools.Hook(console, "clear",(): void => { this.clearClientConsole(); }); - this._hooks.dir = Tools.Hook(window.console, "dir",(message: any): void => { + this._hooks.dir = Tools.Hook(console, "dir",(message: any): void => { var data = { messages: this.getMessages(message), type: "dir" @@ -183,7 +187,7 @@ this.addEntry(data); }); - this._hooks.log = Tools.Hook(window.console, "log", (message: any): void => { + this._hooks.log = Tools.Hook(console, "log", (message: any): void => { var data = { messages: this.getMessages(message), type: "log" @@ -192,7 +196,7 @@ this.addEntry(data); }); - this._hooks.debug = Tools.Hook(window.console, "debug", (message: any): void => { + this._hooks.debug = Tools.Hook(console, "debug", (message: any): void => { var data = { messages: this.getMessages(message), type: "debug" @@ -201,7 +205,7 @@ this.addEntry(data); }); - this._hooks.info = Tools.Hook(window.console, "info",(message: any): void => { + this._hooks.info = Tools.Hook(console, "info",(message: any): void => { var data = { messages: this.getMessages(message), type: "info" @@ -210,7 +214,7 @@ this.addEntry(data); }); - this._hooks.warn = Tools.Hook(window.console, "warn",(message: any): void => { + this._hooks.warn = Tools.Hook(console, "warn",(message: any): void => { var data = { messages: this.getMessages(message), type: "warn" @@ -219,7 +223,7 @@ this.addEntry(data); }); - this._hooks.error = Tools.Hook(window.console, "error",(message: any): void => { + this._hooks.error = Tools.Hook(console, "error",(message: any): void => { var data = { messages: this.getMessages(message), type: "error" @@ -244,13 +248,15 @@ return error; }); - window.addEventListener('error', (err) => { - - if (err && (err).error) { - //this.addEntry({ messages: [err.error.message], type: "exception" }); - this.addEntry({ messages: [(err).error.stack], type: "exception" }); - } - }); + if (Tools.IsWindowAvailable) { + window.addEventListener('error', (err) => { + + if (err && (err).error) { + //this.addEntry({ messages: [err.error.message], type: "exception" }); + this.addEntry({ messages: [(err).error.stack], type: "exception" }); + } + }); + } } public clearClientConsole() { diff --git a/Plugins/Vorlon/plugins/networkMonitor/vorlon.networkMonitor.client.ts b/Plugins/Vorlon/plugins/networkMonitor/vorlon.networkMonitor.client.ts index b30f3039..0df6764f 100644 --- a/Plugins/Vorlon/plugins/networkMonitor/vorlon.networkMonitor.client.ts +++ b/Plugins/Vorlon/plugins/networkMonitor/vorlon.networkMonitor.client.ts @@ -14,30 +14,31 @@ public sendClientData(): void { this.trace("network monitor sending data ") - var entries = window.performance.getEntries(); - //console.log(entries); - this.performanceItems = []; - for (var i = 0; i < entries.length; i++) { - this.performanceItems.push({ - name: entries[i].name, - type: entries[i].initiatorType, - startTime: entries[i].startTime, - duration: entries[i].duration, - redirectStart: entries[i].redirectStart, - redirectDuration: entries[i].redirectEnd - entries[i].redirectStart, - dnsStart: entries[i].domainLookupStart, - dnsDuration: entries[i].domainLookupEnd - entries[i].domainLookupStart, - tcpStart: entries[i].connectStart, - tcpDuration: entries[i].connectEnd - entries[i].connectStart, // TODO - requestStart: entries[i].requestStart, - requestDuration: entries[i].responseStart - entries[i].requestStart, - responseStart: entries[i].responseStart, - responseDuration: (entries[i].responseStart == 0 ? 0 : entries[i].responseEnd - entries[i].responseStart) - }); + + if (window.performance) { + var entries = window.performance.getEntries(); + + for (var i = 0; i < entries.length; i++) { + this.performanceItems.push({ + name: entries[i].name, + type: entries[i].initiatorType, + startTime: entries[i].startTime, + duration: entries[i].duration, + redirectStart: entries[i].redirectStart, + redirectDuration: entries[i].redirectEnd - entries[i].redirectStart, + dnsStart: entries[i].domainLookupStart, + dnsDuration: entries[i].domainLookupEnd - entries[i].domainLookupStart, + tcpStart: entries[i].connectStart, + tcpDuration: entries[i].connectEnd - entries[i].connectStart, // TODO + requestStart: entries[i].requestStart, + requestDuration: entries[i].responseStart - entries[i].requestStart, + responseStart: entries[i].responseStart, + responseDuration: (entries[i].responseStart == 0 ? 0 : entries[i].responseEnd - entries[i].responseStart) + }); + } } - //console.log(this.performanceItems); var message: any = {}; message.entries = this.performanceItems; this.sendCommandToDashboard("performanceItems", message); diff --git a/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.client.ts b/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.client.ts index e69d3b9b..74f8f8f7 100644 --- a/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.client.ts +++ b/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.client.ts @@ -14,7 +14,7 @@ private STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg; private ARGUMENT_NAMES = /([^\s,]+)/g; - private rootProperty = 'window'; + private rootProperty = Tools.IsWindowAvailable ? 'window' : "global"; private getFunctionArgumentNames(func) { var result = []; @@ -154,7 +154,7 @@ } private _getProperty(propertyPath: string): ObjExplorerObjDescriptor { - var selectedObj = window; + var selectedObj = Tools.IsWindowAvailable ? window : global; var tokens = [this.rootProperty]; this.trace("getting obj at " + propertyPath); diff --git a/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.dashboard.ts b/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.dashboard.ts index 997a25a7..67de5be5 100644 --- a/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.dashboard.ts +++ b/Plugins/Vorlon/plugins/objectExplorer/vorlon.objectExplorer.dashboard.ts @@ -177,7 +177,7 @@ var elt = new FluentDOM('DIV', 'objdescriptor', parent); this.element = elt.element; this.isRoot = isRoot; - this.element.__vorlon = this; + (this.element).__vorlon = this; this.item = item; this.plugin = plugin; this.childs = []; @@ -198,7 +198,7 @@ public dispose() { this.clear(); - this.element.__vorlon = null; + (this.element).__vorlon = null; this.plugin = null; this.element = null; this.item = null; @@ -380,7 +380,7 @@ btn.text("-"); var elt = this.element.element.querySelector(".expand-content > .objdescriptor"); if (elt) { - var ctrl = elt.__vorlon; + var ctrl = (elt).__vorlon; if (ctrl) { setTimeout(() => { ctrl.getContent(); diff --git a/Plugins/Vorlon/plugins/unitTestRunner/qunit.js b/Plugins/Vorlon/plugins/unitTestRunner/qunit.js index 317ec406..f51204cb 100644 --- a/Plugins/Vorlon/plugins/unitTestRunner/qunit.js +++ b/Plugins/Vorlon/plugins/unitTestRunner/qunit.js @@ -1,102 +1,248 @@ /*! - * QUnit 1.18.1-pre + * QUnit 1.20.0 * http://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * http://jquery.org/license * - * Date: 2015-06-18T11:09Z + * Date: 2015-10-27T17:53Z */ -(function( window ) { +(function( global ) { -var QUnit, - config, - onErrorFnPrev, - loggingCallbacks = {}, - fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ), - toString = Object.prototype.toString, - hasOwn = Object.prototype.hasOwnProperty, - // Keep a local reference to Date (GH-283) - Date = window.Date, - now = Date.now || function() { - return new Date().getTime(); - }, - globalStartCalled = false, - runStarted = false, - setTimeout = window.setTimeout, - clearTimeout = window.clearTimeout, - defined = { - document: window.document !== undefined, - setTimeout: window.setTimeout !== undefined, - sessionStorage: (function() { - var x = "qunit-test-string"; - try { - sessionStorage.setItem( x, x ); - sessionStorage.removeItem( x ); - return true; - } catch ( e ) { - return false; +var QUnit = {}; + +var Date = global.Date; +var now = Date.now || function() { + return new Date().getTime(); +}; + +var setTimeout = global.setTimeout; +var clearTimeout = global.clearTimeout; + +// Store a local window from the global to allow direct references. +var window = global.window; + +var defined = { + document: window && window.document !== undefined, + setTimeout: setTimeout !== undefined, + sessionStorage: (function() { + var x = "qunit-test-string"; + try { + sessionStorage.setItem( x, x ); + sessionStorage.removeItem( x ); + return true; + } catch ( e ) { + return false; + } + }() ) +}; + +var fileName = ( sourceFromStacktrace( 0 ) || "" ).replace( /(:\d+)+\)?/, "" ).replace( /.+\//, "" ); +var globalStartCalled = false; +var runStarted = false; + +var toString = Object.prototype.toString, + hasOwn = Object.prototype.hasOwnProperty; + +// returns a new Array with the elements that are in a but not in b +function diff( a, b ) { + var i, j, + result = a.slice(); + + for ( i = 0; i < result.length; i++ ) { + for ( j = 0; j < b.length; j++ ) { + if ( result[ i ] === b[ j ] ) { + result.splice( i, 1 ); + i--; + break; } - }()) - }, - /** - * Provides a normalized error string, correcting an issue - * with IE 7 (and prior) where Error.prototype.toString is - * not properly implemented - * - * Based on http://es5.github.com/#x15.11.4.4 - * - * @param {String|Error} error - * @return {String} error message - */ - errorString = function( error ) { - var name, message, - errorString = error.toString(); - if ( errorString.substring( 0, 7 ) === "[object" ) { - name = error.name ? error.name.toString() : "Error"; - message = error.message ? error.message.toString() : ""; - if ( name && message ) { - return name + ": " + message; - } else if ( name ) { - return name; - } else if ( message ) { - return message; + } + } + return result; +} + +// from jquery.js +function inArray( elem, array ) { + if ( array.indexOf ) { + return array.indexOf( elem ); + } + + for ( var i = 0, length = array.length; i < length; i++ ) { + if ( array[ i ] === elem ) { + return i; + } + } + + return -1; +} + +/** + * Makes a clone of an object using only Array or Object as base, + * and copies over the own enumerable properties. + * + * @param {Object} obj + * @return {Object} New object with only the own properties (recursively). + */ +function objectValues ( obj ) { + var key, val, + vals = QUnit.is( "array", obj ) ? [] : {}; + for ( key in obj ) { + if ( hasOwn.call( obj, key ) ) { + val = obj[ key ]; + vals[ key ] = val === Object( val ) ? objectValues( val ) : val; + } + } + return vals; +} + +function extend( a, b, undefOnly ) { + for ( var prop in b ) { + if ( hasOwn.call( b, prop ) ) { + + // Avoid "Member not found" error in IE8 caused by messing with window.constructor + // This block runs on every environment, so `global` is being used instead of `window` + // to avoid errors on node. + if ( prop !== "constructor" || a !== global ) { + if ( b[ prop ] === undefined ) { + delete a[ prop ]; + } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { + a[ prop ] = b[ prop ]; + } + } + } + } + + return a; +} + +function objectType( obj ) { + if ( typeof obj === "undefined" ) { + return "undefined"; + } + + // Consider: typeof null === object + if ( obj === null ) { + return "null"; + } + + var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), + type = match && match[ 1 ]; + + switch ( type ) { + case "Number": + if ( isNaN( obj ) ) { + return "nan"; + } + return "number"; + case "String": + case "Boolean": + case "Array": + case "Set": + case "Map": + case "Date": + case "RegExp": + case "Function": + case "Symbol": + return type.toLowerCase(); + } + if ( typeof obj === "object" ) { + return "object"; + } +} + +// Safe object type checking +function is( type, obj ) { + return QUnit.objectType( obj ) === type; +} + +var getUrlParams = function() { + var i, current; + var urlParams = {}; + var location = window.location; + var params = location.search.slice( 1 ).split( "&" ); + var length = params.length; + + if ( params[ 0 ] ) { + for ( i = 0; i < length; i++ ) { + current = params[ i ].split( "=" ); + current[ 0 ] = decodeURIComponent( current[ 0 ] ); + + // allow just a key to turn on a flag, e.g., test.html?noglobals + current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; + if ( urlParams[ current[ 0 ] ] ) { + urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); } else { - return "Error"; + urlParams[ current[ 0 ] ] = current[ 1 ]; } - } else { - return errorString; } - }, - /** - * Makes a clone of an object using only Array or Object as base, - * and copies over the own enumerable properties. - * - * @param {Object} obj - * @return {Object} New object with only the own properties (recursively). - */ - objectValues = function( obj ) { - var key, val, - vals = QUnit.is( "array", obj ) ? [] : {}; - for ( key in obj ) { - if ( hasOwn.call( obj, key ) ) { - val = obj[ key ]; - vals[ key ] = val === Object( val ) ? objectValues( val ) : val; + } + + return urlParams; +}; + +// Doesn't support IE6 to IE9, it will return undefined on these browsers +// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack +function extractStacktrace( e, offset ) { + offset = offset === undefined ? 4 : offset; + + var stack, include, i; + + if ( e.stack ) { + stack = e.stack.split( "\n" ); + if ( /^error$/i.test( stack[ 0 ] ) ) { + stack.shift(); + } + if ( fileName ) { + include = []; + for ( i = offset; i < stack.length; i++ ) { + if ( stack[ i ].indexOf( fileName ) !== -1 ) { + break; + } + include.push( stack[ i ] ); + } + if ( include.length ) { + return include.join( "\n" ); } } - return vals; - }; + return stack[ offset ]; + + // Support: Safari <=6 only + } else if ( e.sourceURL ) { + + // exclude useless self-reference for generated Error objects + if ( /qunit.js$/.test( e.sourceURL ) ) { + return; + } + + // for actual exceptions, this is useful + return e.sourceURL + ":" + e.line; + } +} + +function sourceFromStacktrace( offset ) { + var error = new Error(); + + // Support: Safari <=7 only, IE <=10 - 11 only + // Not all browsers generate the `stack` property for `new Error()`, see also #636 + if ( !error.stack ) { + try { + throw error; + } catch ( err ) { + error = err; + } + } -QUnit = {}; + return extractStacktrace( error, offset ); +} /** * Config object: Maintain internal state * Later exposed as QUnit.config * `config` initialized at top of scope */ -config = { +var config = { // The queue of tests to run queue: [], @@ -110,15 +256,19 @@ config = { // by default, modify document.title when suite is done altertitle: true, + // HTML Reporter: collapse every test except the first failing test + // If false, all failing tests will be expanded + collapse: true, + // by default, scroll to top of the page when suite is done scrolltop: true, - // when enabled, all tests must call expect() - requireExpects: false, - // depth up-to which object will be dumped maxDepth: 5, + // when enabled, all tests must call expect() + requireExpects: false, + // add checkboxes that are persisted in the query-string // when enabled, the id is set to `true` as a `QUnit.config` property urlConfig: [ @@ -131,7 +281,7 @@ config = { id: "noglobals", label: "Check for Globals", tooltip: "Enabling this will test if any test introduces new properties on the " + - "`window` object. Stored as query-strings." + "global object (`window` in Browsers). Stored as query-strings." }, { id: "notrycatch", @@ -144,6 +294,9 @@ config = { // Set of all modules. modules: [], + // Stack of nested modules + moduleStack: [], + // The first unnamed module currentModule: { name: "", @@ -153,128 +306,230 @@ config = { callbacks: {} }; +var urlParams = defined.document ? getUrlParams() : {}; + // Push a loose unnamed module to the modules collection config.modules.push( config.currentModule ); -// Initialize more QUnit.config and QUnit.urlParams -(function() { - var i, current, - location = window.location || { search: "", protocol: "file:" }, - params = location.search.slice( 1 ).split( "&" ), - length = params.length, - urlParams = {}; +if ( urlParams.filter === true ) { + delete urlParams.filter; +} - if ( params[ 0 ] ) { - for ( i = 0; i < length; i++ ) { - current = params[ i ].split( "=" ); - current[ 0 ] = decodeURIComponent( current[ 0 ] ); +// String search anywhere in moduleName+testName +config.filter = urlParams.filter; - // allow just a key to turn on a flag, e.g., test.html?noglobals - current[ 1 ] = current[ 1 ] ? decodeURIComponent( current[ 1 ] ) : true; - if ( urlParams[ current[ 0 ] ] ) { - urlParams[ current[ 0 ] ] = [].concat( urlParams[ current[ 0 ] ], current[ 1 ] ); - } else { - urlParams[ current[ 0 ] ] = current[ 1 ]; - } - } +config.testId = []; +if ( urlParams.testId ) { + // Ensure that urlParams.testId is an array + urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); + for (var i = 0; i < urlParams.testId.length; i++ ) { + config.testId.push( urlParams.testId[ i ] ); } +} - if ( urlParams.filter === true ) { - delete urlParams.filter; - } +var loggingCallbacks = {}; - QUnit.urlParams = urlParams; +// Register logging callbacks +function registerLoggingCallbacks( obj ) { + var i, l, key, + callbackNames = [ "begin", "done", "log", "testStart", "testDone", + "moduleStart", "moduleDone" ]; - // String search anywhere in moduleName+testName - config.filter = urlParams.filter; + function registerLoggingCallback( key ) { + var loggingCallback = function( callback ) { + if ( objectType( callback ) !== "function" ) { + throw new Error( + "QUnit logging methods require a callback function as their first parameters." + ); + } - if ( urlParams.maxDepth ) { - config.maxDepth = parseInt( urlParams.maxDepth, 10 ) === -1 ? - Number.POSITIVE_INFINITY : - urlParams.maxDepth; - } + config.callbacks[ key ].push( callback ); + }; - config.testId = []; - if ( urlParams.testId ) { + // DEPRECATED: This will be removed on QUnit 2.0.0+ + // Stores the registered functions allowing restoring + // at verifyLoggingCallbacks() if modified + loggingCallbacks[ key ] = loggingCallback; - // Ensure that urlParams.testId is an array - urlParams.testId = decodeURIComponent( urlParams.testId ).split( "," ); - for ( i = 0; i < urlParams.testId.length; i++ ) { - config.testId.push( urlParams.testId[ i ] ); - } + return loggingCallback; } - // Figure out if we're running the tests from a server or not - QUnit.isLocal = location.protocol === "file:"; - - // Expose the current QUnit version - QUnit.version = "1.18.1-pre"; -}()); - -// Root QUnit object. -// `QUnit` initialized at top of scope -extend( QUnit, { - - // call on start of module test to prepend name to all tests - module: function( name, testEnvironment ) { - var currentModule = { - name: name, - testEnvironment: testEnvironment, - tests: [] - }; + for ( i = 0, l = callbackNames.length; i < l; i++ ) { + key = callbackNames[ i ]; - // DEPRECATED: handles setup/teardown functions, - // beforeEach and afterEach should be used instead - if ( testEnvironment && testEnvironment.setup ) { - testEnvironment.beforeEach = testEnvironment.setup; - delete testEnvironment.setup; - } - if ( testEnvironment && testEnvironment.teardown ) { - testEnvironment.afterEach = testEnvironment.teardown; - delete testEnvironment.teardown; + // Initialize key collection of logging callback + if ( objectType( config.callbacks[ key ] ) === "undefined" ) { + config.callbacks[ key ] = []; } - config.modules.push( currentModule ); - config.currentModule = currentModule; - }, + obj[ key ] = registerLoggingCallback( key ); + } +} - // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. - asyncTest: function( testName, expected, callback ) { - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } +function runLoggingCallbacks( key, args ) { + var i, l, callbacks; - QUnit.test( testName, expected, callback, true ); - }, + callbacks = config.callbacks[ key ]; + for ( i = 0, l = callbacks.length; i < l; i++ ) { + callbacks[ i ]( args ); + } +} - test: function( testName, expected, callback, async ) { - var test; +// DEPRECATED: This will be removed on 2.0.0+ +// This function verifies if the loggingCallbacks were modified by the user +// If so, it will restore it, assign the given callback and print a console warning +function verifyLoggingCallbacks() { + var loggingCallback, userCallback; - if ( arguments.length === 2 ) { - callback = expected; - expected = null; - } + for ( loggingCallback in loggingCallbacks ) { + if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { - test = new Test({ - testName: testName, - expected: expected, - async: async, - callback: callback - }); + userCallback = QUnit[ loggingCallback ]; - test.queue(); - }, + // Restore the callback function + QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; - skip: function( testName ) { - var test = new Test({ - testName: testName, - skip: true - }); + // Assign the deprecated given callback + QUnit[ loggingCallback ]( userCallback ); + + if ( global.console && global.console.warn ) { + global.console.warn( + "QUnit." + loggingCallback + " was replaced with a new value.\n" + + "Please, check out the documentation on how to apply logging callbacks.\n" + + "Reference: http://api.qunitjs.com/category/callbacks/" + ); + } + } + } +} + +( function() { + if ( !defined.document ) { + return; + } + + // `onErrorFnPrev` initialized at top of scope + // Preserve other handlers + var onErrorFnPrev = window.onerror; + + // Cover uncaught exceptions + // Returning true will suppress the default browser handler, + // returning false will let it run. + window.onerror = function( error, filePath, linerNr ) { + var ret = false; + if ( onErrorFnPrev ) { + ret = onErrorFnPrev( error, filePath, linerNr ); + } + + // Treat return value as window.onerror itself does, + // Only do our handling if not suppressed. + if ( ret !== true ) { + if ( QUnit.config.current ) { + if ( QUnit.config.current.ignoreGlobalErrors ) { + return true; + } + QUnit.pushFailure( error, filePath + ":" + linerNr ); + } else { + QUnit.test( "global failure", extend(function() { + QUnit.pushFailure( error, filePath + ":" + linerNr ); + }, { validTest: true } ) ); + } + return false; + } + + return ret; + }; +} )(); + +QUnit.urlParams = urlParams; + +// Figure out if we're running the tests from a server or not +QUnit.isLocal = !( defined.document && window.location.protocol !== "file:" ); + +// Expose the current QUnit version +QUnit.version = "1.20.0"; + +extend( QUnit, { + + // call on start of module test to prepend name to all tests + module: function( name, testEnvironment, executeNow ) { + var module, moduleFns; + var currentModule = config.currentModule; + + if ( arguments.length === 2 ) { + if ( testEnvironment instanceof Function ) { + executeNow = testEnvironment; + testEnvironment = undefined; + } + } + + // DEPRECATED: handles setup/teardown functions, + // beforeEach and afterEach should be used instead + if ( testEnvironment && testEnvironment.setup ) { + testEnvironment.beforeEach = testEnvironment.setup; + delete testEnvironment.setup; + } + if ( testEnvironment && testEnvironment.teardown ) { + testEnvironment.afterEach = testEnvironment.teardown; + delete testEnvironment.teardown; + } + + module = createModule(); + + moduleFns = { + beforeEach: setHook( module, "beforeEach" ), + afterEach: setHook( module, "afterEach" ) + }; + + if ( executeNow instanceof Function ) { + config.moduleStack.push( module ); + setCurrentModule( module ); + executeNow.call( module.testEnvironment, moduleFns ); + config.moduleStack.pop(); + module = module.parentModule || currentModule; + } + + setCurrentModule( module ); + + function createModule() { + var parentModule = config.moduleStack.length ? + config.moduleStack.slice( -1 )[ 0 ] : null; + var moduleName = parentModule !== null ? + [ parentModule.name, name ].join( " > " ) : name; + var module = { + name: moduleName, + parentModule: parentModule, + tests: [] + }; + + var env = {}; + if ( parentModule ) { + extend( env, parentModule.testEnvironment ); + delete env.beforeEach; + delete env.afterEach; + } + extend( env, testEnvironment ); + module.testEnvironment = env; + + config.modules.push( module ); + return module; + } + + function setCurrentModule( module ) { + config.currentModule = module; + } - test.queue(); }, + // DEPRECATED: QUnit.asyncTest() will be removed in QUnit 2.0. + asyncTest: asyncTest, + + test: test, + + skip: skip, + + only: only, + // DEPRECATED: The functionality of QUnit.start() will be altered in QUnit 2.0. // In QUnit 2.0, invoking it will ONLY affect the `QUnit.config.autostart` blocking behavior. start: function( count ) { @@ -301,6 +556,17 @@ extend( QUnit, { // If a test is running, adjust its semaphore config.current.semaphore -= count || 1; + // If semaphore is non-numeric, throw error + if ( isNaN( config.current.semaphore ) ) { + config.current.semaphore = 0; + + QUnit.pushFailure( + "Called start() with a non-numeric decrement.", + sourceFromStacktrace( 2 ) + ); + return; + } + // Don't start until equal number of stop-calls if ( config.current.semaphore > 0 ) { return; @@ -337,43 +603,9 @@ extend( QUnit, { config: config, - // Safe object type checking - is: function( type, obj ) { - return QUnit.objectType( obj ) === type; - }, - - objectType: function( obj ) { - if ( typeof obj === "undefined" ) { - return "undefined"; - } - - // Consider: typeof null === object - if ( obj === null ) { - return "null"; - } - - var match = toString.call( obj ).match( /^\[object\s(.*)\]$/ ), - type = match && match[ 1 ] || ""; + is: is, - switch ( type ) { - case "Number": - if ( isNaN( obj ) ) { - return "nan"; - } - return "number"; - case "String": - case "Boolean": - case "Array": - case "Date": - case "RegExp": - case "Function": - return type.toLowerCase(); - } - if ( typeof obj === "object" ) { - return "object"; - } - return undefined; - }, + objectType: objectType, extend: extend, @@ -403,199 +635,7 @@ extend( QUnit, { } }); -// Register logging callbacks -(function() { - var i, l, key, - callbacks = [ "begin", "done", "log", "testStart", "testDone", - "moduleStart", "moduleDone" ]; - - function registerLoggingCallback( key ) { - var loggingCallback = function( callback ) { - if ( QUnit.objectType( callback ) !== "function" ) { - throw new Error( - "QUnit logging methods require a callback function as their first parameters." - ); - } - - config.callbacks[ key ].push( callback ); - }; - - // DEPRECATED: This will be removed on QUnit 2.0.0+ - // Stores the registered functions allowing restoring - // at verifyLoggingCallbacks() if modified - loggingCallbacks[ key ] = loggingCallback; - - return loggingCallback; - } - - for ( i = 0, l = callbacks.length; i < l; i++ ) { - key = callbacks[ i ]; - - // Initialize key collection of logging callback - if ( QUnit.objectType( config.callbacks[ key ] ) === "undefined" ) { - config.callbacks[ key ] = []; - } - - QUnit[ key ] = registerLoggingCallback( key ); - } -})(); - -// `onErrorFnPrev` initialized at top of scope -// Preserve other handlers -onErrorFnPrev = window.onerror; - -// Cover uncaught exceptions -// Returning true will suppress the default browser handler, -// returning false will let it run. -window.onerror = function( error, filePath, linerNr ) { - var ret = false; - if ( onErrorFnPrev ) { - ret = onErrorFnPrev( error, filePath, linerNr ); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if ( ret !== true ) { - if ( QUnit.config.current ) { - if ( QUnit.config.current.ignoreGlobalErrors ) { - return true; - } - QUnit.pushFailure( error, filePath + ":" + linerNr ); - } else { - QUnit.test( "global failure", extend(function() { - QUnit.pushFailure( error, filePath + ":" + linerNr ); - }, { validTest: true } ) ); - } - return false; - } - - return ret; -}; - -function done() { - var runtime, passed; - - config.autorun = true; - - // Log the last module results - if ( config.previousModule ) { - runLoggingCallbacks( "moduleDone", { - name: config.previousModule.name, - tests: config.previousModule.tests, - failed: config.moduleStats.bad, - passed: config.moduleStats.all - config.moduleStats.bad, - total: config.moduleStats.all, - runtime: now() - config.moduleStats.started - }); - } - delete config.previousModule; - - runtime = now() - config.started; - passed = config.stats.all - config.stats.bad; - - runLoggingCallbacks( "done", { - failed: config.stats.bad, - passed: passed, - total: config.stats.all, - runtime: runtime - }); -} - -// Doesn't support IE6 to IE9, it will return undefined on these browsers -// See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack -function extractStacktrace( e, offset ) { - offset = offset === undefined ? 4 : offset; - - var stack, include, i; - - if ( e.stack ) { - stack = e.stack.split( "\n" ); - if ( /^error$/i.test( stack[ 0 ] ) ) { - stack.shift(); - } - if ( fileName ) { - include = []; - for ( i = offset; i < stack.length; i++ ) { - if ( stack[ i ].indexOf( fileName ) !== -1 ) { - break; - } - include.push( stack[ i ] ); - } - if ( include.length ) { - return include.join( "\n" ); - } - } - return stack[ offset ]; - - // Support: Safari <=6 only - } else if ( e.sourceURL ) { - - // exclude useless self-reference for generated Error objects - if ( /qunit.js$/.test( e.sourceURL ) ) { - return; - } - - // for actual exceptions, this is useful - return e.sourceURL + ":" + e.line; - } -} - -function sourceFromStacktrace( offset ) { - var error = new Error(); - - // Support: Safari <=7 only, IE <=10 - 11 only - // Not all browsers generate the `stack` property for `new Error()`, see also #636 - if ( !error.stack ) { - try { - throw error; - } catch ( err ) { - error = err; - } - } - - return extractStacktrace( error, offset ); -} - -function synchronize( callback, last ) { - if ( QUnit.objectType( callback ) === "array" ) { - while ( callback.length ) { - synchronize( callback.shift() ); - } - return; - } - config.queue.push( callback ); - - if ( config.autorun && !config.blocking ) { - process( last ); - } -} - -function process( last ) { - function next() { - process( last ); - } - var start = now(); - config.depth = ( config.depth || 0 ) + 1; - - while ( config.queue.length && !config.blocking ) { - if ( !defined.setTimeout || config.updateRate <= 0 || - ( ( now() - start ) < config.updateRate ) ) { - if ( config.current ) { - - // Reset async tracking for each phase of the Test lifecycle - config.current.usedAsync = false; - } - config.queue.shift()(); - } else { - setTimeout( next, 13 ); - break; - } - } - config.depth--; - if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { - done(); - } -} +registerLoggingCallbacks( QUnit ); function begin() { var i, l, @@ -633,23 +673,30 @@ function begin() { process( true ); } -function resumeProcessing() { - runStarted = true; +function process( last ) { + function next() { + process( last ); + } + var start = now(); + config.depth = ( config.depth || 0 ) + 1; - // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) - if ( defined.setTimeout ) { - setTimeout(function() { - if ( config.current && config.current.semaphore > 0 ) { - return; - } - if ( config.timeout ) { - clearTimeout( config.timeout ); - } + while ( config.queue.length && !config.blocking ) { + if ( !defined.setTimeout || config.updateRate <= 0 || + ( ( now() - start ) < config.updateRate ) ) { + if ( config.current ) { - begin(); - }, 13 ); - } else { - begin(); + // Reset async tracking for each phase of the Test lifecycle + config.current.usedAsync = false; + } + config.queue.shift()(); + } else { + setTimeout( next, 13 ); + break; + } + } + config.depth--; + if ( last && !config.blocking && !config.queue.length && config.depth === 0 ) { + done(); } } @@ -670,127 +717,67 @@ function pauseProcessing() { } } -function saveGlobal() { - config.pollution = []; - - if ( config.noglobals ) { - for ( var key in window ) { - if ( hasOwn.call( window, key ) ) { - // in Opera sometimes DOM element ids show up here, ignore them - if ( /^qunit-test-output/.test( key ) ) { - continue; - } - config.pollution.push( key ); - } - } - } -} - -function checkPollution() { - var newGlobals, - deletedGlobals, - old = config.pollution; - - saveGlobal(); - - newGlobals = diff( config.pollution, old ); - if ( newGlobals.length > 0 ) { - QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); - } - - deletedGlobals = diff( old, config.pollution ); - if ( deletedGlobals.length > 0 ) { - QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); - } -} - -// returns a new Array with the elements that are in a but not in b -function diff( a, b ) { - var i, j, - result = a.slice(); +function resumeProcessing() { + runStarted = true; - for ( i = 0; i < result.length; i++ ) { - for ( j = 0; j < b.length; j++ ) { - if ( result[ i ] === b[ j ] ) { - result.splice( i, 1 ); - i--; - break; + // A slight delay to allow this iteration of the event loop to finish (more assertions, etc.) + if ( defined.setTimeout ) { + setTimeout(function() { + if ( config.current && config.current.semaphore > 0 ) { + return; } - } - } - return result; -} - -function extend( a, b, undefOnly ) { - for ( var prop in b ) { - if ( hasOwn.call( b, prop ) ) { - - // Avoid "Member not found" error in IE8 caused by messing with window.constructor - if ( !( prop === "constructor" && a === window ) ) { - if ( b[ prop ] === undefined ) { - delete a[ prop ]; - } else if ( !( undefOnly && typeof a[ prop ] !== "undefined" ) ) { - a[ prop ] = b[ prop ]; - } + if ( config.timeout ) { + clearTimeout( config.timeout ); } - } - } - - return a; -} - -function runLoggingCallbacks( key, args ) { - var i, l, callbacks; - - callbacks = config.callbacks[ key ]; - for ( i = 0, l = callbacks.length; i < l; i++ ) { - callbacks[ i ]( args ); - } -} -// DEPRECATED: This will be removed on 2.0.0+ -// This function verifies if the loggingCallbacks were modified by the user -// If so, it will restore it, assign the given callback and print a console warning -function verifyLoggingCallbacks() { - var loggingCallback, userCallback; - - for ( loggingCallback in loggingCallbacks ) { - if ( QUnit[ loggingCallback ] !== loggingCallbacks[ loggingCallback ] ) { - - userCallback = QUnit[ loggingCallback ]; - - // Restore the callback function - QUnit[ loggingCallback ] = loggingCallbacks[ loggingCallback ]; - - // Assign the deprecated given callback - QUnit[ loggingCallback ]( userCallback ); - - if ( window.console && window.console.warn ) { - window.console.warn( - "QUnit." + loggingCallback + " was replaced with a new value.\n" + - "Please, check out the documentation on how to apply logging callbacks.\n" + - "Reference: http://api.qunitjs.com/category/callbacks/" - ); - } - } + begin(); + }, 13 ); + } else { + begin(); } } -// from jquery.js -function inArray( elem, array ) { - if ( array.indexOf ) { - return array.indexOf( elem ); +function done() { + var runtime, passed; + + config.autorun = true; + + // Log the last module results + if ( config.previousModule ) { + runLoggingCallbacks( "moduleDone", { + name: config.previousModule.name, + tests: config.previousModule.tests, + failed: config.moduleStats.bad, + passed: config.moduleStats.all - config.moduleStats.bad, + total: config.moduleStats.all, + runtime: now() - config.moduleStats.started + }); } + delete config.previousModule; - for ( var i = 0, length = array.length; i < length; i++ ) { - if ( array[ i ] === elem ) { - return i; - } + runtime = now() - config.started; + passed = config.stats.all - config.stats.bad; + + runLoggingCallbacks( "done", { + failed: config.stats.bad, + passed: passed, + total: config.stats.all, + runtime: runtime + }); +} + +function setHook( module, hookName ) { + if ( module.testEnvironment === undefined ) { + module.testEnvironment = {}; } - return -1; + return function( callback ) { + module.testEnvironment[ hookName ] = callback; + }; } +var focused = false; + function Test( settings ) { var i, l; @@ -863,9 +850,11 @@ Test.prototype = { config.current = this; + if ( this.module.testEnvironment ) { + delete this.module.testEnvironment.beforeEach; + delete this.module.testEnvironment.afterEach; + } this.testEnvironment = extend( {}, this.module.testEnvironment ); - delete this.testEnvironment.beforeEach; - delete this.testEnvironment.afterEach; this.started = now(); runLoggingCallbacks( "testStart", { @@ -891,14 +880,12 @@ Test.prototype = { this.callbackStarted = now(); if ( config.notrycatch ) { - promise = this.callback.call( this.testEnvironment, this.assert ); - this.resolvePromise( promise ); + runTest( this ); return; } try { - promise = this.callback.call( this.testEnvironment, this.assert ); - this.resolvePromise( promise ); + runTest( this ); } catch ( e ) { this.pushFailure( "Died on test #" + ( this.assertions.length + 1 ) + " " + this.stack + ": " + ( e.message || e ), extractStacktrace( e, 0 ) ); @@ -911,6 +898,11 @@ Test.prototype = { QUnit.start(); } } + + function runTest( test ) { + promise = test.callback.call( test.testEnvironment, test.assert ); + test.resolvePromise( promise ); + } }, after: function() { @@ -923,16 +915,19 @@ Test.prototype = { return function runHook() { config.current = test; if ( config.notrycatch ) { - promise = hook.call( test.testEnvironment, test.assert ); - test.resolvePromise( promise, hookName ); + callHook(); return; } try { - promise = hook.call( test.testEnvironment, test.assert ); - test.resolvePromise( promise, hookName ); + callHook(); } catch ( error ) { test.pushFailure( hookName + " failed on " + test.testName + ": " + - ( error.message || error ), extractStacktrace( error, 0 ) ); + ( error.message || error ), extractStacktrace( error, 0 ) ); + } + + function callHook() { + promise = hook.call( test.testEnvironment, test.assert ); + test.resolvePromise( promise, hookName ); } }; }, @@ -941,16 +936,20 @@ Test.prototype = { hooks: function( handler ) { var hooks = []; - // Hooks are ignored on skipped tests - if ( this.skip ) { - return hooks; + function processHooks( test, module ) { + if ( module.parentModule ) { + processHooks( test, module.parentModule ); + } + if ( module.testEnvironment && + QUnit.objectType( module.testEnvironment[ handler ] ) === "function" ) { + hooks.push( test.queueHook( module.testEnvironment[ handler ], handler ) ); + } } - if ( this.module.testEnvironment && - QUnit.objectType( this.module.testEnvironment[ handler ] ) === "function" ) { - hooks.push( this.queueHook( this.module.testEnvironment[ handler ], handler ) ); + // Hooks are ignored on skipped tests + if ( !this.skip ) { + processHooks( this, this.module ); } - return hooks; }, @@ -1011,7 +1010,7 @@ Test.prototype = { }, queue: function() { - var bad, + var priority, test = this; if ( !this.valid() ) { @@ -1027,7 +1026,6 @@ Test.prototype = { }, test.hooks( "beforeEach" ), - function() { test.run(); }, @@ -1043,16 +1041,11 @@ Test.prototype = { ]); } - // `bad` initialized at top of scope - // defer when previous test run passed, if storage is available - bad = QUnit.config.reorder && defined.sessionStorage && + // Prioritize previously failed tests, detected from sessionStorage + priority = QUnit.config.reorder && defined.sessionStorage && +sessionStorage.getItem( "qunit-test-" + this.module.name + "-" + this.testName ); - if ( bad ) { - run(); - } else { - synchronize( run, true ); - } + return synchronize( run, priority ); }, push: function( result, actual, expected, message, negative ) { @@ -1086,7 +1079,7 @@ Test.prototype = { }, pushFailure: function( message, source, actual ) { - if ( !this instanceof Test ) { + if ( !( this instanceof Test ) ) { throw new Error( "pushFailure() assertion outside test context, was " + sourceFromStacktrace( 2 ) ); } @@ -1122,7 +1115,7 @@ Test.prototype = { QUnit.stop(); then.call( promise, - QUnit.start, + function() { QUnit.start(); }, function( error ) { message = "Promise rejected " + ( !phase ? "during" : phase.replace( /Each$/, "" ) ) + @@ -1146,6 +1139,17 @@ Test.prototype = { module = QUnit.urlParams.module && QUnit.urlParams.module.toLowerCase(), fullName = ( this.module.name + ": " + this.testName ).toLowerCase(); + function testInModuleChain( testModule ) { + var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; + if ( testModuleName === module ) { + return true; + } else if ( testModule.parentModule ) { + return testInModuleChain( testModule.parentModule ); + } else { + return false; + } + } + // Internally-generated tests are always valid if ( this.callback && this.callback.validTest ) { return true; @@ -1155,7 +1159,7 @@ Test.prototype = { return false; } - if ( module && ( !this.module.name || this.module.name.toLowerCase() !== module ) ) { + if ( module && !testInModuleChain( this.module ) ) { return false; } @@ -1176,7 +1180,6 @@ Test.prototype = { // Otherwise, do the opposite return !include; } - }; // Resets the test setup. Useful for tests that modify the DOM. @@ -1189,7 +1192,7 @@ QUnit.reset = function() { // Return on non-browser environments // This is necessary to not break on node tests - if ( typeof window === "undefined" ) { + if ( !defined.document ) { return; } @@ -1237,6 +1240,145 @@ function generateHash( module, testName ) { return hex.slice( -8 ); } +function synchronize( callback, priority ) { + var last = !priority; + + if ( QUnit.objectType( callback ) === "array" ) { + while ( callback.length ) { + synchronize( callback.shift() ); + } + return; + } + + if ( priority ) { + priorityFill( callback ); + } else { + config.queue.push( callback ); + } + + if ( config.autorun && !config.blocking ) { + process( last ); + } +} + +// Place previously failed tests on a queue priority line, respecting the order they get assigned. +function priorityFill( callback ) { + var queue, prioritizedQueue; + + queue = config.queue.slice( priorityFill.pos ); + prioritizedQueue = config.queue.slice( 0, -config.queue.length + priorityFill.pos ); + + queue.unshift( callback ); + queue.unshift.apply( queue, prioritizedQueue ); + + config.queue = queue; + + priorityFill.pos += 1; +} +priorityFill.pos = 0; + +function saveGlobal() { + config.pollution = []; + + if ( config.noglobals ) { + for ( var key in global ) { + if ( hasOwn.call( global, key ) ) { + + // in Opera sometimes DOM element ids show up here, ignore them + if ( /^qunit-test-output/.test( key ) ) { + continue; + } + config.pollution.push( key ); + } + } + } +} + +function checkPollution() { + var newGlobals, + deletedGlobals, + old = config.pollution; + + saveGlobal(); + + newGlobals = diff( config.pollution, old ); + if ( newGlobals.length > 0 ) { + QUnit.pushFailure( "Introduced global variable(s): " + newGlobals.join( ", " ) ); + } + + deletedGlobals = diff( old, config.pollution ); + if ( deletedGlobals.length > 0 ) { + QUnit.pushFailure( "Deleted global variable(s): " + deletedGlobals.join( ", " ) ); + } +} + +// Will be exposed as QUnit.asyncTest +function asyncTest( testName, expected, callback ) { + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + QUnit.test( testName, expected, callback, true ); +} + +// Will be exposed as QUnit.test +function test( testName, expected, callback, async ) { + if ( focused ) { return; } + + var newTest; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + newTest = new Test({ + testName: testName, + expected: expected, + async: async, + callback: callback + }); + + newTest.queue(); +} + +// Will be exposed as QUnit.skip +function skip( testName ) { + if ( focused ) { return; } + + var test = new Test({ + testName: testName, + skip: true + }); + + test.queue(); +} + +// Will be exposed as QUnit.only +function only( testName, expected, callback, async ) { + var newTest; + + if ( focused ) { return; } + + QUnit.config.queue.length = 0; + focused = true; + + if ( arguments.length === 2 ) { + callback = expected; + expected = null; + } + + newTest = new Test({ + testName: testName, + expected: expected, + async: async, + callback: callback + }); + + newTest.queue(); +} + function Assert( testContext ) { this.test = testContext; } @@ -1254,25 +1396,36 @@ QUnit.assert = Assert.prototype = { } }, - // Increment this Test's semaphore counter, then return a single-use function that + // Increment this Test's semaphore counter, then return a function that // decrements that counter a maximum of once. - async: function() { + async: function( count ) { var test = this.test, - popped = false; + popped = false, + acceptCallCount = count; + + if ( typeof acceptCallCount === "undefined" ) { + acceptCallCount = 1; + } test.semaphore += 1; test.usedAsync = true; pauseProcessing(); return function done() { - if ( !popped ) { - test.semaphore -= 1; - popped = true; - resumeProcessing(); - } else { - test.pushFailure( "Called the callback returned from `assert.async` more than once", + + if ( popped ) { + test.pushFailure( "Too many calls to the `assert.async` callback", sourceFromStacktrace( 2 ) ); + return; } + acceptCallCount -= 1; + if ( acceptCallCount > 0 ) { + return; + } + + test.semaphore -= 1; + popped = true; + resumeProcessing(); }; }, @@ -1410,229 +1563,305 @@ QUnit.assert = Assert.prototype = { } }; -// Provide an alternative to assert.throws(), for enviroments that consider throws a reserved word +// Provide an alternative to assert.throws(), for environments that consider throws a reserved word // Known to us are: Closure Compiler, Narwhal (function() { /*jshint sub:true */ Assert.prototype.raises = Assert.prototype[ "throws" ]; }()); +function errorString( error ) { + var name, message, + resultErrorString = error.toString(); + if ( resultErrorString.substring( 0, 7 ) === "[object" ) { + name = error.name ? error.name.toString() : "Error"; + message = error.message ? error.message.toString() : ""; + if ( name && message ) { + return name + ": " + message; + } else if ( name ) { + return name; + } else if ( message ) { + return message; + } else { + return "Error"; + } + } else { + return resultErrorString; + } +} + // Test for equality any JavaScript type. // Author: Philippe Rathé QUnit.equiv = (function() { - // Call the o related callback with the given arguments. - function bindCallbacks( o, callbacks, args ) { - var prop = QUnit.objectType( o ); - if ( prop ) { - if ( QUnit.objectType( callbacks[ prop ] ) === "function" ) { - return callbacks[ prop ].apply( callbacks, args ); - } else { - return callbacks[ prop ]; // or undefined - } - } - } + // Stack to decide between skip/abort functions + var callers = []; + + // Stack to avoiding loops from circular referencing + var parents = []; + var parentsB = []; + + function useStrictEquality( b, a ) { - // the real equiv function - var innerEquiv, + /*jshint eqeqeq:false */ + if ( b instanceof a.constructor || a instanceof b.constructor ) { - // stack to decide between skip/abort functions - callers = [], + // To catch short annotation VS 'new' annotation of a declaration. e.g.: + // `var i = 1;` + // `var j = new Number(1);` + return a == b; + } else { + return a === b; + } + } - // stack to avoiding loops from circular referencing - parents = [], - parentsB = [], + function compareConstructors( a, b ) { + var getProto = Object.getPrototypeOf || function( obj ) { - getProto = Object.getPrototypeOf || function( obj ) { - /* jshint camelcase: false, proto: true */ + /*jshint proto: true */ return obj.__proto__; - }, - callbacks = (function() { + }; + var protoA = getProto( a ); + var protoB = getProto( b ); - // for string, boolean, number and null - function useStrictEquality( b, a ) { + // Comparing constructors is more strict than using `instanceof` + if ( a.constructor === b.constructor ) { + return true; + } - /*jshint eqeqeq:false */ - if ( b instanceof a.constructor || a instanceof b.constructor ) { + // Ref #851 + // If the obj prototype descends from a null constructor, treat it + // as a null prototype. + if ( protoA && protoA.constructor === null ) { + protoA = null; + } + if ( protoB && protoB.constructor === null ) { + protoB = null; + } - // to catch short annotation VS 'new' annotation of a - // declaration - // e.g. var i = 1; - // var j = new Number(1); - return a == b; - } else { - return a === b; - } - } + // Allow objects with no prototype to be equivalent to + // objects with Object as their constructor. + if ( ( protoA === null && protoB === Object.prototype ) || + ( protoB === null && protoA === Object.prototype ) ) { + return true; + } - return { - "string": useStrictEquality, - "boolean": useStrictEquality, - "number": useStrictEquality, - "null": useStrictEquality, - "undefined": useStrictEquality, + return false; + } - "nan": function( b ) { - return isNaN( b ); - }, + var callbacks = { + "string": useStrictEquality, + "boolean": useStrictEquality, + "number": useStrictEquality, + "null": useStrictEquality, + "undefined": useStrictEquality, + "symbol": useStrictEquality, - "date": function( b, a ) { - return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); - }, + "nan": function( b ) { + return isNaN( b ); + }, - "regexp": function( b, a ) { - return QUnit.objectType( b ) === "regexp" && + "date": function( b, a ) { + return QUnit.objectType( b ) === "date" && a.valueOf() === b.valueOf(); + }, - // the regex itself - a.source === b.source && + "regexp": function( b, a ) { + return QUnit.objectType( b ) === "regexp" && - // and its modifiers - a.global === b.global && + // The regex itself + a.source === b.source && - // (gmi) ... - a.ignoreCase === b.ignoreCase && - a.multiline === b.multiline && - a.sticky === b.sticky; - }, + // And its modifiers + a.global === b.global && - // - skip when the property is a method of an instance (OOP) - // - abort otherwise, - // initial === would have catch identical references anyway - "function": function() { - var caller = callers[ callers.length - 1 ]; - return caller !== Object && typeof caller !== "undefined"; - }, + // (gmi) ... + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline && + a.sticky === b.sticky; + }, - "array": function( b, a ) { - var i, j, len, loop, aCircular, bCircular; + // - skip when the property is a method of an instance (OOP) + // - abort otherwise, + // initial === would have catch identical references anyway + "function": function() { + var caller = callers[ callers.length - 1 ]; + return caller !== Object && typeof caller !== "undefined"; + }, - // b could be an object literal here - if ( QUnit.objectType( b ) !== "array" ) { - return false; - } + "array": function( b, a ) { + var i, j, len, loop, aCircular, bCircular; - len = a.length; - if ( len !== b.length ) { - // safe and faster - return false; - } + // b could be an object literal here + if ( QUnit.objectType( b ) !== "array" ) { + return false; + } - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - for ( i = 0; i < len; i++ ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - parents.pop(); - parentsB.pop(); - return false; - } - } - } - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + len = a.length; + if ( len !== b.length ) { + // safe and faster + return false; + } + + // Track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + for ( i = 0; i < len; i++ ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[ j ] === a[ i ]; + bCircular = parentsB[ j ] === b[ i ]; + if ( aCircular || bCircular ) { + if ( a[ i ] === b[ i ] || aCircular && bCircular ) { + loop = true; + } else { parents.pop(); parentsB.pop(); return false; } } + } + if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { parents.pop(); parentsB.pop(); - return true; - }, + return false; + } + } + parents.pop(); + parentsB.pop(); + return true; + }, + + "set": function( b, a ) { + var aArray, bArray; - "object": function( b, a ) { + // `b` could be any object here + if ( QUnit.objectType( b ) !== "set" ) { + return false; + } - /*jshint forin:false */ - var i, j, loop, aCircular, bCircular, - // Default to true - eq = true, - aProperties = [], - bProperties = []; + aArray = []; + a.forEach( function( v ) { + aArray.push( v ); + }); + bArray = []; + b.forEach( function( v ) { + bArray.push( v ); + }); - // comparing constructors is more strict than using - // instanceof - if ( a.constructor !== b.constructor ) { + return innerEquiv( bArray, aArray ); + }, - // Allow objects with no prototype to be equivalent to - // objects with Object as their constructor. - if ( !( ( getProto( a ) === null && getProto( b ) === Object.prototype ) || - ( getProto( b ) === null && getProto( a ) === Object.prototype ) ) ) { - return false; - } - } + "map": function( b, a ) { + var aArray, bArray; - // stack constructor before traversing properties - callers.push( a.constructor ); - - // track reference to avoid circular references - parents.push( a ); - parentsB.push( b ); - - // be strict: don't ensure hasOwnProperty and go deep - for ( i in a ) { - loop = false; - for ( j = 0; j < parents.length; j++ ) { - aCircular = parents[ j ] === a[ i ]; - bCircular = parentsB[ j ] === b[ i ]; - if ( aCircular || bCircular ) { - if ( a[ i ] === b[ i ] || aCircular && bCircular ) { - loop = true; - } else { - eq = false; - break; - } - } - } - aProperties.push( i ); - if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + // `b` could be any object here + if ( QUnit.objectType( b ) !== "map" ) { + return false; + } + + aArray = []; + a.forEach( function( v, k ) { + aArray.push( [ k, v ] ); + }); + bArray = []; + b.forEach( function( v, k ) { + bArray.push( [ k, v ] ); + }); + + return innerEquiv( bArray, aArray ); + }, + + "object": function( b, a ) { + var i, j, loop, aCircular, bCircular; + + // Default to true + var eq = true; + var aProperties = []; + var bProperties = []; + + if ( compareConstructors( a, b ) === false ) { + return false; + } + + // Stack constructor before traversing properties + callers.push( a.constructor ); + + // Track reference to avoid circular references + parents.push( a ); + parentsB.push( b ); + + // Be strict: don't ensure hasOwnProperty and go deep + for ( i in a ) { + loop = false; + for ( j = 0; j < parents.length; j++ ) { + aCircular = parents[ j ] === a[ i ]; + bCircular = parentsB[ j ] === b[ i ]; + if ( aCircular || bCircular ) { + if ( a[ i ] === b[ i ] || aCircular && bCircular ) { + loop = true; + } else { eq = false; break; } } + } + aProperties.push( i ); + if ( !loop && !innerEquiv( a[ i ], b[ i ] ) ) { + eq = false; + break; + } + } - parents.pop(); - parentsB.pop(); - callers.pop(); // unstack, we are done + parents.pop(); + parentsB.pop(); - for ( i in b ) { - bProperties.push( i ); // collect b's properties - } + // Unstack, we are done + callers.pop(); - // Ensures identical properties name - return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); - } - }; - }()); + for ( i in b ) { + + // Collect b's properties + bProperties.push( i ); + } - innerEquiv = function() { // can take multiple arguments + // Ensures identical properties name + return eq && innerEquiv( aProperties.sort(), bProperties.sort() ); + } + }; + + function typeEquiv( a, b ) { + var prop = QUnit.objectType( a ); + return callbacks[ prop ]( b, a ); + } + + // The real equiv function + function innerEquiv() { var args = [].slice.apply( arguments ); if ( args.length < 2 ) { - return true; // end transition + + // End transition + return true; } return ( (function( a, b ) { if ( a === b ) { - return true; // catch the most you can + + // Catch the most you can + return true; } else if ( a === null || b === null || typeof a === "undefined" || typeof b === "undefined" || QUnit.objectType( a ) !== QUnit.objectType( b ) ) { - // don't lose time with error prone cases + // Don't lose time with error prone cases return false; } else { - return bindCallbacks( a, callbacks, [ b, a ] ); + return typeEquiv( a, b ); } - // apply transition with (1..n) arguments + // Apply transition with (1..n) arguments }( args[ 0 ], args[ 1 ] ) ) && innerEquiv.apply( this, args.splice( 1, args.length - 1 ) ) ); - }; + } return innerEquiv; }()); @@ -1902,7 +2131,7 @@ QUnit.dump = (function() { QUnit.jsDump = QUnit.dump; // For browser, export only select globals -if ( typeof window !== "undefined" ) { +if ( defined.document ) { // Deprecated // Extend assert methods to QUnit and Global scope through Backwards compatibility @@ -1941,7 +2170,8 @@ if ( typeof window !== "undefined" ) { "notDeepEqual", "strictEqual", "notStrictEqual", - "throws" + "throws", + "raises" ]; for ( i = 0, l = keys.length; i < l; i++ ) { @@ -1966,19 +2196,12 @@ if ( typeof exports !== "undefined" && exports ) { } if ( typeof define === "function" && define.amd ) { - define( function() { + define('QUnit', function() { return QUnit; } ); QUnit.config.autostart = false; } -// Get a reference to the global object, like window in browsers -}( (function() { - return this; -})() )); - -/*istanbul ignore next */ -// jscs:disable maximumLineLength /* * This file is a modified version of google-diff-match-patch's JavaScript implementation * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), @@ -2006,1067 +2229,1103 @@ if ( typeof define === "function" && define.amd ) { * * Usage: QUnit.diff(expected, actual) * - * QUnit.diff( "the quick brown fox jumped over", "the quick fox jumps over" ) === "the quick brown fox jumpsed} Array of diff tuples. - */ - DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines, optDeadline ) { - var deadline, checklines, commonlength, + /** + * Find the differences between two texts. Simplifies the problem by stripping + * any common prefix or suffix off the texts before diffing. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean=} optChecklines Optional speedup flag. If present and false, + * then don't run a line-level diff first to identify the changed areas. + * Defaults to true, which does a faster, slightly less optimal diff. + * @return {!Array.} Array of diff tuples. + */ + DiffMatchPatch.prototype.DiffMain = function( text1, text2, optChecklines ) { + var deadline, checklines, commonlength, commonprefix, commonsuffix, diffs; - // Set a deadline by which time the diff must be complete. - if ( typeof optDeadline === "undefined" ) { - if ( this.DiffTimeout <= 0 ) { - optDeadline = Number.MAX_VALUE; - } else { - optDeadline = ( new Date() ).getTime() + this.DiffTimeout * 1000; - } - } - deadline = optDeadline; - - // Check for null inputs. - if ( text1 === null || text2 === null ) { - throw new Error( "Null input. (DiffMain)" ); - } - - // Check for equality (speedup). - if ( text1 === text2 ) { - if ( text1 ) { - return [ - [ DIFF_EQUAL, text1 ] - ]; - } - return []; - } - - if ( typeof optChecklines === "undefined" ) { - optChecklines = true; - } - - checklines = optChecklines; - - // Trim off common prefix (speedup). - commonlength = this.diffCommonPrefix( text1, text2 ); - commonprefix = text1.substring( 0, commonlength ); - text1 = text1.substring( commonlength ); - text2 = text2.substring( commonlength ); - - // Trim off common suffix (speedup). - ///////// - commonlength = this.diffCommonSuffix( text1, text2 ); - commonsuffix = text1.substring( text1.length - commonlength ); - text1 = text1.substring( 0, text1.length - commonlength ); - text2 = text2.substring( 0, text2.length - commonlength ); - - // Compute the diff on the middle block. - diffs = this.diffCompute( text1, text2, checklines, deadline ); - - // Restore the prefix and suffix. - if ( commonprefix ) { - diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); - } - if ( commonsuffix ) { - diffs.push( [ DIFF_EQUAL, commonsuffix ] ); - } - this.diffCleanupMerge( diffs ); - return diffs; - }; - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { - var changes, equalities, equalitiesLength, lastequality, + + // The diff must be complete in up to 1 second. + deadline = ( new Date() ).getTime() + 1000; + + // Check for null inputs. + if ( text1 === null || text2 === null ) { + throw new Error( "Null input. (DiffMain)" ); + } + + // Check for equality (speedup). + if ( text1 === text2 ) { + if ( text1 ) { + return [ + [ DIFF_EQUAL, text1 ] + ]; + } + return []; + } + + if ( typeof optChecklines === "undefined" ) { + optChecklines = true; + } + + checklines = optChecklines; + + // Trim off common prefix (speedup). + commonlength = this.diffCommonPrefix( text1, text2 ); + commonprefix = text1.substring( 0, commonlength ); + text1 = text1.substring( commonlength ); + text2 = text2.substring( commonlength ); + + // Trim off common suffix (speedup). + commonlength = this.diffCommonSuffix( text1, text2 ); + commonsuffix = text1.substring( text1.length - commonlength ); + text1 = text1.substring( 0, text1.length - commonlength ); + text2 = text2.substring( 0, text2.length - commonlength ); + + // Compute the diff on the middle block. + diffs = this.diffCompute( text1, text2, checklines, deadline ); + + // Restore the prefix and suffix. + if ( commonprefix ) { + diffs.unshift( [ DIFF_EQUAL, commonprefix ] ); + } + if ( commonsuffix ) { + diffs.push( [ DIFF_EQUAL, commonsuffix ] ); + } + this.diffCleanupMerge( diffs ); + return diffs; + }; + + /** + * Reduce the number of edits by eliminating operationally trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupEfficiency = function( diffs ) { + var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - // Is there an insertion operation before the last equality. - preIns = false; - // Is there a deletion operation before the last equality. - preDel = false; - // Is there an insertion operation after the last equality. - postIns = false; - // Is there a deletion operation after the last equality. - postDel = false; - while ( pointer < diffs.length ) { - if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. - if ( diffs[ pointer ][ 1 ].length < this.DiffEditCost && ( postIns || postDel ) ) { - // Candidate found. - equalities[ equalitiesLength++ ] = pointer; - preIns = postIns; - preDel = postDel; - lastequality = diffs[ pointer ][ 1 ]; - } else { - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastequality = null; - } - postIns = postDel = false; - } else { // An insertion or deletion. - if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { - postDel = true; - } else { - postIns = true; - } - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || - ( ( lastequality.length < this.DiffEditCost / 2 ) && - ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { - // Duplicate record. - diffs.splice( equalities[equalitiesLength - 1], 0, [ DIFF_DELETE, lastequality ] ); - // Change second copy to insert. - diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastequality = null; - if (preIns && preDel) { - // No changes made which could affect previous entry, keep going. - postIns = postDel = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; - postIns = postDel = false; - } - changes = true; - } - } - pointer++; - } - - if ( changes ) { - this.diffCleanupMerge( diffs ); - } - }; - - /** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @param {integer} string to be beautified. - * @return {string} HTML representation. - */ - DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { - var op, data, x, html = []; - for ( x = 0; x < diffs.length; x++ ) { - op = diffs[x][0]; // Operation (insert, delete, equal) - data = diffs[x][1]; // Text of change. - switch ( op ) { - case DIFF_INSERT: - html[x] = "" + data + ""; - break; - case DIFF_DELETE: - html[x] = "" + data + ""; - break; - case DIFF_EQUAL: - html[x] = "" + data + ""; - break; - } - } - return html.join(""); - }; - - /** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ - DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { - var pointermid, pointermax, pointermin, pointerstart; - // Quick check for common null cases. - if ( !text1 || !text2 || text1.charAt(0) !== text2.charAt(0) ) { - return 0; - } - // Binary search. - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min( text1.length, text2.length ); - pointermid = pointermax; - pointerstart = 0; - while ( pointermin < pointermid ) { - if ( text1.substring( pointerstart, pointermid ) === text2.substring( pointerstart, pointermid ) ) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); - } - return pointermid; - }; - - /** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ - DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { - var pointermid, pointermax, pointermin, pointerend; - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { - return 0; - } - // Binary search. - // Performance analysis: http://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerend = 0; - while ( pointermin < pointermid ) { - if (text1.substring( text1.length - pointermid, text1.length - pointerend ) === - text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); - } - return pointermid; - }; - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { - var diffs, longtext, shorttext, i, hm, + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + // Is there an insertion operation before the last equality. + preIns = false; + // Is there a deletion operation before the last equality. + preDel = false; + // Is there an insertion operation after the last equality. + postIns = false; + // Is there a deletion operation after the last equality. + postDel = false; + while ( pointer < diffs.length ) { + + // Equality found. + if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { + if ( diffs[ pointer ][ 1 ].length < 4 && ( postIns || postDel ) ) { + + // Candidate found. + equalities[ equalitiesLength++ ] = pointer; + preIns = postIns; + preDel = postDel; + lastequality = diffs[ pointer ][ 1 ]; + } else { + + // Not a candidate, and can never become one. + equalitiesLength = 0; + lastequality = null; + } + postIns = postDel = false; + + // An insertion or deletion. + } else { + + if ( diffs[ pointer ][ 0 ] === DIFF_DELETE ) { + postDel = true; + } else { + postIns = true; + } + + /* + * Five types to be split: + * ABXYCD + * AXCD + * ABXC + * AXCD + * ABXC + */ + if ( lastequality && ( ( preIns && preDel && postIns && postDel ) || + ( ( lastequality.length < 2 ) && + ( preIns + preDel + postIns + postDel ) === 3 ) ) ) { + + // Duplicate record. + diffs.splice( + equalities[ equalitiesLength - 1 ], + 0, + [ DIFF_DELETE, lastequality ] + ); + + // Change second copy to insert. + diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; + equalitiesLength--; // Throw away the equality we just deleted; + lastequality = null; + if ( preIns && preDel ) { + // No changes made which could affect previous entry, keep going. + postIns = postDel = true; + equalitiesLength = 0; + } else { + equalitiesLength--; // Throw away the previous equality. + pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; + postIns = postDel = false; + } + changes = true; + } + } + pointer++; + } + + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + }; + + /** + * Convert a diff array into a pretty HTML report. + * @param {!Array.} diffs Array of diff tuples. + * @param {integer} string to be beautified. + * @return {string} HTML representation. + */ + DiffMatchPatch.prototype.diffPrettyHtml = function( diffs ) { + var op, data, x, + html = []; + for ( x = 0; x < diffs.length; x++ ) { + op = diffs[ x ][ 0 ]; // Operation (insert, delete, equal) + data = diffs[ x ][ 1 ]; // Text of change. + switch ( op ) { + case DIFF_INSERT: + html[ x ] = "" + data + ""; + break; + case DIFF_DELETE: + html[ x ] = "" + data + ""; + break; + case DIFF_EQUAL: + html[ x ] = "" + data + ""; + break; + } + } + return html.join( "" ); + }; + + /** + * Determine the common prefix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the start of each + * string. + */ + DiffMatchPatch.prototype.diffCommonPrefix = function( text1, text2 ) { + var pointermid, pointermax, pointermin, pointerstart; + // Quick check for common null cases. + if ( !text1 || !text2 || text1.charAt( 0 ) !== text2.charAt( 0 ) ) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min( text1.length, text2.length ); + pointermid = pointermax; + pointerstart = 0; + while ( pointermin < pointermid ) { + if ( text1.substring( pointerstart, pointermid ) === + text2.substring( pointerstart, pointermid ) ) { + pointermin = pointermid; + pointerstart = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); + } + return pointermid; + }; + + /** + * Determine the common suffix of two strings. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of each string. + */ + DiffMatchPatch.prototype.diffCommonSuffix = function( text1, text2 ) { + var pointermid, pointermax, pointermin, pointerend; + // Quick check for common null cases. + if ( !text1 || + !text2 || + text1.charAt( text1.length - 1 ) !== text2.charAt( text2.length - 1 ) ) { + return 0; + } + // Binary search. + // Performance analysis: http://neil.fraser.name/news/2007/10/09/ + pointermin = 0; + pointermax = Math.min( text1.length, text2.length ); + pointermid = pointermax; + pointerend = 0; + while ( pointermin < pointermid ) { + if ( text1.substring( text1.length - pointermid, text1.length - pointerend ) === + text2.substring( text2.length - pointermid, text2.length - pointerend ) ) { + pointermin = pointermid; + pointerend = pointermin; + } else { + pointermax = pointermid; + } + pointermid = Math.floor( ( pointermax - pointermin ) / 2 + pointermin ); + } + return pointermid; + }; + + /** + * Find the differences between two texts. Assumes that the texts do not + * have any common prefix or suffix. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {boolean} checklines Speedup flag. If false, then don't run a + * line-level diff first to identify the changed areas. + * If true, then run a faster, slightly less optimal diff. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffCompute = function( text1, text2, checklines, deadline ) { + var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; - if ( !text1 ) { - // Just add some text (speedup). - return [ - [ DIFF_INSERT, text2 ] - ]; - } - - if (!text2) { - // Just delete some text (speedup). - return [ - [ DIFF_DELETE, text1 ] - ]; - } - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - i = longtext.indexOf( shorttext ); - if ( i !== -1 ) { - // Shorter text is inside the longer text (speedup). - diffs = [ - [ DIFF_INSERT, longtext.substring( 0, i ) ], - [ DIFF_EQUAL, shorttext ], - [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] - ]; - // Swap insertions for deletions if diff is reversed. - if ( text1.length > text2.length ) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - - if ( shorttext.length === 1 ) { - // Single character string. - // After the previous speedup, the character can't be an equality. - return [ - [ DIFF_DELETE, text1 ], - [ DIFF_INSERT, text2 ] - ]; - } - - // Check to see if the problem can be split in two. - hm = this.diffHalfMatch(text1, text2); - if (hm) { - // A half-match was found, sort out the return data. - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - midCommon = hm[4]; - // Send both pairs off for separate processing. - diffsA = this.DiffMain(text1A, text2A, checklines, deadline); - diffsB = this.DiffMain(text1B, text2B, checklines, deadline); - // Merge the results. - return diffsA.concat([ - [ DIFF_EQUAL, midCommon ] - ], diffsB); - } - - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diffLineMode(text1, text2, deadline); - } - - return this.diffBisect(text1, text2, deadline); - }; - - /** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ - DiffMatchPatch.prototype.diffHalfMatch = function(text1, text2) { - var longtext, shorttext, dmp, + if ( !text1 ) { + // Just add some text (speedup). + return [ + [ DIFF_INSERT, text2 ] + ]; + } + + if ( !text2 ) { + // Just delete some text (speedup). + return [ + [ DIFF_DELETE, text1 ] + ]; + } + + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + i = longtext.indexOf( shorttext ); + if ( i !== -1 ) { + // Shorter text is inside the longer text (speedup). + diffs = [ + [ DIFF_INSERT, longtext.substring( 0, i ) ], + [ DIFF_EQUAL, shorttext ], + [ DIFF_INSERT, longtext.substring( i + shorttext.length ) ] + ]; + // Swap insertions for deletions if diff is reversed. + if ( text1.length > text2.length ) { + diffs[ 0 ][ 0 ] = diffs[ 2 ][ 0 ] = DIFF_DELETE; + } + return diffs; + } + + if ( shorttext.length === 1 ) { + // Single character string. + // After the previous speedup, the character can't be an equality. + return [ + [ DIFF_DELETE, text1 ], + [ DIFF_INSERT, text2 ] + ]; + } + + // Check to see if the problem can be split in two. + hm = this.diffHalfMatch( text1, text2 ); + if ( hm ) { + // A half-match was found, sort out the return data. + text1A = hm[ 0 ]; + text1B = hm[ 1 ]; + text2A = hm[ 2 ]; + text2B = hm[ 3 ]; + midCommon = hm[ 4 ]; + // Send both pairs off for separate processing. + diffsA = this.DiffMain( text1A, text2A, checklines, deadline ); + diffsB = this.DiffMain( text1B, text2B, checklines, deadline ); + // Merge the results. + return diffsA.concat( [ + [ DIFF_EQUAL, midCommon ] + ], diffsB ); + } + + if ( checklines && text1.length > 100 && text2.length > 100 ) { + return this.diffLineMode( text1, text2, deadline ); + } + + return this.diffBisect( text1, text2, deadline ); + }; + + /** + * Do the two texts share a substring which is at least half the length of the + * longer text? + * This speedup can produce non-minimal diffs. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {Array.} Five element Array, containing the prefix of + * text1, the suffix of text1, the prefix of text2, the suffix of + * text2 and the common middle. Or null if there was no match. + * @private + */ + DiffMatchPatch.prototype.diffHalfMatch = function( text1, text2 ) { + var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; - if (this.DiffTimeout <= 0) { - // Don't risk returning a non-optimal diff if we have unlimited time. - return null; - } - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. - } - dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diffHalfMatchI(longtext, shorttext, i) { - var seed, j, bestCommon, prefixLength, suffixLength, + + longtext = text1.length > text2.length ? text1 : text2; + shorttext = text1.length > text2.length ? text2 : text1; + if ( longtext.length < 4 || shorttext.length * 2 < longtext.length ) { + return null; // Pointless. + } + dmp = this; // 'this' becomes 'window' in a closure. + + /** + * Does a substring of shorttext exist within longtext such that the substring + * is at least half the length of longtext? + * Closure, but does not reference any external variables. + * @param {string} longtext Longer string. + * @param {string} shorttext Shorter string. + * @param {number} i Start index of quarter length substring within longtext. + * @return {Array.} Five element Array, containing the prefix of + * longtext, the suffix of longtext, the prefix of shorttext, the suffix + * of shorttext and the common middle. Or null if there was no match. + * @private + */ + function diffHalfMatchI( longtext, shorttext, i ) { + var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; - // Start with a 1/4 length substring at position i as a seed. - seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - j = -1; - bestCommon = ""; - while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { - prefixLength = dmp.diffCommonPrefix(longtext.substring(i), - shorttext.substring(j)); - suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), - shorttext.substring(0, j)); - if (bestCommon.length < suffixLength + prefixLength) { - bestCommon = shorttext.substring(j - suffixLength, j) + - shorttext.substring(j, j + prefixLength); - bestLongtextA = longtext.substring(0, i - suffixLength); - bestLongtextB = longtext.substring(i + prefixLength); - bestShorttextA = shorttext.substring(0, j - suffixLength); - bestShorttextB = shorttext.substring(j + prefixLength); - } - } - if (bestCommon.length * 2 >= longtext.length) { - return [ bestLongtextA, bestLongtextB, - bestShorttextA, bestShorttextB, bestCommon - ]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - hm1 = diffHalfMatchI(longtext, shorttext, - Math.ceil(longtext.length / 4)); - // Check again based on the third quarter. - hm2 = diffHalfMatchI(longtext, shorttext, - Math.ceil(longtext.length / 2)); - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - text1A, text1B, text2A, text2B; - if (text1.length > text2.length) { - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - } else { - text2A = hm[0]; - text2B = hm[1]; - text1A = hm[2]; - text1B = hm[3]; - } - midCommon = hm[4]; - return [ text1A, text1B, text2A, text2B, midCommon ]; - }; - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffLineMode = function(text1, text2, deadline) { - var a, diffs, linearray, pointer, countInsert, + // Start with a 1/4 length substring at position i as a seed. + seed = longtext.substring( i, i + Math.floor( longtext.length / 4 ) ); + j = -1; + bestCommon = ""; + while ( ( j = shorttext.indexOf( seed, j + 1 ) ) !== -1 ) { + prefixLength = dmp.diffCommonPrefix( longtext.substring( i ), + shorttext.substring( j ) ); + suffixLength = dmp.diffCommonSuffix( longtext.substring( 0, i ), + shorttext.substring( 0, j ) ); + if ( bestCommon.length < suffixLength + prefixLength ) { + bestCommon = shorttext.substring( j - suffixLength, j ) + + shorttext.substring( j, j + prefixLength ); + bestLongtextA = longtext.substring( 0, i - suffixLength ); + bestLongtextB = longtext.substring( i + prefixLength ); + bestShorttextA = shorttext.substring( 0, j - suffixLength ); + bestShorttextB = shorttext.substring( j + prefixLength ); + } + } + if ( bestCommon.length * 2 >= longtext.length ) { + return [ bestLongtextA, bestLongtextB, + bestShorttextA, bestShorttextB, bestCommon + ]; + } else { + return null; + } + } + + // First check if the second quarter is the seed for a half-match. + hm1 = diffHalfMatchI( longtext, shorttext, + Math.ceil( longtext.length / 4 ) ); + // Check again based on the third quarter. + hm2 = diffHalfMatchI( longtext, shorttext, + Math.ceil( longtext.length / 2 ) ); + if ( !hm1 && !hm2 ) { + return null; + } else if ( !hm2 ) { + hm = hm1; + } else if ( !hm1 ) { + hm = hm2; + } else { + // Both matched. Select the longest. + hm = hm1[ 4 ].length > hm2[ 4 ].length ? hm1 : hm2; + } + + // A half-match was found, sort out the return data. + text1A, text1B, text2A, text2B; + if ( text1.length > text2.length ) { + text1A = hm[ 0 ]; + text1B = hm[ 1 ]; + text2A = hm[ 2 ]; + text2B = hm[ 3 ]; + } else { + text2A = hm[ 0 ]; + text2B = hm[ 1 ]; + text1A = hm[ 2 ]; + text1B = hm[ 3 ]; + } + midCommon = hm[ 4 ]; + return [ text1A, text1B, text2A, text2B, midCommon ]; + }; + + /** + * Do a quick line-level diff on both strings, then rediff the parts for + * greater accuracy. + * This speedup can produce non-minimal diffs. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time when the diff should be complete by. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffLineMode = function( text1, text2, deadline ) { + var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; - // Scan the text on a line-by-line basis first. - a = this.diffLinesToChars(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - linearray = a.lineArray; - - diffs = this.DiffMain(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diffCharsToLines(diffs, linearray); - // Eliminate freak matches (e.g. blank lines) - this.diffCleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push( [ DIFF_EQUAL, "" ] ); - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - while (pointer < diffs.length) { - switch ( diffs[pointer][0] ) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (countDelete >= 1 && countInsert >= 1) { - // Delete the offending records and add the merged ones. - diffs.splice(pointer - countDelete - countInsert, - countDelete + countInsert); - pointer = pointer - countDelete - countInsert; - a = this.DiffMain(textDelete, textInsert, false, deadline); - for (j = a.length - 1; j >= 0; j--) { - diffs.splice( pointer, 0, a[j] ); - } - pointer = pointer + a.length; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; - }; - - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisect = function(text1, text2, deadline) { - var text1Length, text2Length, maxD, vOffset, vLength, + // Scan the text on a line-by-line basis first. + a = this.diffLinesToChars( text1, text2 ); + text1 = a.chars1; + text2 = a.chars2; + linearray = a.lineArray; + + diffs = this.DiffMain( text1, text2, false, deadline ); + + // Convert the diff back to original text. + this.diffCharsToLines( diffs, linearray ); + // Eliminate freak matches (e.g. blank lines) + this.diffCleanupSemantic( diffs ); + + // Rediff any replacement blocks, this time character-by-character. + // Add a dummy entry at the end. + diffs.push( [ DIFF_EQUAL, "" ] ); + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ""; + textInsert = ""; + while ( pointer < diffs.length ) { + switch ( diffs[ pointer ][ 0 ] ) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[ pointer ][ 1 ]; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[ pointer ][ 1 ]; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if ( countDelete >= 1 && countInsert >= 1 ) { + // Delete the offending records and add the merged ones. + diffs.splice( pointer - countDelete - countInsert, + countDelete + countInsert ); + pointer = pointer - countDelete - countInsert; + a = this.DiffMain( textDelete, textInsert, false, deadline ); + for ( j = a.length - 1; j >= 0; j-- ) { + diffs.splice( pointer, 0, a[ j ] ); + } + pointer = pointer + a.length; + } + countInsert = 0; + countDelete = 0; + textDelete = ""; + textInsert = ""; + break; + } + pointer++; + } + diffs.pop(); // Remove the dummy entry at the end. + + return diffs; + }; + + /** + * Find the 'middle snake' of a diff, split the problem in two + * and return the recursively constructed diff. + * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisect = function( text1, text2, deadline ) { + var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - maxD = Math.ceil((text1Length + text2Length) / 2); - vOffset = maxD; - vLength = 2 * maxD; - v1 = new Array(vLength); - v2 = new Array(vLength); - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (x = 0; x < vLength; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[vOffset + 1] = 0; - v2[vOffset + 1] = 0; - delta = text1Length - text2Length; - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - front = (delta % 2 !== 0); - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - k1start = 0; - k1end = 0; - k2start = 0; - k2end = 0; - for (d = 0; d < maxD; d++) { - // Bail out if deadline is reached. - if ((new Date()).getTime() > deadline) { - break; - } - - // Walk the front path one step. - for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - k1Offset = vOffset + k1; - if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { - x1 = v1[k1Offset + 1]; - } else { - x1 = v1[k1Offset - 1] + 1; - } - y1 = x1 - k1; - while (x1 < text1Length && y1 < text2Length && - text1.charAt(x1) === text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1Offset] = x1; - if (x1 > text1Length) { - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2Length) { - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - k2Offset = vOffset + delta - k1; - if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - v2[k2Offset]; - if (x1 >= x2) { - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - k2Offset = vOffset + k2; - if ( k2 === -d || (k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { - x2 = v2[k2Offset + 1]; - } else { - x2 = v2[k2Offset - 1] + 1; - } - y2 = x2 - k2; - while (x2 < text1Length && y2 < text2Length && - text1.charAt(text1Length - x2 - 1) === - text2.charAt(text2Length - y2 - 1)) { - x2++; - y2++; - } - v2[k2Offset] = x2; - if (x2 > text1Length) { - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2Length) { - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - k1Offset = vOffset + delta - k2; - if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { - x1 = v1[k1Offset]; - y1 = vOffset + x1 - k1Offset; - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - x2; - if (x1 >= x2) { - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - } - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [ - [ DIFF_DELETE, text1 ], - [ DIFF_INSERT, text2 ] - ]; - }; - - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { - var text1a, text1b, text2a, text2b, diffs, diffsb; - text1a = text1.substring(0, x); - text2a = text2.substring(0, y); - text1b = text1.substring(x); - text2b = text2.substring(y); - - // Compute both diffs serially. - diffs = this.DiffMain(text1a, text2a, false, deadline); - diffsb = this.DiffMain(text1b, text2b, false, deadline); - - return diffs.concat(diffsb); - }; - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupSemantic = function(diffs) { - var changes, equalities, equalitiesLength, lastequality, + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + maxD = Math.ceil( ( text1Length + text2Length ) / 2 ); + vOffset = maxD; + vLength = 2 * maxD; + v1 = new Array( vLength ); + v2 = new Array( vLength ); + // Setting all elements to -1 is faster in Chrome & Firefox than mixing + // integers and undefined. + for ( x = 0; x < vLength; x++ ) { + v1[ x ] = -1; + v2[ x ] = -1; + } + v1[ vOffset + 1 ] = 0; + v2[ vOffset + 1 ] = 0; + delta = text1Length - text2Length; + // If the total number of characters is odd, then the front path will collide + // with the reverse path. + front = ( delta % 2 !== 0 ); + // Offsets for start and end of k loop. + // Prevents mapping of space beyond the grid. + k1start = 0; + k1end = 0; + k2start = 0; + k2end = 0; + for ( d = 0; d < maxD; d++ ) { + // Bail out if deadline is reached. + if ( ( new Date() ).getTime() > deadline ) { + break; + } + + // Walk the front path one step. + for ( k1 = -d + k1start; k1 <= d - k1end; k1 += 2 ) { + k1Offset = vOffset + k1; + if ( k1 === -d || ( k1 !== d && v1[ k1Offset - 1 ] < v1[ k1Offset + 1 ] ) ) { + x1 = v1[ k1Offset + 1 ]; + } else { + x1 = v1[ k1Offset - 1 ] + 1; + } + y1 = x1 - k1; + while ( x1 < text1Length && y1 < text2Length && + text1.charAt( x1 ) === text2.charAt( y1 ) ) { + x1++; + y1++; + } + v1[ k1Offset ] = x1; + if ( x1 > text1Length ) { + // Ran off the right of the graph. + k1end += 2; + } else if ( y1 > text2Length ) { + // Ran off the bottom of the graph. + k1start += 2; + } else if ( front ) { + k2Offset = vOffset + delta - k1; + if ( k2Offset >= 0 && k2Offset < vLength && v2[ k2Offset ] !== -1 ) { + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - v2[ k2Offset ]; + if ( x1 >= x2 ) { + // Overlap detected. + return this.diffBisectSplit( text1, text2, x1, y1, deadline ); + } + } + } + } + + // Walk the reverse path one step. + for ( k2 = -d + k2start; k2 <= d - k2end; k2 += 2 ) { + k2Offset = vOffset + k2; + if ( k2 === -d || ( k2 !== d && v2[ k2Offset - 1 ] < v2[ k2Offset + 1 ] ) ) { + x2 = v2[ k2Offset + 1 ]; + } else { + x2 = v2[ k2Offset - 1 ] + 1; + } + y2 = x2 - k2; + while ( x2 < text1Length && y2 < text2Length && + text1.charAt( text1Length - x2 - 1 ) === + text2.charAt( text2Length - y2 - 1 ) ) { + x2++; + y2++; + } + v2[ k2Offset ] = x2; + if ( x2 > text1Length ) { + // Ran off the left of the graph. + k2end += 2; + } else if ( y2 > text2Length ) { + // Ran off the top of the graph. + k2start += 2; + } else if ( !front ) { + k1Offset = vOffset + delta - k2; + if ( k1Offset >= 0 && k1Offset < vLength && v1[ k1Offset ] !== -1 ) { + x1 = v1[ k1Offset ]; + y1 = vOffset + x1 - k1Offset; + // Mirror x2 onto top-left coordinate system. + x2 = text1Length - x2; + if ( x1 >= x2 ) { + // Overlap detected. + return this.diffBisectSplit( text1, text2, x1, y1, deadline ); + } + } + } + } + } + // Diff took too long and hit the deadline or + // number of diffs equals number of characters, no commonality at all. + return [ + [ DIFF_DELETE, text1 ], + [ DIFF_INSERT, text2 ] + ]; + }; + + /** + * Given the location of the 'middle snake', split the diff in two parts + * and recurse. + * @param {string} text1 Old string to be diffed. + * @param {string} text2 New string to be diffed. + * @param {number} x Index of split point in text1. + * @param {number} y Index of split point in text2. + * @param {number} deadline Time at which to bail if not yet complete. + * @return {!Array.} Array of diff tuples. + * @private + */ + DiffMatchPatch.prototype.diffBisectSplit = function( text1, text2, x, y, deadline ) { + var text1a, text1b, text2a, text2b, diffs, diffsb; + text1a = text1.substring( 0, x ); + text2a = text2.substring( 0, y ); + text1b = text1.substring( x ); + text2b = text2.substring( y ); + + // Compute both diffs serially. + diffs = this.DiffMain( text1a, text2a, false, deadline ); + diffsb = this.DiffMain( text1b, text2b, false, deadline ); + + return diffs.concat( diffsb ); + }; + + /** + * Reduce the number of edits by eliminating semantically trivial equalities. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupSemantic = function( diffs ) { + var changes, equalities, equalitiesLength, lastequality, pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - // Number of characters that changed prior to the equality. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - // Number of characters that changed after the equality. - lengthInsertions2 = 0; - lengthDeletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found. - equalities[equalitiesLength++] = pointer; - lengthInsertions1 = lengthInsertions2; - lengthDeletions1 = lengthDeletions2; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = diffs[pointer][1]; - } else { // An insertion or deletion. - if (diffs[pointer][0] === DIFF_INSERT) { - lengthInsertions2 += diffs[pointer][1].length; - } else { - lengthDeletions2 += diffs[pointer][1].length; - } - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastequality && (lastequality.length <= - Math.max(lengthInsertions1, lengthDeletions1)) && - (lastequality.length <= Math.max(lengthInsertions2, - lengthDeletions2))) { - // Duplicate record. - diffs.splice( equalities[ equalitiesLength - 1 ], 0, [ DIFF_DELETE, lastequality ] ); - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - // Throw away the equality we just deleted. - equalitiesLength--; - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - lengthInsertions1 = 0; // Reset the counters. - lengthDeletions1 = 0; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if (changes) { - this.diffCleanupMerge(diffs); - } - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] === DIFF_DELETE && - diffs[pointer][0] === DIFF_INSERT) { - deletion = diffs[pointer - 1][1]; - insertion = diffs[pointer][1]; - overlapLength1 = this.diffCommonOverlap(deletion, insertion); - overlapLength2 = this.diffCommonOverlap(insertion, deletion); - if (overlapLength1 >= overlapLength2) { - if (overlapLength1 >= deletion.length / 2 || - overlapLength1 >= insertion.length / 2) { - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice( pointer, 0, [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] ); - diffs[pointer - 1][1] = - deletion.substring(0, deletion.length - overlapLength1); - diffs[pointer + 1][1] = insertion.substring(overlapLength1); - pointer++; - } - } else { - if (overlapLength2 >= deletion.length / 2 || - overlapLength2 >= insertion.length / 2) { - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice( pointer, 0, [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] ); - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = - insertion.substring(0, insertion.length - overlapLength2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = - deletion.substring(overlapLength2); - pointer++; - } - } - pointer++; - } - pointer++; - } - }; - - /** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ - DiffMatchPatch.prototype.diffCommonOverlap = function(text1, text2) { - var text1Length, text2Length, textLength, + changes = false; + equalities = []; // Stack of indices where equalities are found. + equalitiesLength = 0; // Keeping our own length var is faster in JS. + /** @type {?string} */ + lastequality = null; + // Always equal to diffs[equalities[equalitiesLength - 1]][1] + pointer = 0; // Index of current position. + // Number of characters that changed prior to the equality. + lengthInsertions1 = 0; + lengthDeletions1 = 0; + // Number of characters that changed after the equality. + lengthInsertions2 = 0; + lengthDeletions2 = 0; + while ( pointer < diffs.length ) { + if ( diffs[ pointer ][ 0 ] === DIFF_EQUAL ) { // Equality found. + equalities[ equalitiesLength++ ] = pointer; + lengthInsertions1 = lengthInsertions2; + lengthDeletions1 = lengthDeletions2; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = diffs[ pointer ][ 1 ]; + } else { // An insertion or deletion. + if ( diffs[ pointer ][ 0 ] === DIFF_INSERT ) { + lengthInsertions2 += diffs[ pointer ][ 1 ].length; + } else { + lengthDeletions2 += diffs[ pointer ][ 1 ].length; + } + // Eliminate an equality that is smaller or equal to the edits on both + // sides of it. + if ( lastequality && ( lastequality.length <= + Math.max( lengthInsertions1, lengthDeletions1 ) ) && + ( lastequality.length <= Math.max( lengthInsertions2, + lengthDeletions2 ) ) ) { + + // Duplicate record. + diffs.splice( + equalities[ equalitiesLength - 1 ], + 0, + [ DIFF_DELETE, lastequality ] + ); + + // Change second copy to insert. + diffs[ equalities[ equalitiesLength - 1 ] + 1 ][ 0 ] = DIFF_INSERT; + + // Throw away the equality we just deleted. + equalitiesLength--; + + // Throw away the previous equality (it needs to be reevaluated). + equalitiesLength--; + pointer = equalitiesLength > 0 ? equalities[ equalitiesLength - 1 ] : -1; + + // Reset the counters. + lengthInsertions1 = 0; + lengthDeletions1 = 0; + lengthInsertions2 = 0; + lengthDeletions2 = 0; + lastequality = null; + changes = true; + } + } + pointer++; + } + + // Normalize the diff. + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + + // Find any overlaps between deletions and insertions. + // e.g: abcxxxxxxdef + // -> abcxxxdef + // e.g: xxxabcdefxxx + // -> defxxxabc + // Only extract an overlap if it is as big as the edit ahead or behind it. + pointer = 1; + while ( pointer < diffs.length ) { + if ( diffs[ pointer - 1 ][ 0 ] === DIFF_DELETE && + diffs[ pointer ][ 0 ] === DIFF_INSERT ) { + deletion = diffs[ pointer - 1 ][ 1 ]; + insertion = diffs[ pointer ][ 1 ]; + overlapLength1 = this.diffCommonOverlap( deletion, insertion ); + overlapLength2 = this.diffCommonOverlap( insertion, deletion ); + if ( overlapLength1 >= overlapLength2 ) { + if ( overlapLength1 >= deletion.length / 2 || + overlapLength1 >= insertion.length / 2 ) { + // Overlap found. Insert an equality and trim the surrounding edits. + diffs.splice( + pointer, + 0, + [ DIFF_EQUAL, insertion.substring( 0, overlapLength1 ) ] + ); + diffs[ pointer - 1 ][ 1 ] = + deletion.substring( 0, deletion.length - overlapLength1 ); + diffs[ pointer + 1 ][ 1 ] = insertion.substring( overlapLength1 ); + pointer++; + } + } else { + if ( overlapLength2 >= deletion.length / 2 || + overlapLength2 >= insertion.length / 2 ) { + + // Reverse overlap found. + // Insert an equality and swap and trim the surrounding edits. + diffs.splice( + pointer, + 0, + [ DIFF_EQUAL, deletion.substring( 0, overlapLength2 ) ] + ); + + diffs[ pointer - 1 ][ 0 ] = DIFF_INSERT; + diffs[ pointer - 1 ][ 1 ] = + insertion.substring( 0, insertion.length - overlapLength2 ); + diffs[ pointer + 1 ][ 0 ] = DIFF_DELETE; + diffs[ pointer + 1 ][ 1 ] = + deletion.substring( overlapLength2 ); + pointer++; + } + } + pointer++; + } + pointer++; + } + }; + + /** + * Determine if the suffix of one string is the prefix of another. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {number} The number of characters common to the end of the first + * string and the start of the second string. + * @private + */ + DiffMatchPatch.prototype.diffCommonOverlap = function( text1, text2 ) { + var text1Length, text2Length, textLength, best, length, pattern, found; - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - // Eliminate the null case. - if (text1Length === 0 || text2Length === 0) { - return 0; - } - // Truncate the longer string. - if (text1Length > text2Length) { - text1 = text1.substring(text1Length - text2Length); - } else if (text1Length < text2Length) { - text2 = text2.substring(0, text1Length); - } - textLength = Math.min(text1Length, text2Length); - // Quick check for the worst case. - if (text1 === text2) { - return textLength; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: http://neil.fraser.name/news/2010/11/04/ - best = 0; - length = 1; - while (true) { - pattern = text1.substring(textLength - length); - found = text2.indexOf(pattern); - if (found === -1) { - return best; - } - length += found; - if (found === 0 || text1.substring(textLength - length) === - text2.substring(0, length)) { - best = length; - length++; - } - } - }; - - /** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ - DiffMatchPatch.prototype.diffLinesToChars = function(text1, text2) { - var lineArray, lineHash, chars1, chars2; - lineArray = []; // e.g. lineArray[4] === 'Hello\n' - lineHash = {}; // e.g. lineHash['Hello\n'] === 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ""; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diffLinesToCharsMunge(text) { - var chars, lineStart, lineEnd, lineArrayLength, line; - chars = ""; - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - lineStart = 0; - lineEnd = -1; - // Keeping our own length variable is faster than looking it up. - lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf("\n", lineStart); - if (lineEnd === -1) { - lineEnd = text.length - 1; - } - line = text.substring(lineStart, lineEnd + 1); - lineStart = lineEnd + 1; - - if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : - (lineHash[line] !== undefined)) { - chars += String.fromCharCode( lineHash[ line ] ); - } else { - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - } - return chars; - } - - chars1 = diffLinesToCharsMunge(text1); - chars2 = diffLinesToCharsMunge(text2); - return { - chars1: chars1, - chars2: chars2, - lineArray: lineArray - }; - }; - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ - DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { - var x, chars, text, y; - for ( x = 0; x < diffs.length; x++ ) { - chars = diffs[x][1]; - text = []; - for ( y = 0; y < chars.length; y++ ) { - text[y] = lineArray[chars.charCodeAt(y)]; - } - diffs[x][1] = text.join(""); - } - }; - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupMerge = function(diffs) { - var pointer, countDelete, countInsert, textInsert, textDelete, - commonlength, changes; - diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - commonlength; - while (pointer < diffs.length) { - switch ( diffs[ pointer ][ 0 ] ) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - // Upon reaching an equality, check for prior redundancies. - if (countDelete + countInsert > 1) { - if (countDelete !== 0 && countInsert !== 0) { - // Factor out any common prefixies. - commonlength = this.diffCommonPrefix(textInsert, textDelete); - if (commonlength !== 0) { - if ((pointer - countDelete - countInsert) > 0 && - diffs[pointer - countDelete - countInsert - 1][0] === - DIFF_EQUAL) { - diffs[pointer - countDelete - countInsert - 1][1] += - textInsert.substring(0, commonlength); - } else { - diffs.splice( 0, 0, [ DIFF_EQUAL, - textInsert.substring( 0, commonlength ) - ] ); - pointer++; - } - textInsert = textInsert.substring(commonlength); - textDelete = textDelete.substring(commonlength); - } - // Factor out any common suffixies. - commonlength = this.diffCommonSuffix(textInsert, textDelete); - if (commonlength !== 0) { - diffs[pointer][1] = textInsert.substring(textInsert.length - - commonlength) + diffs[pointer][1]; - textInsert = textInsert.substring(0, textInsert.length - - commonlength); - textDelete = textDelete.substring(0, textDelete.length - - commonlength); - } - } - // Delete the offending records and add the merged ones. - if (countDelete === 0) { - diffs.splice( pointer - countInsert, - countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); - } else if (countInsert === 0) { - diffs.splice( pointer - countDelete, - countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); - } else { - diffs.splice( pointer - countDelete - countInsert, - countDelete + countInsert, [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] ); - } - pointer = pointer - countDelete - countInsert + - (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; - } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - } - if (diffs[diffs.length - 1][1] === "") { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - changes = false; - pointer = 1; - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] === DIFF_EQUAL && - diffs[pointer + 1][0] === DIFF_EQUAL) { - // This is a single edit surrounded by equalities. - if ( diffs[ pointer ][ 1 ].substring( diffs[ pointer ][ 1 ].length - - diffs[ pointer - 1 ][ 1 ].length ) === diffs[ pointer - 1 ][ 1 ] ) { - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + - diffs[pointer][1].substring(0, diffs[pointer][1].length - - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if ( diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === - diffs[ pointer + 1 ][ 1 ] ) { - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = - diffs[pointer][1].substring(diffs[pointer + 1][1].length) + - diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } - } - pointer++; - } - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - return function(o, n) { + // Cache the text lengths to prevent multiple calls. + text1Length = text1.length; + text2Length = text2.length; + // Eliminate the null case. + if ( text1Length === 0 || text2Length === 0 ) { + return 0; + } + // Truncate the longer string. + if ( text1Length > text2Length ) { + text1 = text1.substring( text1Length - text2Length ); + } else if ( text1Length < text2Length ) { + text2 = text2.substring( 0, text1Length ); + } + textLength = Math.min( text1Length, text2Length ); + // Quick check for the worst case. + if ( text1 === text2 ) { + return textLength; + } + + // Start by looking for a single character match + // and increase length until no match is found. + // Performance analysis: http://neil.fraser.name/news/2010/11/04/ + best = 0; + length = 1; + while ( true ) { + pattern = text1.substring( textLength - length ); + found = text2.indexOf( pattern ); + if ( found === -1 ) { + return best; + } + length += found; + if ( found === 0 || text1.substring( textLength - length ) === + text2.substring( 0, length ) ) { + best = length; + length++; + } + } + }; + + /** + * Split two texts into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * @param {string} text1 First string. + * @param {string} text2 Second string. + * @return {{chars1: string, chars2: string, lineArray: !Array.}} + * An object containing the encoded text1, the encoded text2 and + * the array of unique strings. + * The zeroth element of the array of unique strings is intentionally blank. + * @private + */ + DiffMatchPatch.prototype.diffLinesToChars = function( text1, text2 ) { + var lineArray, lineHash, chars1, chars2; + lineArray = []; // e.g. lineArray[4] === 'Hello\n' + lineHash = {}; // e.g. lineHash['Hello\n'] === 4 + + // '\x00' is a valid character, but various debuggers don't like it. + // So we'll insert a junk entry to avoid generating a null character. + lineArray[ 0 ] = ""; + + /** + * Split a text into an array of strings. Reduce the texts to a string of + * hashes where each Unicode character represents one line. + * Modifies linearray and linehash through being a closure. + * @param {string} text String to encode. + * @return {string} Encoded string. + * @private + */ + function diffLinesToCharsMunge( text ) { + var chars, lineStart, lineEnd, lineArrayLength, line; + chars = ""; + // Walk the text, pulling out a substring for each line. + // text.split('\n') would would temporarily double our memory footprint. + // Modifying text would create many large strings to garbage collect. + lineStart = 0; + lineEnd = -1; + // Keeping our own length variable is faster than looking it up. + lineArrayLength = lineArray.length; + while ( lineEnd < text.length - 1 ) { + lineEnd = text.indexOf( "\n", lineStart ); + if ( lineEnd === -1 ) { + lineEnd = text.length - 1; + } + line = text.substring( lineStart, lineEnd + 1 ); + lineStart = lineEnd + 1; + + if ( lineHash.hasOwnProperty ? lineHash.hasOwnProperty( line ) : + ( lineHash[ line ] !== undefined ) ) { + chars += String.fromCharCode( lineHash[ line ] ); + } else { + chars += String.fromCharCode( lineArrayLength ); + lineHash[ line ] = lineArrayLength; + lineArray[ lineArrayLength++ ] = line; + } + } + return chars; + } + + chars1 = diffLinesToCharsMunge( text1 ); + chars2 = diffLinesToCharsMunge( text2 ); + return { + chars1: chars1, + chars2: chars2, + lineArray: lineArray + }; + }; + + /** + * Rehydrate the text in a diff from a string of line hashes to real lines of + * text. + * @param {!Array.} diffs Array of diff tuples. + * @param {!Array.} lineArray Array of unique strings. + * @private + */ + DiffMatchPatch.prototype.diffCharsToLines = function( diffs, lineArray ) { + var x, chars, text, y; + for ( x = 0; x < diffs.length; x++ ) { + chars = diffs[ x ][ 1 ]; + text = []; + for ( y = 0; y < chars.length; y++ ) { + text[ y ] = lineArray[ chars.charCodeAt( y ) ]; + } + diffs[ x ][ 1 ] = text.join( "" ); + } + }; + + /** + * Reorder and merge like edit sections. Merge equalities. + * Any edit section can move as long as it doesn't cross an equality. + * @param {!Array.} diffs Array of diff tuples. + */ + DiffMatchPatch.prototype.diffCleanupMerge = function( diffs ) { + var pointer, countDelete, countInsert, textInsert, textDelete, + commonlength, changes, diffPointer, position; + diffs.push( [ DIFF_EQUAL, "" ] ); // Add a dummy entry at the end. + pointer = 0; + countDelete = 0; + countInsert = 0; + textDelete = ""; + textInsert = ""; + commonlength; + while ( pointer < diffs.length ) { + switch ( diffs[ pointer ][ 0 ] ) { + case DIFF_INSERT: + countInsert++; + textInsert += diffs[ pointer ][ 1 ]; + pointer++; + break; + case DIFF_DELETE: + countDelete++; + textDelete += diffs[ pointer ][ 1 ]; + pointer++; + break; + case DIFF_EQUAL: + // Upon reaching an equality, check for prior redundancies. + if ( countDelete + countInsert > 1 ) { + if ( countDelete !== 0 && countInsert !== 0 ) { + // Factor out any common prefixies. + commonlength = this.diffCommonPrefix( textInsert, textDelete ); + if ( commonlength !== 0 ) { + if ( ( pointer - countDelete - countInsert ) > 0 && + diffs[ pointer - countDelete - countInsert - 1 ][ 0 ] === + DIFF_EQUAL ) { + diffs[ pointer - countDelete - countInsert - 1 ][ 1 ] += + textInsert.substring( 0, commonlength ); + } else { + diffs.splice( 0, 0, [ DIFF_EQUAL, + textInsert.substring( 0, commonlength ) + ] ); + pointer++; + } + textInsert = textInsert.substring( commonlength ); + textDelete = textDelete.substring( commonlength ); + } + // Factor out any common suffixies. + commonlength = this.diffCommonSuffix( textInsert, textDelete ); + if ( commonlength !== 0 ) { + diffs[ pointer ][ 1 ] = textInsert.substring( textInsert.length - + commonlength ) + diffs[ pointer ][ 1 ]; + textInsert = textInsert.substring( 0, textInsert.length - + commonlength ); + textDelete = textDelete.substring( 0, textDelete.length - + commonlength ); + } + } + // Delete the offending records and add the merged ones. + if ( countDelete === 0 ) { + diffs.splice( pointer - countInsert, + countDelete + countInsert, [ DIFF_INSERT, textInsert ] ); + } else if ( countInsert === 0 ) { + diffs.splice( pointer - countDelete, + countDelete + countInsert, [ DIFF_DELETE, textDelete ] ); + } else { + diffs.splice( + pointer - countDelete - countInsert, + countDelete + countInsert, + [ DIFF_DELETE, textDelete ], [ DIFF_INSERT, textInsert ] + ); + } + pointer = pointer - countDelete - countInsert + + ( countDelete ? 1 : 0 ) + ( countInsert ? 1 : 0 ) + 1; + } else if ( pointer !== 0 && diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL ) { + + // Merge this equality with the previous one. + diffs[ pointer - 1 ][ 1 ] += diffs[ pointer ][ 1 ]; + diffs.splice( pointer, 1 ); + } else { + pointer++; + } + countInsert = 0; + countDelete = 0; + textDelete = ""; + textInsert = ""; + break; + } + } + if ( diffs[ diffs.length - 1 ][ 1 ] === "" ) { + diffs.pop(); // Remove the dummy entry at the end. + } + + // Second pass: look for single edits surrounded on both sides by equalities + // which can be shifted sideways to eliminate an equality. + // e.g: ABAC -> ABAC + changes = false; + pointer = 1; + + // Intentionally ignore the first and last element (don't need checking). + while ( pointer < diffs.length - 1 ) { + if ( diffs[ pointer - 1 ][ 0 ] === DIFF_EQUAL && + diffs[ pointer + 1 ][ 0 ] === DIFF_EQUAL ) { + + diffPointer = diffs[ pointer ][ 1 ]; + position = diffPointer.substring( + diffPointer.length - diffs[ pointer - 1 ][ 1 ].length + ); + + // This is a single edit surrounded by equalities. + if ( position === diffs[ pointer - 1 ][ 1 ] ) { + + // Shift the edit over the previous equality. + diffs[ pointer ][ 1 ] = diffs[ pointer - 1 ][ 1 ] + + diffs[ pointer ][ 1 ].substring( 0, diffs[ pointer ][ 1 ].length - + diffs[ pointer - 1 ][ 1 ].length ); + diffs[ pointer + 1 ][ 1 ] = + diffs[ pointer - 1 ][ 1 ] + diffs[ pointer + 1 ][ 1 ]; + diffs.splice( pointer - 1, 1 ); + changes = true; + } else if ( diffPointer.substring( 0, diffs[ pointer + 1 ][ 1 ].length ) === + diffs[ pointer + 1 ][ 1 ] ) { + + // Shift the edit over the next equality. + diffs[ pointer - 1 ][ 1 ] += diffs[ pointer + 1 ][ 1 ]; + diffs[ pointer ][ 1 ] = + diffs[ pointer ][ 1 ].substring( diffs[ pointer + 1 ][ 1 ].length ) + + diffs[ pointer + 1 ][ 1 ]; + diffs.splice( pointer + 1, 1 ); + changes = true; + } + } + pointer++; + } + // If shifts were made, the diff needs reordering and another shift sweep. + if ( changes ) { + this.diffCleanupMerge( diffs ); + } + }; + + return function( o, n ) { var diff, output, text; - diff = new DiffMatchPatch(); - output = diff.DiffMain(o, n); - //console.log(output); - diff.diffCleanupEfficiency(output); - text = diff.diffPrettyHtml(output); - - return text; - }; -}()); -// jscs:enable + diff = new DiffMatchPatch(); + output = diff.DiffMain( o, n ); + diff.diffCleanupEfficiency( output ); + text = diff.diffPrettyHtml( output ); + + return text; + }; +}() ); + +// Get a reference to the global object, like window in browsers +}( (function() { + return this; +})() )); (function() { +// Don't load the HTML Reporter on non-Browser environments +if ( typeof window === "undefined" || !window.document ) { + return; +} + // Deprecated QUnit.init - Ref #530 // Re-initialize the configuration options QUnit.init = function() { @@ -3124,12 +3383,8 @@ QUnit.init = function() { } }; -// Don't load the HTML Reporter on non-Browser environments -if ( typeof window === "undefined" ) { - return; -} - var config = QUnit.config, + collapseNext = false, hasOwn = Object.prototype.hasOwnProperty, defined = { document: window.document !== undefined, @@ -3526,6 +3781,17 @@ function storeFixture() { } } +function appendFilteredTest() { + var testId = QUnit.config.testId; + if ( !testId || testId.length <= 0 ) { + return ""; + } + return "
Rerunning selected tests: " + testId.join(", ") + + " " + "Run all tests" + "
"; +} + function appendUserAgent() { var userAgent = id( "qunit-userAgent" ); @@ -3533,7 +3799,7 @@ function appendUserAgent() { userAgent.innerHTML = ""; userAgent.appendChild( document.createTextNode( - "QUnit " + QUnit.version + "; " + navigator.userAgent + "QUnit " + QUnit.version + "; " + navigator.userAgent ) ); } @@ -3597,6 +3863,7 @@ QUnit.begin(function( details ) { "

" + escapeText( document.title ) + "

" + "

" + "
" + + appendFilteredTest() + "

" + "
    "; } @@ -3813,6 +4080,16 @@ QUnit.testDone(function( details ) { } if ( bad === 0 ) { + + // Collapse the passing tests + addClass( assertList, "qunit-collapsed" ); + } else if ( bad && config.collapse && !collapseNext ) { + + // Skip collapsing the first failing test + collapseNext = true; + } else { + + // Collapse remaining tests addClass( assertList, "qunit-collapsed" ); } @@ -3879,3 +4156,4 @@ if ( defined.document ) { } })(); + diff --git a/Plugins/Vorlon/plugins/unitTestRunner/vorlon.unitTestRunner.interfaces.ts b/Plugins/Vorlon/plugins/unitTestRunner/vorlon.unitTestRunner.interfaces.ts deleted file mode 100644 index 97191ee8..00000000 --- a/Plugins/Vorlon/plugins/unitTestRunner/vorlon.unitTestRunner.interfaces.ts +++ /dev/null @@ -1,6 +0,0 @@ -module VORLON { - //export class KeyValue { - // public key: string; - // public value: string; - //} -} \ No newline at end of file diff --git a/Plugins/Vorlon/plugins/vorlontheme.css b/Plugins/Vorlon/plugins/vorlontheme.css new file mode 100644 index 00000000..e69de29b diff --git a/Plugins/Vorlon/plugins/webstandards/axe.min.js b/Plugins/Vorlon/plugins/webstandards/axe.min.js new file mode 100644 index 00000000..8090eaee --- /dev/null +++ b/Plugins/Vorlon/plugins/webstandards/axe.min.js @@ -0,0 +1,15 @@ +/*! aXe v1.1.1 + * Copyright (c) 2015 Deque Systems, Inc. + * + * Your use of this Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This entire copyright notice must appear in every copy of this file you + * distribute or in any file that contains substantial portions of this source + * code. + */ +!function(a,b){function c(a){"use strict";var b,d,e=a;if(null!==a&&"object"==typeof a)if(Array.isArray(a))for(e=[],b=0,d=a.length;d>b;b++)e[b]=c(a[b]);else{e={};for(b in a)e[b]=c(a[b])}return e}function d(a){"use strict";var b=a||{};return b.rules=b.rules||[],b.tools=b.tools||[],b.checks=b.checks||[],b.data=b.data||{checks:{},rules:{}},b}function e(a,b,c){"use strict";var d,e;for(d=0,e=a.length;e>d;d++)b[c](a[d])}function f(a){"use strict";a=d(a),S.commons=R=a.commons,this.reporter=a.reporter,this.rules=[],this.tools={},this.checks={},e(a.rules,this,"addRule"),e(a.tools,this,"addTool"),e(a.checks,this,"addCheck"),this.data=a.data||{checks:{},rules:{}},H(a.style)}function g(a){"use strict";this.id=a.id,this.data=null,this.relatedNodes=[],this.result=null}function h(a){"use strict";this.id=a.id,this.options=a.options,this.selector=a.selector,this.evaluate=a.evaluate,a.after&&(this.after=a.after),a.matches&&(this.matches=a.matches),this.enabled=a.hasOwnProperty("enabled")?a.enabled:!0}function i(a,b){"use strict";if(!T.isHidden(b)){var c=T.findBy(a,"node",b);c||a.push({node:b,include:[],exclude:[]})}}function j(a,c,d){"use strict";a.frames=a.frames||[];var e,f,g=b.querySelectorAll(d.shift());a:for(var h=0,i=g.length;i>h;h++){f=g[h];for(var j=0,k=a.frames.length;k>j;j++)if(a.frames[j].node===f){a.frames[j][c].push(d);break a}e={node:f,include:[],exclude:[]},d&&e[c].push(d),a.frames.push(e)}}function k(a){"use strict";if(a&&"object"==typeof a||a instanceof NodeList){if(a instanceof Node)return{include:[a],exclude:[]};if(a.hasOwnProperty("include")||a.hasOwnProperty("exclude"))return{include:a.include||[b],exclude:a.exclude||[]};if(a.length===+a.length)return{include:a,exclude:[]}}return"string"==typeof a?{include:[a],exclude:[]}:{include:[b],exclude:[]}}function l(a,c){"use strict";for(var d,e=[],f=0,g=a[c].length;g>f;f++){if(d=a[c][f],"string"==typeof d){e=e.concat(T.toArray(b.querySelectorAll(d)));break}d&&d.length?d.length>1?j(a,c,d):e=e.concat(T.toArray(b.querySelectorAll(d[0]))):e.push(d)}return e.filter(function(a){return a})}function m(a){"use strict";var c=this;this.frames=[],this.initiator=a&&"boolean"==typeof a.initiator?a.initiator:!0,this.page=!1,a=k(a),this.exclude=a.exclude,this.include=a.include,this.include=l(this,"include"),this.exclude=l(this,"exclude"),T.select("frame, iframe",this).forEach(function(a){M(a,c)&&i(c.frames,a)}),1===this.include.length&&this.include[0]===b&&(this.page=!0)}function n(a){"use strict";this.id=a.id,this.result=S.constants.result.NA,this.pageLevel=a.pageLevel,this.impact=null,this.nodes=[]}function o(a,b){"use strict";this._audit=b,this.id=a.id,this.selector=a.selector||"*",this.excludeHidden="boolean"==typeof a.excludeHidden?a.excludeHidden:!0,this.enabled="boolean"==typeof a.enabled?a.enabled:!0,this.pageLevel="boolean"==typeof a.pageLevel?a.pageLevel:!1,this.any=a.any||[],this.all=a.all||[],this.none=a.none||[],this.tags=a.tags||[],a.matches&&(this.matches=a.matches)}function p(a){"use strict";return T.getAllChecks(a).map(function(b){var c=a._audit.checks[b.id||b];return"function"==typeof c.after?c:null}).filter(Boolean)}function q(a,b){"use strict";var c=[];return a.forEach(function(a){var d=T.getAllChecks(a);d.forEach(function(a){a.id===b&&c.push(a)})}),c}function r(a){"use strict";return a.filter(function(a){return a.filtered!==!0})}function s(a){"use strict";var b=["any","all","none"],c=a.nodes.filter(function(a){var c=0;return b.forEach(function(b){a[b]=r(a[b]),c+=a[b].length}),c>0});return a.pageLevel&&c.length&&(c=[c.reduce(function(a,c){return a?(b.forEach(function(b){a[b].push.apply(a[b],c[b])}),a):void 0})]),c}function t(a){"use strict";a.source=a.source||{},this.id=a.id,this.options=a.options,this._run=a.source.run,this._cleanup=a.source.cleanup,this.active=!1}function u(a){"use strict";if(!S._audit)throw new Error("No audit configured");var c=T.queue();Object.keys(S._audit.tools).forEach(function(a){var b=S._audit.tools[a];b.active&&c.defer(function(a){b.cleanup(a)})}),T.toArray(b.querySelectorAll("frame, iframe")).forEach(function(a){c.defer(function(b){return T.sendCommandToFrame(a,{command:"cleanup-tool"},b)})}),c.then(a)}function v(a,c){"use strict";var d=a&&a.context||{};d.include&&!d.include.length&&(d.include=[b]);var e=a&&a.options||{};switch(a.command){case"rules":return x(d,e,c);case"run-tool":return y(a.parameter,a.selectorArray,e,c);case"cleanup-tool":return u(c)}}function w(a){"use strict";return"string"==typeof a&&W[a]?W[a]:"function"==typeof a?a:V}function x(a,b,c){"use strict";a=new m(a);var d=T.queue(),e=S._audit;a.frames.length&&d.defer(function(c){T.collectResultsFromFrames(a,b,"rules",null,c)}),d.defer(function(c){e.run(a,b,c)}),d.then(function(d){var f=T.mergeResults(d.map(function(a){return{results:a}}));a.initiator&&(f=e.after(f,b),f=f.map(T.finalizeRuleResult)),c(f)})}function y(a,c,d,e){"use strict";if(!S._audit)throw new Error("No audit configured");if(c.length>1){var f=b.querySelector(c.shift());return T.sendCommandToFrame(f,{options:d,command:"run-tool",parameter:a,selectorArray:c},e)}var g=b.querySelector(c.shift());S._audit.tools[a].run(g,d,e)}function z(a,b){"use strict";if(b=b||300,a.length>b){var c=a.indexOf(">");a=a.substring(0,c+1)}return a}function A(a){"use strict";var b=a.outerHTML;return b||"function"!=typeof XMLSerializer||(b=(new XMLSerializer).serializeToString(a)),z(b||"")}function B(a,b){"use strict";b=b||{},this.selector=b.selector||[T.getSelector(a)],this.source=void 0!==b.source?b.source:A(a),this.element=a}function C(a,b){"use strict";Object.keys(S.constants.raisedMetadata).forEach(function(c){var d=S.constants.raisedMetadata[c],e=b.reduce(function(a,b){var e=d.indexOf(b[c]);return e>a?e:a},-1);d[e]&&(a[c]=d[e])})}function D(a){"use strict";var b=a.any.length||a.all.length||a.none.length;return b?S.constants.result.FAIL:S.constants.result.PASS}function E(a){"use strict";function b(a){return T.extendBlacklist({},a,["result"])}var c=T.extendBlacklist({violations:[],passes:[]},a,["nodes"]);return a.nodes.forEach(function(a){var d=T.getFailingChecks(a),e=D(d);return e===S.constants.result.FAIL?(C(a,T.getAllChecks(d)),a.any=d.any.map(b),a.all=d.all.map(b),a.none=d.none.map(b),void c.violations.push(a)):(a.any=a.any.filter(function(a){return a.result}).map(b),a.all=a.all.map(b),a.none=a.none.map(b),void c.passes.push(a))}),C(c,c.violations),c.result=c.violations.length?S.constants.result.FAIL:c.passes.length?S.constants.result.PASS:c.result,c}function F(a){"use strict";for(var b=1,c=a.nodeName;a=a.previousElementSibling;)a.nodeName===c&&b++;return b}function G(a,b){"use strict";var c,d,e=a.parentNode.children;if(!e)return!1;var f=e.length;for(c=0;f>c;c++)if(d=e[c],d!==a&&T.matchesSelector(d,b))return!0;return!1}function H(a){"use strict";if(X&&X.parentNode&&(X.parentNode.removeChild(X),X=null),a){var c=b.head||b.getElementsByTagName("head")[0];return X=b.createElement("style"),X.type="text/css",void 0===X.styleSheet?X.appendChild(b.createTextNode(a)):X.styleSheet.cssText=a,c.appendChild(X),X}}function I(a,b,c){"use strict";a.forEach(function(a){a.node.selector.unshift(c),a.node=new T.DqElement(b,a.node);var d=T.getAllChecks(a);d.length&&d.forEach(function(a){a.relatedNodes.forEach(function(a){a.selector.unshift(c),a=new T.DqElement(b,a)})})})}function J(a,b){"use strict";for(var c,d,e=b[0].node,f=0,g=a.length;g>f;f++)if(d=a[f].node,c=T.nodeSorter(d.element,e.element),c>0||0===c&&e.selector.lengthd;d++)-1===a.indexOf(b[d])&&M(b[d],c)&&a.push(b[d])}var O,P=function(){"use strict";function a(a){var b,c,d=a.Element.prototype,e=["matches","matchesSelector","mozMatchesSelector","webkitMatchesSelector","msMatchesSelector"],f=e.length;for(b=0;f>b;b++)if(c=e[b],d[c])return c}var b;return function(c,d){return b&&c[b]||(b=a(c.ownerDocument.defaultView)),c[b](d)}}(),Q=function(a){"use strict";for(var b,c=String(a),d=c.length,e=-1,f="",g=c.charCodeAt(0);++e=1&&31>=b||b>=127&&159>=b||0==e&&b>=48&&57>=b||1==e&&b>=48&&57>=b&&45==g?"\\"+b.toString(16)+" ":(1!=e||45!=b||45!=g)&&(b>=128||45==b||95==b||b>=48&&57>=b||b>=65&&90>=b||b>=97&&122>=b)?c.charAt(e):"\\"+c.charAt(e)}return f};!function(a){function b(a,b,c){var d=b&&c||0,e=0;for(b=b||[],a.toLowerCase().replace(/[0-9a-f]{2}/g,function(a){16>e&&(b[d+e++]=l[a])});16>e;)b[d+e++]=0;return b}function c(a,b){var c=b||0,d=k;return d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+"-"+d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]+d[a[c++]]}function d(a,b,d){var e=b&&d||0,f=b||[];a=a||{};var g=null!=a.clockseq?a.clockseq:p,h=null!=a.msecs?a.msecs:(new Date).getTime(),i=null!=a.nsecs?a.nsecs:r+1,j=h-q+(i-r)/1e4;if(0>j&&null==a.clockseq&&(g=g+1&16383),(0>j||h>q)&&null==a.nsecs&&(i=0),i>=1e4)throw new Error("uuid.v1(): Can't create more than 10M uuids/sec");q=h,r=i,p=g,h+=122192928e5;var k=(1e4*(268435455&h)+i)%4294967296;f[e++]=k>>>24&255,f[e++]=k>>>16&255,f[e++]=k>>>8&255,f[e++]=255&k;var l=h/4294967296*1e4&268435455;f[e++]=l>>>8&255,f[e++]=255&l,f[e++]=l>>>24&15|16,f[e++]=l>>>16&255,f[e++]=g>>>8|128,f[e++]=255&g;for(var m=a.node||o,n=0;6>n;n++)f[e+n]=m[n];return b?b:c(f)}function e(a,b,d){var e=b&&d||0;"string"==typeof a&&(b="binary"==a?new j(16):null,a=null),a=a||{};var g=a.random||(a.rng||f)();if(g[6]=15&g[6]|64,g[8]=63&g[8]|128,b)for(var h=0;16>h;h++)b[e+h]=g[h];return b||c(g)}var f,g=a.crypto||a.msCrypto;if(!f&&g&&g.getRandomValues){var h=new Uint8Array(16);f=function(){return g.getRandomValues(h),h}}if(!f){var i=new Array(16);f=function(){for(var a,b=0;16>b;b++)0===(3&b)&&(a=4294967296*Math.random()),i[b]=a>>>((3&b)<<3)&255;return i}}for(var j="function"==typeof a.Buffer?a.Buffer:Array,k=[],l={},m=0;256>m;m++)k[m]=(m+256).toString(16).substr(1),l[k[m]]=m;var n=f(),o=[1|n[0],n[1],n[2],n[3],n[4],n[5]],p=16383&(n[6]<<8|n[7]),q=0,r=0;O=e,O.v1=d,O.v4=e,O.parse=b,O.unparse=c,O.BufferClass=j}(a);var R,S={},T=S.utils={};T.matchesSelector=P,T.escapeSelector=Q,T.clone=c;var U={};f.prototype.addRule=function(a){"use strict";a.metadata&&(this.data.rules[a.id]=a.metadata);for(var b,c=0,d=this.rules.length;d>c;c++)if(b=this.rules[c],b.id===a.id)return void(this.rules[c]=new o(a,this));this.rules.push(new o(a,this))},f.prototype.addTool=function(a){"use strict";this.tools[a.id]=new t(a)},f.prototype.addCheck=function(a){"use strict";a.metadata&&(this.data.checks[a.id]=a.metadata),this.checks[a.id]=new h(a)},f.prototype.run=function(a,b,c){"use strict";var d=T.queue();this.rules.forEach(function(c){T.ruleShouldRun(c,a,b)&&d.defer(function(d){c.run(a,b,d)})}),d.then(c)},f.prototype.after=function(a,b){"use strict";var c=this.rules;return a.map(function(a){var d=T.findBy(c,"id",a.id);return d.after(a,b)})},h.prototype.matches=function(a){"use strict";return!this.selector||T.matchesSelector(a,this.selector)?!0:!1},h.prototype.run=function(a,b,c){"use strict";b=b||{};var d=b.hasOwnProperty("enabled")?b.enabled:this.enabled,e=b.options||this.options;if(d&&this.matches(a)){var f,h=new g(this),i=T.checkHelper(h,c);try{f=this.evaluate.call(i,a,e)}catch(j){return S.log(j.message,j.stack),void c(null)}i.isAsync||(h.result=f,setTimeout(function(){c(h)},0))}else c(null)},o.prototype.matches=function(){"use strict";return!0},o.prototype.gather=function(a){"use strict";var b=T.select(this.selector,a);return this.excludeHidden?b.filter(function(a){return!T.isHidden(a)}):b},o.prototype.runChecks=function(a,b,c,d){"use strict";var e=this,f=T.queue();this[a].forEach(function(a){var d=e._audit.checks[a.id||a],g=T.getCheckOption(d,e.id,c);f.defer(function(a){d.run(b,g,a)})}),f.then(function(b){b=b.filter(function(a){return a}),d({type:a,results:b})})},o.prototype.run=function(a,b,c){"use strict";var d,e=this.gather(a),f=T.queue(),g=this;d=new n(this),e.forEach(function(a){g.matches(a)&&f.defer(function(c){var e=T.queue();e.defer(function(c){g.runChecks("any",a,b,c)}),e.defer(function(c){g.runChecks("all",a,b,c)}),e.defer(function(c){g.runChecks("none",a,b,c)}),e.then(function(b){if(b.length){var e=!1,f={node:new T.DqElement(a)};b.forEach(function(a){var b=a.results.filter(function(a){return a});f[a.type]=b,b.length&&(e=!0)}),e&&d.nodes.push(f)}c()})})}),f.then(function(){c(d)})},o.prototype.after=function(a,b){"use strict";var c=p(this),d=this.id;return c.forEach(function(c){var e=q(a.nodes,c.id),f=T.getCheckOption(c,d,b),g=c.after(e,f);e.forEach(function(a){-1===g.indexOf(a)&&(a.filtered=!0)})}),a.nodes=s(a),a},t.prototype.run=function(a,b,c){"use strict";b="undefined"==typeof b?this.options:b,this.active=!0,this._run(a,b,c)},t.prototype.cleanup=function(a){"use strict";this.active=!1,this._cleanup(a)},S.constants={},S.constants.result={PASS:"PASS",FAIL:"FAIL",NA:"NA"},S.constants.raisedMetadata={impact:["minor","moderate","serious","critical"]},S.version="dev",a.axe=S,S.log=function(){"use strict";"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)},S.cleanup=u,S.configure=function(a){"use strict";var b=S._audit;if(!b)throw new Error("No audit configured");a.reporter&&("function"==typeof a.reporter||W[a.reporter])&&(b.reporter=a.reporter),a.checks&&a.checks.forEach(function(a){b.addCheck(a)}),a.rules&&a.rules.forEach(function(a){b.addRule(a)}),a.tools&&a.tools.forEach(function(a){b.addTool(a)})},S.getRules=function(a){"use strict";a=a||[];var b=a.length?S._audit.rules.filter(function(b){return!!a.filter(function(a){return-1!==b.tags.indexOf(a)}).length}):S._audit.rules,c=S._audit.data.rules||{};return b.map(function(a){var b=c[a.id]||{};return{ruleId:a.id,description:b.description,help:b.help,helpUrl:b.helpUrl,tags:a.tags}})},S._load=function(a){"use strict";T.respondable.subscribe("axe.ping",function(a,b){b({axe:!0})}),T.respondable.subscribe("axe.start",v),S._audit=new f(a)};var V,W={};S.reporter=function(a,b,c){"use strict";W[a]=b,c&&(V=b)},S.a11yCheck=function(a,b,c){"use strict";"function"==typeof b&&(c=b,b={}),b&&"object"==typeof b||(b={});var d=S._audit;if(!d)throw new Error("No audit configured");var e=w(b.reporter||d.reporter);x(a,b,function(a){e(a,c)})},S.tool=y,U.failureSummary=function(a){"use strict";var b={};return b.none=a.none.concat(a.all),b.any=a.any,Object.keys(b).map(function(a){return b[a].length?S._audit.data.failureSummaries[a].failureMessage(b[a].map(function(a){return a.message||""})):void 0}).filter(function(a){return void 0!==a}).join("\n\n")},U.formatCheck=function(a){"use strict";return{id:a.id,impact:a.impact,message:a.message,data:a.data,relatedNodes:a.relatedNodes.map(U.formatNode)}},U.formatChecks=function(a,b){"use strict";return a.any=b.any.map(U.formatCheck),a.all=b.all.map(U.formatCheck),a.none=b.none.map(U.formatCheck),a},U.formatNode=function(a){"use strict";return{target:a?a.selector:null,html:a?a.source:null}},U.formatRuleResult=function(a){"use strict";return{id:a.id,description:a.description,help:a.help,helpUrl:a.helpUrl||null,impact:null,tags:a.tags,nodes:[]}},U.splitResultsWithChecks=function(a){"use strict";return U.splitResults(a,U.formatChecks)},U.splitResults=function(b,c){"use strict";var d=[],e=[];return b.forEach(function(a){function b(b){var d=b.result||a.result,e=U.formatNode(b.node);return e.impact=b.impact||null,c(e,b,d)}var f,g=U.formatRuleResult(a);f=T.clone(g),f.impact=a.impact||null,f.nodes=a.violations.map(b),g.nodes=a.passes.map(b),f.nodes.length&&d.push(f),g.nodes.length&&e.push(g)}),{violations:d,passes:e,url:a.location.href,timestamp:new Date}},S.reporter("na",function(a,b){"use strict";var c=a.filter(function(a){return 0===a.violations.length&&0===a.passes.length}).map(U.formatRuleResult),d=U.splitResultsWithChecks(a);b({violations:d.violations,passes:d.passes,notApplicable:c,timestamp:d.timestamp,url:d.url})}),S.reporter("no-passes",function(a,b){"use strict";var c=U.splitResultsWithChecks(a);b({violations:c.violations,timestamp:c.timestamp,url:c.url})}),S.reporter("raw",function(a,b){"use strict";b(a)}),S.reporter("v1",function(a,b){"use strict";var c=U.splitResults(a,function(a,b,c){return c===S.constants.result.FAIL&&(a.failureSummary=U.failureSummary(b)),a});b({violations:c.violations,passes:c.passes,timestamp:c.timestamp,url:c.url})}),S.reporter("v2",function(a,b){"use strict";var c=U.splitResultsWithChecks(a);b({violations:c.violations,passes:c.passes,timestamp:c.timestamp,url:c.url})},!0),T.checkHelper=function(a,b){"use strict";return{isAsync:!1,async:function(){return this.isAsync=!0,function(c){a.value=c,b(a)}},data:function(b){a.data=b},relatedNodes:function(b){b=b instanceof Node?[b]:T.toArray(b),a.relatedNodes=b.map(function(a){return new T.DqElement(a)})}}},T.sendCommandToFrame=function(a,b,c){"use strict";var d=a.contentWindow;if(!d)return S.log("Frame does not have a content window",a),c({});var e=setTimeout(function(){e=setTimeout(function(){S.log("No response from frame: ",a),c(null)},0)},500);T.respondable(d,"axe.ping",null,function(){clearTimeout(e),e=setTimeout(function(){S.log("Error returning results from frame: ",a),c({}),c=null},3e4),T.respondable(d,"axe.start",b,function(a){c&&(clearTimeout(e),c(a))})})},T.collectResultsFromFrames=function(a,b,c,d,e){"use strict";function f(e){var f={options:b,command:c,parameter:d,context:{initiator:!1,page:a.page,include:e.include||[],exclude:e.exclude||[]}};g.defer(function(a){var b=e.node;T.sendCommandToFrame(b,f,function(c){return c?a({results:c,frameElement:b,frame:T.getSelector(b)}):void a(null)})})}for(var g=T.queue(),h=a.frames,i=0,j=h.length;j>i;i++)f(h[i]);g.then(function(a){e(T.mergeResults(a))})},T.contains=function(a,b){"use strict";return"function"==typeof a.contains?a.contains(b):!!(16&a.compareDocumentPosition(b))},B.prototype.toJSON=function(){"use strict";return{selector:this.selector,source:this.source}},T.DqElement=B,T.extendBlacklist=function(a,b,c){"use strict";c=c||[];for(var d in b)b.hasOwnProperty(d)&&-1===c.indexOf(d)&&(a[d]=b[d]);return a},T.extendMetaData=function(a,b){"use strict";for(var c in b)if(b.hasOwnProperty(c))if("function"==typeof b[c])try{a[c]=b[c](a)}catch(d){a[c]=null}else a[c]=b[c]},T.getFailingChecks=function(a){"use strict";var b=a.any.filter(function(a){return!a.result});return{all:a.all.filter(function(a){return!a.result}),any:b.length===a.any.length?b:[],none:a.none.filter(function(a){return!!a.result})}},T.finalizeRuleResult=function(a){"use strict";return T.publishMetaData(a),E(a)},T.findBy=function(a,b,c){"use strict";a=a||[];var d,e;for(d=0,e=a.length;e>d;d++)if(a[d][b]===c)return a[d]},T.getAllChecks=function(a){"use strict";var b=[];return b.concat(a.any||[]).concat(a.all||[]).concat(a.none||[])},T.getCheckOption=function(a,b,c){"use strict";var d=((c.rules&&c.rules[b]||{}).checks||{})[a.id],e=(c.checks||{})[a.id],f=a.enabled,g=a.options;return e&&(e.hasOwnProperty("enabled")&&(f=e.enabled),e.hasOwnProperty("options")&&(g=e.options)),d&&(d.hasOwnProperty("enabled")&&(f=d.enabled),d.hasOwnProperty("options")&&(g=d.options)),{enabled:f,options:g}},T.getSelector=function(a){"use strict";function c(a){return T.escapeSelector(a)}for(var d,e=[];a.parentNode;){if(d="",a.id&&1===b.querySelectorAll("#"+T.escapeSelector(a.id)).length){e.unshift("#"+T.escapeSelector(a.id));break}if(a.className&&"string"==typeof a.className&&(d="."+a.className.trim().split(/\s+/).map(c).join("."),("."===d||G(a,d))&&(d="")),!d){if(d=T.escapeSelector(a.nodeName).toLowerCase(),"html"===d||"body"===d){e.unshift(d);break}G(a,d)&&(d+=":nth-of-type("+F(a)+")")}e.unshift(d),a=a.parentNode}return e.join(" > ")};var X;T.isHidden=function(b,c){"use strict";if(9===b.nodeType)return!1;var d=a.getComputedStyle(b,null);return d&&b.parentNode&&"none"!==d.getPropertyValue("display")&&(c||"hidden"!==d.getPropertyValue("visibility"))&&"true"!==b.getAttribute("aria-hidden")?T.isHidden(b.parentNode,!0):!0},T.mergeResults=function(a){"use strict";var b=[];return a.forEach(function(a){var c=K(a);c&&c.length&&c.forEach(function(c){c.nodes&&a.frame&&I(c.nodes,a.frameElement,a.frame);var d=T.findBy(b,"id",c.id);d?c.nodes.length&&J(d.nodes,c.nodes):b.push(c)})}),b},T.nodeSorter=function(a,b){"use strict";return a===b?0:4&a.compareDocumentPosition(b)?-1:1},T.publishMetaData=function(a){"use strict";function b(a){return function(b){var d=c[b.id]||{},e=d.messages||{},f=T.extendBlacklist({},d,["messages"]);f.message=b.result===a?e.pass:e.fail,T.extendMetaData(b,f)}}var c=S._audit.data.checks||{},d=S._audit.data.rules||{},e=T.findBy(S._audit.rules,"id",a.id)||{};a.tags=T.clone(e.tags||[]);var f=b(!0),g=b(!1);a.nodes.forEach(function(a){a.any.forEach(f),a.all.forEach(f),a.none.forEach(g)}),T.extendMetaData(a,T.clone(d[a.id]||{}))},function(){"use strict";function a(){}function b(){function b(){for(var a=e.length;a>f;f++){var b=e[f],d=b.shift();b.push(c(f)),d.apply(null,b)}}function c(a){return function(b){e[a]=b,--g||d()}}function d(){h(e)}var e=[],f=0,g=0,h=a;return{defer:function(a){e.push([a]),++g,b()},then:function(a){h=a,g||d()},abort:function(b){h=a,b(e)}}}T.queue=b}(),function(b){"use strict";function c(a){return"object"==typeof a&&"string"==typeof a.uuid&&a._respondable===!0}function d(a,b,c,d,e){var f={uuid:d,topic:b,message:c,_respondable:!0};h[d]=e,a.postMessage(JSON.stringify(f),"*")}function e(a,b,c,e){var f=O.v1();d(a,b,c,f,e)}function f(a,b){var c=b.topic,d=b.message,e=i[c];e&&e(d,g(a.source,null,b.uuid))}function g(a,b,c){return function(e,f){d(a,b,e,c,f)}}var h={},i={};e.subscribe=function(a,b){i[a]=b},a.addEventListener("message",function(a){if("string"==typeof a.data){var b;try{b=JSON.parse(a.data)}catch(d){}if(c(b)){var e=b.uuid;h[e]&&(h[e](b.message,g(a.source,b.topic,e)),h[e]=null),f(a,b)}}},!1),b.respondable=e}(T),T.ruleShouldRun=function(a,b,c){"use strict";if(a.pageLevel&&!b.page)return!1;var d=c.runOnly,e=(c.rules||{})[a.id];return d?"rule"===d.type?-1!==d.values.indexOf(a.id):!!(d.values||[]).filter(function(b){return-1!==a.tags.indexOf(b)}).length:(e&&e.hasOwnProperty("enabled")?e.enabled:a.enabled)?!0:!1},T.select=function(a,b){"use strict";for(var c,d=[],e=0,f=b.include.length;f>e;e++)c=b.include[e],c.nodeType===c.ELEMENT_NODE&&T.matchesSelector(c,a)&&N(d,[c],b),N(d,c.querySelectorAll(a),b);return d.sort(T.nodeSorter)},T.toArray=function(a){"use strict";return Array.prototype.slice.call(a)},S._load({data:{rules:{accesskeys:{description:"Ensures every accesskey attribute value is unique",help:"accesskey attribute value must be unique",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/accesskeys"},"area-alt":{description:"Ensures elements of image maps have alternate text",help:"Active elements must have alternate text",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/area-alt"},"aria-allowed-attr":{description:"Ensures ARIA attributes are allowed for an element's role",help:"Elements must only use allowed ARIA attributes",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-allowed-attr"},"aria-required-attr":{description:"Ensures elements with ARIA roles have all required ARIA attributes",help:"Required ARIA attributes must be provided",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-required-attr"},"aria-required-children":{description:"Ensures elements with an ARIA role that require child roles contain them",help:"Certain ARIA roles must contain particular children",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-required-children"},"aria-required-parent":{description:"Ensures elements with an ARIA role that require parent roles are contained by them",help:"Certain ARIA roles must be contained by particular parents",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-required-parent"},"aria-roles":{description:"Ensures all elements with a role attribute use a valid value",help:"ARIA roles used must conform to valid values",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-roles"},"aria-valid-attr-value":{description:"Ensures all ARIA attributes have valid values",help:"ARIA attributes must conform to valid values",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-valid-attr-value"},"aria-valid-attr":{description:"Ensures attributes that begin with aria- are valid ARIA attributes",help:"ARIA attributes must conform to valid names",helpUrl:"https://dequeuniversity.com/rules/axe/1.1/aria-valid-attr"},"audio-caption":{description:"Ensures