I am developing an firefox extension using webExtensions that would help me ease my work with the scenario below.
I have to click around 50-60 buttons on the site that update the task status. On click of this button, the web page is calling the webpage's updTask(id) JavaScript function that is then making a web-service call to update the task.
I am not able to do this from my content script using the code below:
manifest.json:
"permissions": [
"activeTab",
"cross-domain-content": ["http://workdomain.com/","http://workdomain.org/","http://www.workdomain.com/","http://www.workdomain.org/"]
]
Content-Script code:
function taskUpdate(request, sender, sendResponse) {
console.log(request.start + 'inside task update');
updateTask(45878);
chrome.runtime.onMessage.removeListener(taskUpdate);
}
function updateTask(id) {
//TODO: code to get all buttons and task id's
updTask(id); // Not working
}
Plugin Script:
document.addEventListener("click", function(e) {
if (e.target.classList.contains("startButton")) {
chrome.tabs.executeScript(null, {
file: "/content_scripts/taskUpdate.js"
});
chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {start: "start"});
});
return;
}
else if (e.target.classList.contains("clear")) {
chrome.tabs.reload();
window.close();
return;
}
});
Could someone point me in the right direction, what am I missing here??
Your content script is in a different context/scope from that of page scripts (scripts that already exist in the webpage). Your content script has higher privileges than are granted to page scripts. Keeping content scripts separate from page scripts is a normal architecture for browser extensions, which is done for security reasons.
Because your content script is in a different context from page scripts, you can't directly access functions and variables that are defined in page scripts from your content script. There are a few different ways which you can access information in the page context. The cross-browser method of doing so is to cause some specific parts of your code to execute in the page context. I find the most convenient, and cross-browser compatible, way to do so is to create and insert a <script> element into the page's DOM containing the code you want to execute.
You could do something like:
function updateTask(id) {
let newScript = document.createElement('script');
newScript.innerHTML='updTask(' + id + ');';
document.head.appendChild(newScript);
//newScript.remove(); //Can be removed, if desired.
}
The added script gets run in the page context because it is now a <script> element in the DOM. The browser recognizes that a <script> element was added and evaluates it (executes the code contained) as soon as the script which inserted it is no longer processing. It does basically the same thing for any other element you add to the DOM. Because it is part of the page, the code inside the gets run in the page script context/scope.
Getting data back to your content script
There are a variety of ways to communicate between code you have running in the page context and your code in the content script context. My preferred method of doing so is to use CustomEvents. I describe why in the first part of this answer. Generally, I use at least one custom event type to communicate from the page context to the content script context and another from the content script context to the page context. You can use as many CustomEvent types as you want. I'll often use multiple events, each communicating a different thing, rather than a single event type out of which I parse several different types of messages.
Generalized code to execute in the page context from a content script
The easiest way to maintain code which you are going to execute in the page context is to write it as a function in your content script, then inject that function into the page context. Here is some generalized code which will do that while passing parameters to the function you are executing in the page context:
This utility function, executeInPage(), will execute a function in the page context and pass any provided arguments to the function. Arguments must be Object, Array, function, RegExp, Date, and/or other primitives (Boolean, null, undefined, Number, String, but not Symbol).
/* executeInPage takes a function defined in this context, converts it to a string
* and inserts it into the page context inside a <script>. It is placed in an IIFE and
* passed all of the additional parameters passed to executeInPage.
* Parameters:
* func The function which you desire to execute in the page.
* leaveInPage If this does not evaluate to a truthy value, then the <script> is
* immediately removed from the page after insertion. Immediately
* removing the script can normally be done. In some corner cases,
* it's desirable for the script to remain in the page. However,
* even for asynchronous functionality it's usually not necessary, as
* the context containing the code will be kept with any references
* (e.g. the reference to a callback function).
* id If this is a non-blank string, it is used as the ID for the <script>
* All additional parameters are passed to the function executing in the page.
* This is done by converting them to JavaScript code-text and back.
* All such parameters must be Object, Array, functions, RegExp,
* Date, and/or other primitives (Boolean, null, undefined, Number,
* String, but not Symbol). Circular references are not supported.
* If you need to communicate DOM elements, you will need to
* pass selectors, or other descriptors of them (e.g. temporarily
* assign them a unique class), or otherwise communicate them to the
* script (e.g. you could dispatch a custom event once the script is
* inserted into the page context).
*/
function executeInPage(functionToRunInPage, leaveInPage, id) {
//Execute a function in the page context.
// Any additional arguments passed to this function are passed into the page to the
// functionToRunInPage.
// Such arguments must be JSON-ifiable (also Date, Function, and RegExp) (prototypes
// are not copied).
// Using () => doesn't set arguments, so can't use it to define this function.
// This has to be done without jQuery, as jQuery creates the script
// within this context, not the page context, which results in
// permission denied to run the function.
function convertToText(args) {
//This uses the fact that the arguments are converted to text which is
// interpreted within a <script>. That means we can create other types of
// objects by recreating their normal JavaScript representation.
// It's actually easier to do this without JSON.strigify() for the whole
// Object/Array.
var asText = '';
var level = 0;
function lineSeparator(adj, isntLast) {
level += adj - ((typeof isntLast === 'undefined' || isntLast) ? 0 : 1);
asText += (isntLast ? ',' : '') +'\n'+ (new Array(level * 2 + 1)).join('');
}
function recurseObject(obj) {
if (Array.isArray(obj)) {
asText += '[';
lineSeparator(1);
obj.forEach(function(value, index, array) {
recurseObject(value);
lineSeparator(0, index !== array.length - 1);
});
asText += ']';
} else if (obj === null) {
asText +='null';
//undefined
} else if (obj === void(0)) {
asText +='void(0)';
//Special cases for Number
} else if (Number.isNaN(obj)) {
asText +='Number.NaN';
} else if (obj === 1/0) {
asText +='1/0';
} else if (obj === 1/-0) {
asText +='1/-0';
//function
} else if (obj instanceof RegExp || typeof obj === 'function') {
asText += obj.toString();
} else if (obj instanceof Date) {
asText += 'new Date("' + obj.toJSON() + '")';
} else if (typeof obj === 'object') {
asText += '{';
lineSeparator(1);
Object.keys(obj).forEach(function(prop, index, array) {
asText += JSON.stringify(prop) + ': ';
recurseObject(obj[prop]);
lineSeparator(0, index !== array.length - 1);
});
asText += '}';
} else if (['boolean', 'number', 'string'].indexOf(typeof obj) > -1) {
asText += JSON.stringify(obj);
} else {
console.log('Didn\'t handle: typeof obj:', typeof obj, ':: obj:', obj);
}
}
recurseObject(args);
return asText;
}
var newScript = document.createElement('script');
if(typeof id === 'string' && id) {
newScript.id = id;
}
var args = [];
//using .slice(), or other Array methods, on arguments prevents optimization
for(var index=3;index<arguments.length;index++){
args.push(arguments[index]);
}
newScript.textContent = '(' + functionToRunInPage.toString() + ').apply(null,'
+ convertToText(args) + ");";
(document.head || document.documentElement).appendChild(newScript);
if(!leaveInPage) {
//Synchronous scripts are executed immediately and can be immediately removed.
//Scripts with asynchronous functionality of any type must remain in the page
// until complete.
document.head.removeChild(newScript);
}
return newScript;
};
Using excuteInPage():
function logInPageContext(arg0,arg1,arg2,arg3){
console.log('arg0:', arg0);
console.log('arg1:', arg1);
console.log('arg2:', arg2);
console.log('arg3:', arg3);
}
executeInPage(logInPageContext, false, '', 'This', 'is', 'a', 'test');
/* executeInPage takes a function defined in this context, converts it to a string
* and inserts it into the page context inside a <script>. It is placed in an IIFE and
* passed all of the additional parameters passed to executeInPage.
* Parameters:
* func The function which you desire to execute in the page.
* leaveInPage If this does not evaluate to a truthy value, then the <script> is
* immediately removed from the page after insertion. Immediately
* removing the script can normally be done. In some corner cases,
* it's desirable for the script to remain in the page. However,
* even for asynchronous functionality it's usually not necessary, as
* the context containing the code will be kept with any references
* (e.g. the reference to a callback function).
* id If this is a non-blank string, it is used as the ID for the <script>
* All additional parameters are passed to the function executing in the page.
* This is done by converting them to JavaScript code-text and back.
* All such parameters must be Object, Array, functions, RegExp,
* Date, and/or other primitives (Boolean, null, undefined, Number,
* String, but not Symbol). Circular references are not supported.
* If you need to communicate DOM elements, you will need to
* pass selectors, or other descriptors of them (e.g. temporarily
* assign them a unique class), or otherwise communicate them to the
* script (e.g. you could dispatch a custom event once the script is
* inserted into the page context).
*/
function executeInPage(functionToRunInPage, leaveInPage, id) {
//Execute a function in the page context.
// Any additional arguments passed to this function are passed into the page to the
// functionToRunInPage.
// Such arguments must be JSON-ifiable (also Date, Function, and RegExp) (prototypes
// are not copied).
// Using () => doesn't set arguments, so can't use it to define this function.
// This has to be done without jQuery, as jQuery creates the script
// within this context, not the page context, which results in
// permission denied to run the function.
function convertToText(args) {
//This uses the fact that the arguments are converted to text which is
// interpreted within a <script>. That means we can create other types of
// objects by recreating their normal JavaScript representation.
// It's actually easier to do this without JSON.strigify() for the whole
// Object/Array.
var asText = '';
var level = 0;
function lineSeparator(adj, isntLast) {
level += adj - ((typeof isntLast === 'undefined' || isntLast) ? 0 : 1);
asText += (isntLast ? ',' : '') +'\n'+ (new Array(level * 2 + 1)).join('');
}
function recurseObject(obj) {
if (Array.isArray(obj)) {
asText += '[';
lineSeparator(1);
obj.forEach(function(value, index, array) {
recurseObject(value);
lineSeparator(0, index !== array.length - 1);
});
asText += ']';
} else if (obj === null) {
asText +='null';
//undefined
} else if (obj === void(0)) {
asText +='void(0)';
//Special cases for Number
} else if (Number.isNaN(obj)) {
asText +='Number.NaN';
} else if (obj === 1/0) {
asText +='1/0';
} else if (obj === 1/-0) {
asText +='1/-0';
//function
} else if (obj instanceof RegExp || typeof obj === 'function') {
asText += obj.toString();
} else if (obj instanceof Date) {
asText += 'new Date("' + obj.toJSON() + '")';
} else if (typeof obj === 'object') {
asText += '{';
lineSeparator(1);
Object.keys(obj).forEach(function(prop, index, array) {
asText += JSON.stringify(prop) + ': ';
recurseObject(obj[prop]);
lineSeparator(0, index !== array.length - 1);
});
asText += '}';
} else if (['boolean', 'number', 'string'].indexOf(typeof obj) > -1) {
asText += JSON.stringify(obj);
} else {
console.log('Didn\'t handle: typeof obj:', typeof obj, ':: obj:', obj);
}
}
recurseObject(args);
return asText;
}
var newScript = document.createElement('script');
if(typeof id === 'string' && id) {
newScript.id = id;
}
var args = [];
//using .slice(), or other Array methods, on arguments prevents optimization
for(var index=3;index<arguments.length;index++){
args.push(arguments[index]);
}
newScript.textContent = '(' + functionToRunInPage.toString() + ').apply(null,'
+ convertToText(args) + ");";
(document.head || document.documentElement).appendChild(newScript);
if(!leaveInPage) {
//Synchronous scripts are executed immediately and can be immediately removed.
//Scripts with asynchronous functionality of any type must remain in the page
// until complete.
document.head.removeChild(newScript);
}
return newScript;
};
The text for this answer was largely taken from my other answers: this one and this one.
I had a similar issue today where my webextension had to call a javascript function that can only be accessed from the context of a specific web page and I was kinda annoyed that I was supposed to do all the script injection and json serialization and whatnot. But there's actually a pretty simple solution:
window.eval(`updTask(${id})`)
eval will execute whatever code you pass to it in the context of the page instead of your content script.
yeah, I know eval is evil, but in this context it really makes sense, because what I want to do (execute arbitrary code in the context of a web page) is evil by definition. It's like parsing HTML with regex; once you're at that point, you're tainted. Also, you're finally free from the burdens of writing code that is considered lawful good. So let's embrace the eval, just this one time ;)
Related
So, I have this little code in my js file:
window.onload = function Equal() {
var a = 'b1'
var b = 'box1'
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
var a = 'b2'
var b = 'box2'
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
The function itself is not important (it equals checkboxvalues set in the localstorage), but I execute it 2 times. First time with var a & b set to 'b1' & 'box1'. Then I run the script again (same script), but with var a & b set to 'b2' & 'box2'. Now, this code works, but my question is if there is a shorter way to write this? I can imagine some sort of array with a loop, but I could not get it to work for some reason. The 2 variables are pairs, and I know this might be a dumb question, but I can't find the answer anywhere.
You can use a second function which will accept the local storage key and the checkbox id like
window.onload = function Equal() {
setCheckboxState('box1', 'b1');
setCheckboxState('box2', 'b2');
}
function setCheckboxState(id, key) {
document.getElementById(id).checked = 1 == localStorage.getItem(key);
}
You might separate common logic into another function
window.onload = function Equal() {
function extractFromStorage(a, b) {
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
extractFromStorage('b1', 'box1');
extractFromStorage('b2', 'box2');
}
function doTheStuff(a, b) {
var bookstorname = localStorage.getItem(a)
if (bookstorname == 1) {
document.getElementById(b).setAttribute('checked','checked');
}
if (bookstorname == 0) {
document.getElementById(b).removeAttribute('checked','checked');
}
}
window.onload = function Equal() {
doTheStuff('b1', 'box1');
doTheStuff('b2', 'box2');
}
?
This is how I would do it.
There are several problems with your code.
You do not check that the element you are stetting an attribute to
exists. You do not check if the localStorage item you get is
defined.
You pollute the global name space with the function name Equal.
That function should not be named with a capital as it is not a Object generator.
There is no need to use setAttribute and removeAttribute, in
fact removeAttribute makes no sense in this case as you can not
remove the checked attribute from the element. BTW why use setAttribute here and not for window.onload?
The checked attribute is either true or false, it does not use the
string "checked"
Binding the load event via the onload attribute is not safe as you may
block 3rd party code, or worse 3rd party code may block you.
There is no error checking. DOM pages are dynamic environments, pages
have adverts and content from many places that can interfer with your
code. Always code with this in mind. Check for possible errors and deal with them in a friendly way for the end user. In this case I used an alert, not friendly for a normal user but for you the coder.
My solution.
// add an event listener rather than replace the event listener
window.addEventListener(
"load", // for the load event
function(){
// the update function that is called for each item;
var update = function(item){
// the right hand side equates to true if the localstorage
// is equal to "1". LocalStorage allways returns a string or
// undefined if the key is not defined.
item.element.checked = localStorage[item.storageName] === "1";
}
// safe element getter
var getElement = function(eId){
var e = document.getElementById(eId); // try and get the element
if(e === null){ // does it exist?
throw "Missing element:"+eId; // no then we can not continue
// the program stops here unless
// you catch the error and deal with
// it gracefully.
}
return e; //ok return the element.
}
// Item creator. This creates a new item.
// sName is the local storage name
// eId id the element ID
var item = function(sName, eId){
return {
storageName: sName, // set the loaclStorage name
element:getElement(eId); // get the element and check its safe
};
}
// make it all safe
try{
// create an array of items.
var items = [
item("b1","box1"),
item("b2","box2")
];
// for each item update the element status
items.forEach(update);
}catch(e){
alert("Could not update page?");
}
}
);
I'm currently writing a library using jQuery, and I'm facing a wall I can't pass alone.
First, here is some code :
(function($) {
// default options
var defaults = {
option1: "what ever",
option2: "what ever"
};
// available methods
var methods = {
method1: function(param1) {
console.log(param1);
/* ... */
},
method2: function(param1, param2) {
console.log(param1);
console.log(param2); // gives undefined
/* ... */
}
};
// Where magic happens
$.fn.pskChart = function(params) {
if (methods[params] != undefined) {
if (this.length > 0) {
return $(this).each(function(i) {
return methods[params].apply($(this), Array.prototype.slice.call(arguments, 1));
});
}
} else {
$.error("Method " + params + " doesn't exist for pskChart");
}
}
})(jQuery);
$("#sample").pskChart("method1", "param1"); // will work
$("#sample").pskChart("method2", "param1", "param2"); // won't work
This code is working in case I provide only two parameters (method and another parameter), but won't work if I have more parameters
I understand that Array.prototype.slice.call(arguments, 1) returns only one object containing all the remaining arguments.
In the second example, method2 will be called with only one parameter that contains ["param1","param2"]
I would like to "split" this object in order to provide as many parameters I have.
I feel like methods[params].apply($(this), Array.prototype.slice.call(arguments, 1)); but I have no clue how to fix it.
Note that I could have an infinite (or at least big) number of parameters (in addition to the method, that will always be first).
Playing with Array.slice would not be a great (nor proper) solution.
I don't see why it would work even with just one parameter, as you're using the wrong arguments object.
Instead (see comments):
// Where magic happens
$.fn.pskChart = function(params) {
var args; // **Change** We'll use this below
if (methods[params] != undefined) {
if (this.length > 0) {
// **Change** Grab the arguments here, in the `pskChart` function
args = Array.prototype.slice.call(arguments, 1);
return $(this).each(function(i) {
// **Change** Use them here; you can't use `arguments` here
// because, it will be the arguments to this anonymouus
// function, not the ones to `pskChart`
return methods[params].apply($(this), args);
});
}
} else {
$.error("Method " + params + " doesn't exist for pskChart");
}
}
Side note: params is a very strange name for an argument you're using as a method name. Perhaps methodName?
Side note 2: Rather than repeatedly looking up methods[params], consider using a variable. Large sets may benefit from the reduced work.
Side note 3: This line inside your each loop is almost certainly a mistake:
return methods[params].apply($(this), args);
That will return the return value of the method to jQuery's each code. jQuery's each code will completely ignore that vallue unless it's === false, in which case it will stop the each loop. Not at all likely to be what you meant that to do.
Side note 4: Your pskChart function, being a jQuery plugin, should return this unless it has a good reason for returning something else. Right now, the return value is chaotic: If the set has no elements, you return undefined (implicitly); if the set has objects, you return a new jQuery object, not this, because of this line:
return $(this).each(function(i) {
There's no reason for using $() there, this is already a jQuery set (which is why the previous line using this.length works).
Side note 5: You're missing a semicolon from the assignment statement (e.g., there should be one after the closing } on the function expression).
Recommendation:
// Where magic happens
$.fn.pskChart = function(methodName) {
var args, method = methods[methodName];
if (!method) {
// Throws
$.error("Method " + params + " doesn't exist for pskChart");
}
if (this.length > 0) {
args = Array.prototype.slice.call(arguments, 1);
this.each(function() {
method.apply($(this), args);
});
}
return this;
};
I would like to measure the computing time of methods.
A nice way is (How do you performance test JavaScript code?) with console.time('Function #1'); and console.timeEnd('Function #1');
My idea is to add these console outputs on lifecycle-methods. In this case using SAPUI5 like createContent:funtion(){}; methods.
This should be possible with AOP using before() and after() to runt the time counting.
Which AOP framework would you suggest and how to implement it with the need of modifying the identification string "Function #1" automatically?
There actually is no need for aspects in Javascript since you can change any function of any object at any time. JavaScript prototypes allows you to manipulate method implementations of all instances of an object at runtime. Here are two approaches for what you plan.
You could use a generic wrapper function:
var measureId = 0;
var fnMeasureFunction = function(fnToMeasure) {
console.time('measure'+ measureId);
fnToMeasure();
console.timeEnd('measure'+ measureId);
measureId++;
}
Admittedly that requires you to change your actual code...
For static functions or functions that belong to a prototype you could also do sth. like this from the outside without the need of any change to your existing code:
// any static function
var measureId = 0;
var fnOriginalFunction = sap.ui.core.mvc.JSViewRenderer.render;
sap.ui.core.mvc.JSViewRenderer.render = function() {
console.time('measure'+ measureId);
fnOriginalFunction.apply(this, arguments);
console.timeEnd('measure'+ measureId);
measureId++;
}
// any prototype function
var fnOriginalFunction = sap.m.Button.prototype.ontouchstart;
sap.m.Button.prototype.ontouchstart= function() {
console.time('measure'+ measureId);
fnOriginalFunction.apply(this, arguments);
console.timeEnd('measure'+ measureId);
measureId++;
}
This should be possible with AOP using before() and after() to runt the time counting.
As it already got mentioned, one really is not in need of real Aspect-oriented Programming
in order to solve such tasks in JavaScript. But this language might deserve some more standardized
method-modifiers in addition to the already existing bind method.
Please check back with my 2 most recent posts on this matter:
sandwich pattern in javascript code
Can you alter a Javascript function after declaring it?
... and how to implement it with the need of modifying the identification string "Function #1" automatically?
One does not need to since the console's time / timeEnd functionality only has to have
identical entry and exit points for measuring time (like the start/stop trigger of a stopwatch).
So one gets along with exactly the reference of the function/method one is currently running/measuring.
In order to solve the given task I will suggest around only instead of both before and
after for the former generates less overhead. The next code block exemplarily shows a
possible prototypal implementation. It also is the base for the afterwards following example
that finally might solve the OP's task.
(function (Function) {
var
isFunction = function (type) {
return (
(typeof type == "function")
&& (typeof type.call == "function")
&& (typeof type.apply == "function")
);
},
getSanitizedTarget = function (target) {
return ((target != null) && target) || null;
}
;
Function.prototype.around = function (handler, target) { // [around]
target = getSanitizedTarget(target);
var proceed = this;
return (isFunction(handler) && isFunction(proceed) && function () {
return handler.call(target, proceed, handler, arguments);
}) || proceed;
};
}(Function));
The next example takes into account that method-modification essentially relies on
functionality that is bound to an object. It is not just function wrapping. In order
to not loose the context a method is operating on, context has to be delegated /
passed around as target throughout all operations.
For this the example does not modify calculate since it is not bound to an object
but it modifies trigger instead.
var testObject = {
calculate: function (hugeInteger) {
var
i = hugeInteger,
k = 0
;
while (i--) {
k++;
}
return k;
},
trigger: function (hugeInteger) {
this.result = this.calculate(hugeInteger);
},
result: -1
};
console.log("testObject.result : ", testObject.result);
console.log("testObject.trigger(Math.pow(2, 26)) : ", testObject.trigger(Math.pow(2, 26))); // takes some time.
console.log("testObject.result : ", testObject.result);
console.log("testObject.someTrigger(0) : ", testObject.trigger(0)); // logs immediately after.
console.log("testObject.result : ", testObject.result);
testObject.trigger = testObject.trigger.around(function (proceed, interceptor, args) {
// before:
console.time(proceed);
// proceed:
proceed.apply(this, args);
// after:
console.timeEnd(proceed);
}, testObject); // omitting the 2nd argument - the [target] object - might break code that did work before.
console.log("testObject.trigger(Math.pow(2, 26)) : ", testObject.trigger(Math.pow(2, 26)));
console.log("testObject.result : ", testObject.result);
.as-console-wrapper { min-height: 100%!important; top: 0; }
<script>
(function (Function) {
var
isFunction = function (type) {
return (
(typeof type == "function")
&& (typeof type.call == "function")
&& (typeof type.apply == "function")
);
},
getSanitizedTarget = function (target) {
return ((target != null) && target) || null;
}
;
Function.prototype.around = function (handler, target) { // [around]
target = getSanitizedTarget(target);
var proceed = this;
return (isFunction(handler) && isFunction(proceed) && function () {
return handler.call(target, proceed, handler, arguments);
}) || proceed;
};
}(Function));
</script>
It should be simple to create JavaScript intepreter in JavaScript using eval. I got this (using jQuery terminal):
term = $('#term_demo').terminal(function(command, term) {
if (command !== '') {
var result = window.eval("(" + command + ")");
if (result !== undefined) {
term.echo(String(result));
}
} else {
term.echo('');
}
}, {
greetings: 'Javascript Interpreter',
name: 'js_demo',
height: 200,
prompt: 'js> '
});
Demo
but it don't work when I execute function foo() { ... } the foo is not defined I need to use foo = function() { ... }. eval act like executed within (function() { <code> })(). Can it be there more complicated code that will not work too?
Is it possible to create a JavaScript interpreter using simple code without use the of js.js, that will work the same as browser console?
I've created a bookmarklet which appends a kind of REPL in a page, designed for the major five browsers (Chrome 1+, IE 6+, Firefox 1+, Safari 3+, Opera 9+Can't remember the exacte version).
The core component, which evaluates the code is posted below, slightly modified + annotated.
/**
* Evaluates some code in the global scope.
* #param String code: Code to eval
* #return Stringified result, prefixed with 'E:' if error.
*/
function globalEval(/*string*/ code) {
var win = window, r, raw;
try {
if (win.execScript) { // eval in IE sucks, so use execScript instead
r = win.$_$_$globalEval$_$_$ = {c:code};
win.execScript('try{$_$_$globalEval$_$_$.r=eval($_$_$globalEval$_$_$.c);}catch(e){$_$_$globalEval$_$_$.e=e}');
// /*Optional clean-up:*/ delete win.$_$_$globalEval$_$_$;
if (r.e) throw r.e; // Error
raw = r.r;
} else {
raw = win.eval(code);
}
r = '' + raw; // Stringify in the try-block
// It is possible that an error is thrown
// for example, for the following code: ({toString:1})
} catch(err) {
r = (err + ''); // Convert error to string
// IE: If found, "[object" will be at index zero, which is falsy
if (!r.indexOf('[object')) r = err.message;
// r =
r = 'E:' + (raw=r);
} finally {
// raw = unmodified result (or Error instance)
// FOR THIS EXAMPLE, raw is not used, and string r is returned
return /*string*/ r;
}
}
I've implemented the functionality in a form, which contains several controls including an input+output textarea.
Note: The code is evaluated in the global context. And such, any variables in code will be leaked to the global scope. For an interpreter, you could use an iframe to create a new scope (and modify var win in my function).
var win = frames['name_of_frame'], ... // or
var win = frame_DOM_element.contentWindow, ...
The parenthesis you're appending around your incoming command produces illegal syntax. You should wrap it with an anonymous self-executing function instead.
Example: http://jsfiddle.net/bW6Fv/1/
var command = "function foo(x){return x+x;} alert(foo(10));";
window.eval("!function(){" + command + "}()");
EDIT
If you want your evaluated scripts to be available globally, you'll have to leave them unwrapped or explicitly assign values to the global window object.
Example: http://jsfiddle.net/bW6Fv/2/
var command = "window.foo = function(x){return x+x;}; alert(foo(10));";
window.eval("!function(){ " + command + "}();");
window.eval("alert(foo(20));");
command = "function bar(x){return x / 2;}; alert(bar(10));";
window.eval(command);
window.eval("alert(bar(20));");
Given a function, I'm trying to find out the names of the nested functions in it (only one level deep).
A simple regex against toString() worked until I started using functions with comments in them. It turns out that some browsers store parts of the raw source while others reconstruct the source from what's compiled; The output of toString() may contain the original code comments in some browsers. As an aside, here are my findings:
Test subject
function/*post-keyword*/fn/*post-name*/()/*post-parens*/{
/*inside*/
}
document.write(fn.toString());
Results
Browser post-keyword post-name post-parens inside
----------- ------------ --------- ----------- --------
Firefox No No No No
Safari No No No No
Chrome No No Yes Yes
IE Yes Yes Yes Yes
Opera Yes Yes Yes Yes
I'm looking for a cross-browser way of extracting the nested function names from a given function. The solution should be able to extract "fn1" and "fn2" out of the following function:
function someFn() {
/**
* Some comment
*/
function fn1() {
alert("/*This is not a comment, it's a string literal*/");
}
function // keyword
fn2 // name
(x, y) // arguments
{
/*
body
*/
}
var f = function () { // anonymous, ignore
};
}
The solution doesn't have to be pure regex.
Update: You can assume that we're always dealing with valid, properly nested code with all string literals, comments and blocks terminated properly. This is because I'm parsing a function that has already been compiled as a valid function.
Update2: If you're wondering about the motivation behind this: I'm working on a new JavaScript unit testing framework that's called jsUnity. There are several different formats in which you can write tests & test suites. One of them is a function:
function myTests() {
function setUp() {
}
function tearDown() {
}
function testSomething() {
}
function testSomethingElse() {
}
}
Since the functions are hidden inside a closure, there's no way for me invoke them from outside the function. I therefore convert the outer function to a string, extract the function names, append a "now run the given inner function" statement at the bottom and recompile it as a function with new Function(). If the test function have comments in them, it gets tricky to extract the function names and to avoid false positives. Hence I'm soliciting the help of the SO community...
Update3: I've come up with a new solution that doesn't require a lot of semantic fiddling with code. I use the original source itself to probe for first-level functions.
Cosmetic changes and bugfix
The regular expression must read \bfunction\b to avoid false positives!
Functions defined in blocks (e.g. in the bodies of loops) will be ignored if nested does not evaluate to true.
function tokenize(code) {
var code = code.split(/\\./).join(''),
regex = /\bfunction\b|\(|\)|\{|\}|\/\*|\*\/|\/\/|"|'|\n|\s+/mg,
tokens = [],
pos = 0;
for(var matches; matches = regex.exec(code); pos = regex.lastIndex) {
var match = matches[0],
matchStart = regex.lastIndex - match.length;
if(pos < matchStart)
tokens.push(code.substring(pos, matchStart));
tokens.push(match);
}
if(pos < code.length)
tokens.push(code.substring(pos));
return tokens;
}
var separators = {
'/*' : '*/',
'//' : '\n',
'"' : '"',
'\'' : '\''
};
function extractInnerFunctionNames(func, nested) {
var names = [],
tokens = tokenize(func.toString()),
level = 0;
for(var i = 0; i < tokens.length; ++i) {
var token = tokens[i];
switch(token) {
case '{':
++level;
break;
case '}':
--level;
break;
case '/*':
case '//':
case '"':
case '\'':
var sep = separators[token];
while(++i < tokens.length && tokens[i] !== sep);
break;
case 'function':
if(level === 1 || (nested && level)) {
while(++i < tokens.length) {
token = tokens[i];
if(token === '(')
break;
if(/^\s+$/.test(token))
continue;
if(token === '/*' || token === '//') {
var sep = separators[token];
while(++i < tokens.length && tokens[i] !== sep);
continue;
}
names.push(token);
break;
}
}
break;
}
}
return names;
}
The academically correct way to handle this would be creating a lexer and parser for a subset of Javascript (the function definition), generated by a formal grammar (see this link on the subject, for example).
Take a look at JS/CC, for a Javascript parser generator.
Other solutions are just regex hacks, that lead to unmaintainable/unreadable code and probably to hidden parsing errors in particular cases.
As a side note, I'm not sure to understand why you aren't specifying the list of unit test functions in your product in a different way (an array of functions?).
Would it matter if you defined your tests like:
var tests = {
test1: function (){
console.log( "test 1 ran" );
},
test2: function (){
console.log( "test 2 ran" );
},
test3: function (){
console.log( "test 3 ran" );
}
};
Then you could run them as easily as this:
for( var test in tests ){
tests[test]();
}
Which looks much more easier.
You can even carry the tests around in JSON that way.
I like what you're doing with jsUnity. And when I see something I like (and have enough free time ;)), I try to reimplement it in a way which better suits my needs (also known as 'not-invented-here' syndrome).
The result of my efforts is described in this article, the code can be found here.
Feel free to rip-out any parts you like - you can assume the code to be in the public domain.
The trick is to basically generate a probe function that will check if a given name is the name of a nested (first-level) function. The probe function uses the function body of the original function, prefixed with code to check the given name within the scope of the probe function. OK, this can be better explained with the actual code:
function splitFunction(fn) {
var tokens =
/^[\s\r\n]*function[\s\r\n]*([^\(\s\r\n]*?)[\s\r\n]*\([^\)\s\r\n]*\)[\s\r\n]*\{((?:[^}]*\}?)+)\}\s*$/
.exec(fn);
if (!tokens) {
throw "Invalid function.";
}
return {
name: tokens[1],
body: tokens[2]
};
}
var probeOutside = function () {
return eval(
"typeof $fn$ === \"function\""
.split("$fn$")
.join(arguments[0]));
};
function extractFunctions(fn) {
var fnParts = splitFunction(fn);
var probeInside = new Function(
splitFunction(probeOutside).body + fnParts.body);
var tokens;
var fns = [];
var tokenRe = /(\w+)/g;
while ((tokens = tokenRe.exec(fnParts.body))) {
var token = tokens[1];
try {
if (probeInside(token) && !probeOutside(token)) {
fns.push(token);
}
} catch (e) {
// ignore token
}
}
return fns;
}
Runs fine against the following on Firefox, IE, Safari, Opera and Chrome:
function testGlobalFn() {}
function testSuite() {
function testA() {
function testNested() {
}
}
// function testComment() {}
// function testGlobalFn() {}
function // comments
testB /* don't matter */
() // neither does whitespace
{
var s = "function testString() {}";
}
}
document.write(extractFunctions(testSuite));
// writes "testA,testB"
Edit by Christoph, with inline answers by Ates:
Some comments, questions and suggestions:
Is there a reason for checking
typeof $fn$ !== "undefined" && $fn$ instanceof Function
instead of using
typeof $fn$ === "function"
instanceof is less safe than using typeof because it will fail when passing objects between frame boundaries. I know that IE returns wrong typeof information for some built-in functions, but afaik instanceof will fail in these cases as well, so why the more complicated but less safe test?
[AG] There was absolutely no legitimate reason for it. I've changed it to the simpler "typeof === function" as you suggested.
How are you going to prevent the wrongful exclusion of functions for which a function with the same name exists in the outer scope, e.g.
function foo() {}
function TestSuite() {
function foo() {}
}
[AG] I have no idea. Can you think of anything. Which one is better do you think? (a) Wrongful exclusion of a function inside. (b) Wronfgul inclusion of a function outside.
I started to think that the ideal solution will be a combination of your solution and this probing approach; figure out the real function names that are inside the closure and then use probing to collect references to the actual functions (so that they can be directly called from outside).
It might be possible to modify your implementation so that the function's body only has to be eval()'ed once and not once per token, which is rather inefficient. I might try to see what I can come up with when I have some more free time today...
[AG] Note that the entire function body is not eval'd. It's only the bit that's inserted to the top of the body.
[CG] Your right - the function's body only gets parsed once during the creation of probeInside - you did some nice hacking, there ;). I have some free time today, so let's see what I can come up with...
A solution that uses your parsing method to extract the real function names could just use one eval to return an array of references to the actual functions:
return eval("[" + fnList + "]");
[CG] Here is with what I came up. An added bonus is that the outer function stays intact and thus may still act as closure around the inner functions. Just copy the code into a blank page and see if it works - no guarantees on bug-freelessness ;)
<pre><script>
var extractFunctions = (function() {
var level, names;
function tokenize(code) {
var code = code.split(/\\./).join(''),
regex = /\bfunction\b|\(|\)|\{|\}|\/\*|\*\/|\/\/|"|'|\n|\s+|\\/mg,
tokens = [],
pos = 0;
for(var matches; matches = regex.exec(code); pos = regex.lastIndex) {
var match = matches[0],
matchStart = regex.lastIndex - match.length;
if(pos < matchStart)
tokens.push(code.substring(pos, matchStart));
tokens.push(match);
}
if(pos < code.length)
tokens.push(code.substring(pos));
return tokens;
}
function parse(tokens, callback) {
for(var i = 0; i < tokens.length; ++i) {
var j = callback(tokens[i], tokens, i);
if(j === false) break;
else if(typeof j === 'number') i = j;
}
}
function skip(tokens, idx, limiter, escapes) {
while(++idx < tokens.length && tokens[idx] !== limiter)
if(escapes && tokens[idx] === '\\') ++idx;
return idx;
}
function removeDeclaration(token, tokens, idx) {
switch(token) {
case '/*':
return skip(tokens, idx, '*/');
case '//':
return skip(tokens, idx, '\n');
case ')':
tokens.splice(0, idx + 1);
return false;
}
}
function extractTopLevelFunctionNames(token, tokens, idx) {
switch(token) {
case '{':
++level;
return;
case '}':
--level;
return;
case '/*':
return skip(tokens, idx, '*/');
case '//':
return skip(tokens, idx, '\n');
case '"':
case '\'':
return skip(tokens, idx, token, true);
case 'function':
if(level === 1) {
while(++idx < tokens.length) {
token = tokens[idx];
if(token === '(')
return idx;
if(/^\s+$/.test(token))
continue;
if(token === '/*') {
idx = skip(tokens, idx, '*/');
continue;
}
if(token === '//') {
idx = skip(tokens, idx, '\n');
continue;
}
names.push(token);
return idx;
}
}
return;
}
}
function getTopLevelFunctionRefs(func) {
var tokens = tokenize(func.toString());
parse(tokens, removeDeclaration);
names = [], level = 0;
parse(tokens, extractTopLevelFunctionNames);
var code = tokens.join('') + '\nthis._refs = [' +
names.join(',') + '];';
return (new (new Function(code)))._refs;
}
return getTopLevelFunctionRefs;
})();
function testSuite() {
function testA() {
function testNested() {
}
}
// function testComment() {}
// function testGlobalFn() {}
function // comments
testB /* don't matter */
() // neither does whitespace
{
var s = "function testString() {}";
}
}
document.writeln(extractFunctions(testSuite).join('\n---\n'));
</script></pre>
Not as elegant as LISP-macros, but still nice what JAvaScript is capable of ;)
<pre>
<script type="text/javascript">
function someFn() {
/**
* Some comment
*/
function fn1() {
alert("/*This is not a comment, it's a string literal*/");
}
function // keyword
fn2 // name
(x, y) // arguments
{
/*
body
*/
}
function fn3() {
alert("this is the word function in a string literal");
}
var f = function () { // anonymous, ignore
};
}
var s = someFn.toString();
// remove inline comments
s = s.replace(/\/\/.*/g, "");
// compact all whitespace to a single space
s = s.replace(/\s{2,}/g, " ");
// remove all block comments, including those in string literals
s = s.replace(/\/\*.*?\*\//g, "");
document.writeln(s);
// remove string literals to avoid false matches with the keyword 'function'
s = s.replace(/'.*?'/g, "");
s = s.replace(/".*?"/g, "");
document.writeln(s);
// find all the function definitions
var matches = s.match(/function(.*?)\(/g);
for (var ii = 1; ii < matches.length; ++ii) {
// extract the function name
var funcName = matches[ii].replace(/function(.+)\(/, "$1");
// remove any remaining leading or trailing whitespace
funcName = funcName.replace(/\s+$|^\s+/g, "");
if (funcName === '') {
// anonymous function, discard
continue;
}
// output the results
document.writeln('[' + funcName + ']');
}
</script>
</pre>
I'm sure I missed something, but from your requirements in the original question, I think I've met the goal, including getting rid of the possibility of finding the function keyword in string literals.
One last point, I don't see any problem with mangling the string literals in the function blocks. Your requirement was to find the function names, so I didn't bother trying to preserve the function content.