How can I improve this script to delete a left over table? - javascript

This script basically makes it so that it deletes content from specific users from a forum. It is working as intended but there is still a left over element (table) from the deleted content. Any ideas are highly appreciated.
Code:
https://greasyfork.org/scripts/2690-fitmisc-total-ignore/code
// ==UserScript==
// #name Fitmisc_Total_Ignore
// #author Arris
// #description This script is designed to completly eradicate from sight the worst posters on Fitmisc.com
// #include http://fitmisc.com/*
// #namespace http://fitmisc.com/
// #version 0.9
// ==/UserScript==
function canIgnore(sUser) {
if( sUser.match(/niko/i) )
return true;
if( sUser.match(/thesavagepony/i) )
return true;
if( sUser.match(/Lloyd Banks/i) )
return true;
if( sUser.match(/Lil B/i) )
return true;
if( sUser.match(/Round-Mound/i) )
return true;
return false;
}
function setIgnoreThread() {
var a; var s;
a=document.evaluate(
"//div[starts-with(#class, 'threadmeta')]",
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);
for (var i=0; i<a.snapshotLength; i++) {
s=a.snapshotItem(i).innerHTML;
if( canIgnore(s) ) {
//a.snapshotItem(i).parentNode.parentNode.parentNode.style.display = 'none';
a.snapshotItem(i).parentNode.parentNode.parentNode.innerHTML = '';
}
}
}
function setIgnorePost() {
var a; var s;
a=document.evaluate(
"//div[starts-with(#class, 'username_container')]",
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);
for (var i=0; i<a.snapshotLength; i++) {
s=a.snapshotItem(i).innerHTML;
if( canIgnore(s) ) {
//a.snapshotItem(i).parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.style.display = 'none';
a.snapshotItem(i).parentNode.parentNode.parentNode.innerHTML = '<li class="postbitlegacy postbitim postcontainer old" style="background:white;border-color:white;"></li>';
}
}
}
function setIgnoreQuote() {
var a; var s;
a=document.evaluate(
"//div[starts-with(#class, 'bbcode_postedby')]",
document,
null,
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
null);
for (var i=0; i<a.snapshotLength; i++) {
s=a.snapshotItem(i).innerHTML;
if( canIgnore(s) ) {
//a.snapshotItem(i).parentNode.parentNode.parentNode.style.display = 'none';
a.snapshotItem(i).parentNode.innerHTML = '';
}
}
}
if(window.opera) { //opera only
(function(){
document.addEventListener('DOMContentLoaded', function() {
setIgnoreThread();
setIgnorePost();
setIgnoreQuote();
}, false);
})()
} else {
setIgnoreThread();
setIgnorePost();
setIgnoreQuote();
}

Instead of hardcode the usernames each in a dedicated regulare expression consider a generation of a single expression matching any item of an array of ignored user names. Use the RegExp.test() method when you don't need to store a result.
(I've removed user names in my examples, except the anonymous operator account "OP". I don't like defamation in the public.)
var
ignoredUsers = ['user 1', 'another user', 'OP', 'yet another user']
;
function canIgnore(sUser)
{
return new RegExp(ignoredUsers.join('|'), 'i').test(sUser);
}
However, there's no need to extract user names, check them against regular expressions and do further processing, since you are using XPath expressions which can find content and step back in the DOM tree. They are capable to retrieve all text nodes with one of a list of users inside a container class and give back their ancestors ([grand]parents) selected by a class.
// = =UserScript= =
// #name Fitmisc_Total_Ignore
// #author Arris
// #description This script is designed to completly eradicate from sight the worst posters on Fitmisc.com
// #include http://fitmisc.com/*
// #namespace http://fitmisc.com/
// #grant GM_xpath
// #version 0.9
// = =/UserScript= =
var
// TODO: GM_ get/set Value as JSON ; put users into igno list by mouse click
ignoredUsers = ['user 1', 'another user', 'OP', 'yet another user']
;
function removeContents(ignoreList)
{
if(ignoreList instanceof Array && ignoreList.length)
GM_xpath
( { path: "//div[#class='username_container']//*[text() = '"
+ ignoreList.map(function(user) { return user.replace("'", "\\'"); } ).join("' or text() = '")
+ "']/ancestor::li[contains(#class, 'postcontainer')]",
all :true
}
).forEach(function(elem){ elem.parentElement.removeChild(elem); });
}
removeContents(ignoredUsers);
Since users accounts could be renamed, consider to filter user id, e.g. in the href attribute of profile link or photo, instead of relying on changable names.
The expample above does the job on thread posts. You can combine multiple XPath expressions into one by the "|" char.

Related

Underscorejs 'find' not working as expected

I use the following code, in a nodejs app, to build a tree from an array of database rows that form an adjacency list:
// Lay out every node in the tree in one flat array.
var flatTree = [];
_.each(rows, function(row) {
flatTree.push(row);
});
// For each node, find its parent and add it to that parent's children.
_.each(rows, function(row) {
// var parent = _.find(flatTree, function(p) {
// p.Id == row.ParentId;
// });
var parent;
for (var i = 0; i < flatTree.length; i++){
if (flatTree[i].Id == row.ParentId) {
parent = flatTree[i];
break;
}
};
if (parent){
if (!parent.subItems) {
parent.subItems = [];
};
parent.subItems.push(row);
}
});
I expect the commented out _.find call to do exactly the same as what the work-around for loop below it does, but _.find never finds the parent node in flatTree, while the for loop always does.
Similarly, a call to _.filter just doesn't work either, while the substitute loop does:
// var rootItems = _.filter(flatTree, function (node) {
// //node.ParentId === null;
// node.NoParent === 1;
// })
var rootItems = [];
for (var i = 0; i < flatTree.length; i++){
if (flatTree[i].ParentId == null){
rootItems.push(flatTree[i]);
}
}
I am using the underscore-node package, but have tried and had the same results with the regular underscore package.
Just missed the return.
var parent = _.find(flatTree, function(p) {
return p.Id == row.ParentId; // Return true if the ID matches
^^^^^^ <-- This
});
In your code nothing is returned, so by default undefined will be returned and parent will not contain any data.

how to code around storing data (objects, custom attributes) on DOM nodes

I wanted to write some pure javascript to better understand it (I realize in "real practice" that frameworks such as jQuery are much more advised and applicable, but this isn't really about how to use frameworks, more about how pure javascript works and best practices).
Anyways, I wrote some simple javascript code. I wanted to create a set of groups of buttons that had one state at a time from the set {on,off} and each state would map to a corresponding function to be fired upon entering that state. Each group of buttons within the master set could contain only one button in the on state at a time. The concept is similar to the idea of radio buttons. Why not use a radio button then? Well semantically it's just suppose to be some buttons for some control elements, but either way I suppose I could have but the question isn't really about that.
The thing is, to pull this off, I added a lot of custom attributes to specific button elements by id in my Javascript. I was doing some research, and found this question and this question, regarding using custom attributes on DOM node (objects). They seem to advocate against such a practice, one even goes so far as to say that doing so could cause potential memory leaks depending on the browser's implementation.
However, for each button I create I need to keep track of a lot of attributes, and if I expand this script I may have to add even more. So what's the best way around storing them on the DOM node but still keeping track of everything and being able to use this in attached functions, etc. al?
It wasn't readily obvious to me how to do this without at the minimum storing a reference of a well name spaced object to the DOM node button element.
I was able to see that from this question jQuery has some way to do this, but I want to know how this is done with just pure javascript.
Here's the full sample code I am working with:
<!DOCTYPE html>
<html>
<head>
<title>Button Test Script</title>
<script language="javascript" type="text/javascript">
window.button_groups = {};
function isset( type ) {
return !(type==='undefined');
}
function debug( txt ) {
if( !isset(typeof console) ) {
alert( txt );
} else {
console.log(txt);
}
}
function img( src ) {
var t = new Image();
t.src = src;
return t;
}
function turnGroupOff( group ) {
if( isset(typeof window.button_groups[group]) ) {
for( var i = 0; i < window.button_groups[group].length; i++ ) {
if( window.button_groups[group][i].toggle == 1)
window.button_groups[group][i].click();
}
}
}
/**
* buttonId = id attribute of <button>
* offImg = src of img for off state of button
* onImg = src of img for on state of button
* on = function to be fired when button enters on state
* off = function to be fired when button enters off state
*/
function newButton( buttonId, offImg, onImg, group, on, off ) {
var b = document.getElementById(buttonId);
b.offImg = img( offImg );
b.onImg = img( onImg );
b.on = on;
b.off = off;
b.img = document.createElement('img');
b.appendChild(b.img);
b.img.src = b.offImg.src;
b.group = group;
b.toggle = 0;
b.onclick = function() {
switch(this.toggle) {
case 0:
turnGroupOff( this.group );
this.on();
this.toggle = 1;
this.img.src = this.onImg.src;
break;
case 1:
this.off();
this.toggle = 0;
this.img.src = this.offImg.src;
break;
}
}
if( !isset(typeof window.button_groups[group]) )
window.button_groups[group] = [];
window.button_groups[group].push(b);
}
function init() {
var on = function() { debug(this.id + " turned on") };
newButton( 'button1', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group1',
on,
function() { debug(this.id + " turned off"); }
);
newButton( 'button2', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group1',
on,
function() { debug(this.id + " turned off (diff then usual turn off)"); }
);
newButton( 'button3', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group2',
on,
function() { debug(this.id + " turned off (diff then usual turn off2)"); }
);
newButton( 'button4', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group2',
on,
function() { debug(this.id + " turned off (diff then usual turn off3)"); }
);
}
window.onload = init;
</script>
</head>
<body>
<button id="button1" type="button"></button>
<button id="button2" type="button"></button>
<br/>
<button id="button3" type="button"></button>
<button id="button4" type="button"></button>
</body>
</html>
UPDATE
The jQuery thing was a bit overkill for my purposes. I don't need to extend an arbitrary element. I have a good idea of how that is done now specific to jQuery (with the arbitrary randomly named attribute storing a cache index integer).
I know ahead of time which host elements I need to extend, and how; also I can/want to setup an id attribute on each of them on the HTML side.
So, inspired by the jQuery setup, I decided to also create a global cache variable except I am going to use the DOM node's id attribute as my cache key. Since it should be a unique identifier (by definition), and I have no plans to dynamically alter id's ever, this should be a simple task. It completely divorces my Javascript objects from the DOM objects, but it does make my code look quite a bit uglier and difficult to read with the many calls to data. I present the modifications below:
<!DOCTYPE html>
<html>
<head>
<title>Button Test Script</title>
<script language="javascript" type="text/javascript">
window.button_groups = {};
function isset( type ) { // For browsers that throw errors for !object syntax
return !(type==='undefined');
}
var c = { // For browsers without console support
log: function( o ) {
if( isset(typeof console) ) {
console.log(o);
} else {
alert( o );
}
},
dir: function( o ) {
if( isset(typeof console) ) {
console.dir(o);
}
}
};
function img( src ) { // To avoid repeats of setting new Image src
var t = new Image();
t.src = src;
return t;
}
var cache = {};
function data( elemId, key, data ) { // retrieve/set data tied to element id
if(isset(typeof data)) {// setting data
if(!isset(typeof cache[elemId]))
cache[elemId] = {};
cache[elemId][key] = data;
} else { // retreiving data
return cache[elemId][key];
}
}
var button_groups = {}; // set of groups of buttons
function turnGroupOff( group ) { // turn off all buttons within a group
if( isset(typeof window.button_groups[group]) ) {
for( var i = 0; i < window.button_groups[group].length; i++ ) {
if( data(window.button_groups[group][i].id, 'toggle') == 1)
window.button_groups[group][i].click();
}
}
}
/**
* buttonId = id attribute of <button>
* offImg = src of img for off state of button
* onImg = src of img for on state of button
* on = function to be fired when button enters on state
* off = function to be fired when button enters off state
*/
function newButton( buttonId, offImg, onImg, group, on, off ) {
var b = document.getElementById(buttonId);
data( b.id, 'offImg', img( offImg ) );
data( b.id, 'onImg', img( onImg ) );
data( b.id, 'on', on );
data( b.id, 'off', off );
var btnImg = document.createElement('img');
btnImg.src = data( b.id, 'offImg' ).src;
data( b.id, 'img', btnImg );
b.appendChild( btnImg );
data( b.id, 'group', group );
data( b.id, 'toggle', 0 );
var click = function() {
switch(data(this.id,'toggle')) {
case 0:
turnGroupOff( data(this.id,'group') );
(data(this.id,'on'))();
data(this.id,'toggle',1);
data(this.id,'img').src = data(this.id,'onImg').src;
break;
case 1:
(data(this.id,'off'))();
data(this.id,'toggle',0);
data(this.id,'img').src = data(this.id,'offImg').src;
break;
}
}
b.onclick = click;
if( !isset(typeof window.button_groups[group]) )
window.button_groups[group] = [];
window.button_groups[group].push(b);
}
function init() {
var on = function() { c.log(this.id + " turned on") };
newButton( 'button1', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group1',
on,
function() { c.log(this.id + " turned off"); }
);
newButton( 'button2', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group1',
on,
function() { c.log(this.id + " turned off (diff then usual turn off)"); }
);
newButton( 'button3', 'images/apply-off.jpg', 'images/apply-on.jpg', 'group2',
on,
function() { c.log(this.id + " turned off (diff then usual turn off2)"); }
);
newButton( 'button4', 'images/unapply-off.jpg', 'images/unapply-on.jpg', 'group2',
on,
function() { c.log(this.id + " turned off (diff then usual turn off3)"); }
);
}
window.onload = init;
</script>
</head>
<body>
<button id="button1" type="button"></button>
<button id="button2" type="button"></button>
<br/>
<button id="button3" type="button"></button>
<button id="button4" type="button"></button>
</body>
</html>
UPDATE 2
I found that through using the power of closure I truly only need to store one "special" attribute, that is the group the button belonged to.
I changed the newButton function to the following, which through closure, eliminates the need to store many of those other things I was:
function newButton( buttonId, offImg, onImg, group, on, off ) {
var b = document.getElementById(buttonId);
offImg = img( offImg );
onImg = img( onImg );
var btnImg = document.createElement('img');
btnImg.src = offImg.src;
b.appendChild( btnImg );
data( b.id, 'group', group );
var toggle = 0;
var click = function(event) {
switch(toggle) {
case 0:
turnGroupOff( data(this.id,'group') );
if( on(event) ) {
toggle = 1;
btnImg.src = onImg.src;
}
break;
case 1:
if( off(event) ) {
toggle = 0;
btnImg.src = offImg.src;
}
break;
}
}
b.onclick = click;
if( !isset(typeof window.button_groups[group]) )
window.button_groups[group] = [];
window.button_groups[group].push(b);
b = null;
}
You either extend objects (which is bad for host objects) or you wrap the objects as jQuery does, using the wrapped object to identify associated data in the hash table. In essence you hash the DOM node and do a lookup in a hash table for the associated data. Of course you still need to extend the host object, but you add only a single property which you know to be reasonably safe to add across browsers, rather than a set of arbitrary properties. If you inspect an element with associated data you might see something like element.jQuery171023696433915756643, which contains the internal storage index for that element. I would recommend reading the jQuery source if you are that interested, particularly the data() function
data: function( elem, name, data, pvt /* Internal Use Only */ ) {
if ( !jQuery.acceptData( elem ) ) {
return;
}
var privateCache, thisCache, ret,
internalKey = jQuery.expando,
getByName = typeof name === "string",
// We have to handle DOM nodes and JS objects differently because IE6-7
// can't GC object references properly across the DOM-JS boundary
isNode = elem.nodeType,
// Only DOM nodes need the global jQuery cache; JS object data is
// attached directly to the object so GC can occur automatically
cache = isNode ? jQuery.cache : elem,
// Only defining an ID for JS objects if its cache already exists allows
// the code to shortcut on the same path as a DOM node with no cache
id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey,
isEvents = name === "events";
// Avoid doing any more work than we need to when trying to get data on an
// object that has no data at all
if ( (!id || !cache[id] || (!isEvents && !pvt && !cache[id].data)) && getByName && data === undefined ) {
return;
}
if ( !id ) {
// Only DOM nodes need a new unique ID for each element since their data
// ends up in the global cache
if ( isNode ) {
elem[ internalKey ] = id = ++jQuery.uuid;
} else {
id = internalKey;
}
}
if ( !cache[ id ] ) {
cache[ id ] = {};
// Avoids exposing jQuery metadata on plain JS objects when the object
// is serialized using JSON.stringify
if ( !isNode ) {
cache[ id ].toJSON = jQuery.noop;
}
}
// An object can be passed to jQuery.data instead of a key/value pair; this gets
// shallow copied over onto the existing cache
if ( typeof name === "object" || typeof name === "function" ) {
if ( pvt ) {
cache[ id ] = jQuery.extend( cache[ id ], name );
} else {
cache[ id ].data = jQuery.extend( cache[ id ].data, name );
}
}
privateCache = thisCache = cache[ id ];
// jQuery data() is stored in a separate object inside the object's internal data
// cache in order to avoid key collisions between internal data and user-defined
// data.
if ( !pvt ) {
if ( !thisCache.data ) {
thisCache.data = {};
}
thisCache = thisCache.data;
}
if ( data !== undefined ) {
thisCache[ jQuery.camelCase( name ) ] = data;
}
// Users should not attempt to inspect the internal events object using jQuery.data,
// it is undocumented and subject to change. But does anyone listen? No.
if ( isEvents && !thisCache[ name ] ) {
return privateCache.events;
}
// Check for both converted-to-camel and non-converted data property names
// If a data property was specified
if ( getByName ) {
// First Try to find as-is property data
ret = thisCache[ name ];
// Test for null|undefined property data
if ( ret == null ) {
// Try to find the camelCased property
ret = thisCache[ jQuery.camelCase( name ) ];
}
} else {
ret = thisCache;
}
return ret;
}
I found this article on javascript design patterns that may give you some ideas. Have a look at The Prototype Pattern, this allows you to reuse methods across instances.

YUI3 Plugin.base not rendering

I'm trying to use a yui plugin that pulls from a json file and populates a div on the page. Everything should be a go, however, since the plugin never gets to the render stage, the rest of it does not run. It is successfully loaded otherwise (if I stick an alert or console.log at the beginning of the event, it works fine).
Here's the code:
YUI.add('events', function(Y) {
var urlEvents = //"/cgi-bin/eventview-json/?cal=admissions/events&days=10";
"/admissions/events/events.json";
//var eventContainer = $("#insert-events");
/* EventList class constructor */
var EventList = function(config) {
EventList.superclass.constructor.apply(this, arguments);
};
/*
* Required NAME static field, to identify the class and
* used as an event prefix, to generate class names etc. (set to the
* class name in camel case).
*/
EventList.NAME = "EventList";
/*
* Required NS static field, to identify the property on the host which will,
* be used to refer to the plugin instance ( e.g. host.feature.doSomething() )
*/
EventList.NS = "EventList";
/*
* The attribute configuration for the plugin. This defines the core user facing state of the plugin
*/
EventList.ATTRS = {};
var convertYYYYMMDDtoJS = function(s) {
var a, jsdate = null;
try {
a = /^(\d\d\d\d)(\d\d)(\d\d)$/.exec(s);
if (a) {
jsdate = new Date(a[1], a[2]-1, a[3]);
}
} catch (ex) {
/* Nothing */
}
return jsdate;
};
var insertEvents = function(id, response, e) {
alert('hello');
var i, resp, events, event, html, jsdate, label, seenevent, yyyymmdd;
var maxevents = 5, eventcount;
try {
resp = Y.JSON.parse(response.responseText);
events = resp.results;
html = "";
seenevent = {};
eventcount = 0;
yyyymmdd = "";
for (i = 0; i < events.length; i++) {
event = events[i];
if (seenevent[event.title]) {
continue;
}
seenevent[event.title] = true;
if (event.date !== yyyymmdd) {
// This is the first event on this date.
// If we've seen maxevents events, then bail before starting a new day.
if (eventcount >= maxevents) {
break;
}
// Put out a new label for this day.
jsdate = convertYYYYMMDDtoJS(event.date);
label = Y.DataType.Date.format(jsdate, {format: "%b %e %a"});
/*
* The first empty div below, "<div class='clear'></div>" is only needed for IE 7.
* IE 7 does not properly clear both left and right floats when "clear: both" is specified
* if the element itself is floated. The extra div clears the floats, but isn't floated
* itself. The extra div doesn't cause any grief in newer browsers, so I add it always.
*/
html += "<div class='clear'></div><div class='event-datelabel'>" + label + "</div>\n";
yyyymmdd = event.date;
}
html += "<div class='event-text'>" + event.html + "</div>\n";
eventcount++;
}
this.get('host').setContent(html + "<div id='events-footer'><a href='/calendar/'>all events</a></div>");
} catch(ex) {
console.log("Error", ex);
this.get('host').setContent("Event list not available");
return;
}
};
var insertEventList = function(yyyy, mm, dd) {
var url = urlEvents;
if (yyyy) {
url += '&yyyy=' + yyyy;
}
if (mm) {
url += '&mm=' + mm;
}
if (dd) {
url += '&dd=' + dd;
}
Y.io(url, {on: {success: insertEvents}, context: this});
};
/* EventList extends the base Plugin.Base class */
Y.extend(EventList, Y.Plugin.Base, {
render: function() {
insertEventList.call(this);
}
});
//console.log("assigning", EventList);
Y.namespace("Plugin").EventList = EventList;
}, '1.0.0' ,{requires:['node', 'base', 'plugin', "json-parse", "datatype-date"]});
Here's the excerpt from the code with the render bit:
Y.extend(EventList, Y.Plugin.Base, {
render: function() {
insertEventList.call(this);
}
Admittedly, YUI3 confuses me, and I would prefer other libraries, but I don't have a choice in this situation. There's likely one thing that I've just completely looked over.
Thanks guys
I've used YUI3 plugins before and they are a bit difficult to grasp, but I'll try to help if I can. Once you've created the plugin, which, from what I can tell, you've already done so successfully, you plug it into an object somewhere else in your code:
someObj.plug(Y.Plugin.EventList, cfg);
After that, you can access the plugin's methods from within the object's plugin namespace. In your case you'd do this like so:
someObj.EventList.render();
Hopefully I'm understanding your question correctly and I hope that helps clear some stuff up for you. Good luck! :)

How can I make this javascript easier to read, maintain, and understand from an OO background?

I come from the land of Java, C#, etc. I am working on a javascript report engine for a web application I have. I am using jQuery, AJAX, etc. I am having difficulty making things work the way I feel they should - for instance, I have gone to what seems like too much trouble to make sure that when I make an AJAX call, my callback has access to the object's members. Those callback functions don't need to be that complicated, do they? I know I must be doing something wrong. Please point out what I could be doing better - let me know if the provided snippet is too much/too little/too terrible to look at.
What I'm trying to do:
On page load, I have a select full of users.
I create the reports (1 for now) and add them to a select box.
When both a user and report are selected, I run the report.
The report involves making a series of calls - getting practice serieses, leagues, and tournaments - for each league and tournament, it gets all of those serieses, and then for each series it grabs all games.
It maintains a counter of the calls that are active, and when they have all completed the report is run and displayed to the user.
Code:
//Initializes the handlers and reports
function loadUI() {
loadReports();
$("#userSelect").change(updateRunButton);
$("#runReport").click(runReport);
updateRunButton();
return;
$("#userSelect").change(loadUserGames);
var user = $("#userSelect").val();
if(user) {
getUserGames(user);
}
}
//Creates reports and adds them to the select
function loadReports() {
var reportSelect = $("#reportSelect");
var report = new SpareReport();
engine.reports[report.name] = report;
reportSelect.append($("<option/>").text(report.name));
reportSelect.change(updateRunButton);
}
//The class that represents the 1 report we can run right now.
function SpareReport() {
this.name = "Spare Percentages";
this.activate = function() {
};
this.canRun = function() {
return true;
};
//Collects the data for the report. Initializes/resets the class variables,
//and initiates calls to retrieve all user practices, leagues, and tournaments.
this.run = function() {
var rC = $("#rC");
var user = engine.currentUser();
rC.html("<img src='/img/loading.gif' alt='Loading...'/> <span id='reportProgress'>Loading games...</span>");
this.pendingOperations = 3;
this.games = [];
$("#runReport").enabled = false;
$.ajaxSetup({"error":(function(report) {
return function(event, XMLHttpRequest, ajaxOptions, thrownError) {
report.ajaxError(event, XMLHttpRequest, ajaxOptions, thrownError);
};
})(this)});
$.getJSON("/api/leagues", {"user":user}, (function(report) {
return function(leagues) {
report.addSeriesGroup(leagues);
};
})(this));
$.getJSON("/api/tournaments", {"user":user}, (function(report) {
return function(tournaments) {
report.addSeriesGroup(tournaments);
};
})(this));
$.getJSON("/api/practices", {"user":user}, (function(report) {
return function(practices) {
report.addSerieses(practices);
};
})(this));
};
// Retrieves the serieses (group of IDs) for a series group, such as a league or
// tournament.
this.addSeriesGroup = function(seriesGroups) {
var report = this;
if(seriesGroups) {
$.each(seriesGroups, function(index, seriesGroup) {
report.pendingOperations += 1;
$.getJSON("/api/seriesgroup", {"group":seriesGroup.key}, (function(report) {
return function(serieses) {
report.addSerieses(serieses);
};
})(report));
});
}
this.pendingOperations -= 1;
this.tryFinishReport();
};
// Retrieves the actual serieses for a series group. Takes a set of
// series IDs and retrieves each series.
this.addSerieses = function(serieses) {
var report = this;
if(serieses) {
$.each(serieses, function(index, series) {
report.pendingOperations += 1;
$.getJSON("/api/series", {"series":series.key}, (function(report) {
return function(series) {
report.addSeries(series);
};
})(report));
});
}
this.pendingOperations -= 1;
this.tryFinishReport();
};
// Adds the games for the series to the list of games
this.addSeries = function(series) {
var report = this;
if(series && series.games) {
$.each(series.games, function(index, game) {
report.games.push(game);
});
}
this.pendingOperations -= 1;
this.tryFinishReport();
};
// Checks to see if all pending requests have completed - if so, runs the
// report.
this.tryFinishReport = function() {
if(this.pendingOperations > 0) {
return;
}
var progress = $("#reportProgress");
progress.text("Performing calculations...");
setTimeout((function(report) {
return function() {
report.finishReport();
};
})(this), 1);
}
// Performs report calculations and displays them to the user.
this.finishReport = function() {
var rC = $("#rC");
//snip a page of calculations/table generation
rC.html(html);
$("#rC table").addClass("tablesorter").attr("cellspacing", "1").tablesorter({"sortList":[[3,1]]});
};
// Handles errors (by ignoring them)
this.ajaxError = function(event, XMLHttpRequest, ajaxOptions, thrownError) {
this.pendingOperations -= 1;
};
return true;
}
// A class to track the state of the various controls. The "series set" stuff
// is for future functionality.
function ReportingEngine() {
this.seriesSet = [];
this.reports = {};
this.getSeriesSet = function() {
return this.seriesSet;
};
this.clearSeriesSet = function() {
this.seriesSet = [];
};
this.addGame = function(series) {
this.seriesSet.push(series);
};
this.currentUser = function() {
return $("#userSelect").val();
};
this.currentReport = function() {
reportName = $("#reportSelect").val();
if(reportName) {
return this.reports[reportName];
}
return null;
};
}
// Sets the enablement of the run button based on the selections to the inputs
function updateRunButton() {
var report = engine.currentReport();
var user = engine.currentUser();
setRunButtonEnablement(report != null && user != null);
}
function setRunButtonEnablement(enabled) {
if(enabled) {
$("#runReport").removeAttr("disabled");
} else {
$("#runReport").attr("disabled", "disabled");
}
}
var engine = new ReportingEngine();
$(document).ready( function() {
loadUI();
});
function runReport() {
var report = engine.currentReport();
if(report == null) {
updateRunButton();
return;
}
report.run();
}
I am about to start adding new reports, some of which will operate on only a subset of user's games. I am going to be trying to use subclasses (prototype?), but if I can't figure out how to simplify some of this... I don't know how to finish that sentence. Help!
$.getJSON("/api/leagues", {"user":user}, (function(report) {
return function(leagues) {
report.addSeriesGroup(leagues);
};
})(this));
Can be written as:
var self = this;
$.getJSON("/api/leagues", {"user":user}, (function(leagues) {
self.addSeriesGroup(leagues);
});
The function-returning-function is more useful when you're inside a loop and want to bind to a variable that changes each time around the loop.
Provide "some" comments where necessary.
I'm going to be honest with you and say that I didn't read the whole thing. However, I think there is something about JavaScript you should know and that is that it has closures.
var x = 1;
$.ajax({
success: function () {
alert(x);
}
});
No matter how long time it takes for the AJAX request to complete, it will have access to x and will alert "1" once it succeeds.
Understand Closures. This takes some getting used to. (which, many will use, and is certainly the typical way of going about things, so it's good if you understand how that's happening)
This is a good thread to read to get a simple explanation of how to use them effectively.
You should use prototypes to define methods and do inheritance:
function Parent(x) {
this.x = x; /* Set an instance variable. Methods come later. */
}
/* Make Parent inherit from Object by assigning an
* instance of Object to Parent.prototype. This is
* very different from how you do inheritance in
* Java or C# !
*/
Parent.prototype = { /* Define a method in the parent class. */
foo: function () {
return 'parent ' + this.x; /* Use an instance variable. */
}
}
function Child(x) {
Parent.call(this, x) /* Call the parent implementation. */
}
/* Similar to how Parent inherits from Object; you
* assign an instance of the parent class (Parent) to
* the prototype attribute of the child constructor
* (Child).
*/
Child.prototype = new Parent();
/* Specialize the parent implementation. */
Child.prototype.foo = function() {
return Parent.prototype.foo.call(this) + ' child ' + this.x;
}
/* Define a method in Child that does not override
* something in Parent.
*/
Child.prototype.bar = function() {
return 'bar';
}
var p = new Parent(1);
alert(p.foo());
var ch = new Child(2);
alert(ch.foo());
alert(ch.bar());
I'm not familiar with jQuery, but I know the Prototype library (worst name choice ever) has some functionality that make it easier to work with inheritance.
Also, while coming up with the answer to this question, I found a nice page that goes into more detail on how to do OO right in JS, which you may want to look at.

Extracting nested function names from a JavaScript function

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.

Categories