ImageBoard Downloader

The original fullsize images downloader for gelbooru, rule34, yande.re, donmai, and sankakucomplex imageboards

Versión del día 30/10/2017. Echa un vistazo a la versión más reciente.

// ==UserScript==
// @name        ImageBoard Downloader
// @description The original fullsize images downloader for gelbooru, rule34, yande.re, donmai, and sankakucomplex imageboards
// @namespace   https://greasyfork.org/users/155308
// @include     https://gelbooru.com*
// @include     https://rule34.xxx*
// @include     https://yande.re*
// @include     *://*.donmai.us*
// @include     *://*.sankakucomplex.com*
// @version     0.0.8
// @grant       GM_xmlhttpRequest
// ==/UserScript==

/*
 0.0.8
	+ sankaku downloader
		- chan.sankakucomplex.com
		- idol.sankakucomplex.com
 0.0.7
	+ donmai downloader
		- safebooru.donmai.us
		- danbooru.donmai.us
		- sonohara.donmai.us
		- hijiribe.donmai.us
 0.0.6
	+ yande.re downloader
	+ download jpeg image on yande.re [false]
 0.0.5
	+ rule34 downloader
	+ add the imageboard name to the image name [true]
 0.0.3
	+ gelbooru downloader
	+ maximum tags in the image name [10]
	+ tags delimeter in the image name ['-']
*/
if( window.self !== window.top )
	return;
var RANDOM = '1681238';//Math.floor(Math.random()*1e6 + 1e6);
console.log('start..');
(function(){
	function blank(){}
	var clog = console.log;
	clog = blank;
	var userOptions = initOptions();
	var methodsObject = initMethodsObject();
	userOptions.set({
		'maxTagsInName': 10,
		'tagsDelim': '-',
		'addImgBrdName': true,// add the imageboard name to the image name
		'downloadJPEG': false,// download jpeg image on yande.re when exists
	});
	var imageBoard = initImageBoard();
	newCssClasses();
	
	//------------------------------------------------------------------------------------//
	//------------------------------------- SITE LIST ------------------------------------//
	function initSiteList()
	{
		var retVal = {
			get current(){return this.currentObj.val;},
			get gelbooru(){return this.data.gelbooru.val;},
			get rule34(){return this.data.rule34.val;},
			get yandere(){return this.data['yande.re'].val;},
			data: {
				'gelbooru': {
					val: null,
					regexp: /gelbooru/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#fff',
						width: '180px',
						background: '#0773fb',
						backgroundHover: '#fbb307',
						colorHover: '#fff',
					},
					init: function(){
						this.val = this.val || initGelbooruObject();
					},
					name: 'gelbooru',
				},
				'rule34': {
					val: null,
					regexp: /rule34/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#fff',
						width: '180px',
						background: '#84AE83',
						backgroundHover: '#A4CEA3',
						colorHover: '#fff',
					},
					init: function(){
						this.val = this.val || initRule34Object();
					},
					name: 'rule34',
				},
				'yande.re': {
					val: null,
					regexp: /yande\.re/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#ee8887',
						width: '180px',
						background: '#222',
						backgroundHover: '#444',
						colorHover: '#ee8887',
					},
					init: function(){
						this.val = this.val || initYandereObject();
					},
					name: 'yande.re',
				},
				'donmai': {
					val: null,
					regexp: /donmai/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#0073ff',
						width: '180px',
						background: '#f5f5ff',
						backgroundHover: '#f5f5ff',
						colorHover: '#80b9ff',
					},
					init: function(){
						this.val = this.val || initDonmaiObject();
					},
					name: 'donmai',
				},
				'sankaku': {
					val: null,
					regexp: /sankaku/,
					get needXHR(){return this.val.needXHR;},
					style: {
						color: '#ff761c',
						width: '180px',
						background: '',
						backgroundHover: '',
						colorHover: '#666',
					},
					init: function(){
						this.val = this.val || initSankakuObject();
					},
					name: 'sankaku',
				},
			},
			get: function( type, prop1, prop2 ){
				var obj = (type === undefined || type === null) ? this.currentObj : this.data[type];
				return nodeWalk.call( obj, prop1, prop2 );
			},
			style: function(type){
				return this.get( type, 'style' );
			},
			val: function(type){
				return this.get( type, 'val' );
			},
			needXHR: function(type){
				return this.get( type, 'needXHR' );
			},
			init: function(type){
				if( type === undefined || type === null )
					this.initCurrent();
				else if( this.data[type] )
					this.data[type].init();
			},
			getSiteType: function(url){
				url = url || window.location.href;
				for( var key in this.data )
				{
					if( this.data[key].regexp.test(url) )
						return key;
				}
				console.error("no site object found for this host");
				return null;
			},
			initCurrent: function(){
				if( !this.currentObj )
				{
					var type = this.getSiteType();
					if( !type )
						return;
					this.currentObj = this.data[type];
				}
				this.currentObj.init();
			},
		};
		retVal.init();
		clog("siteList.current: ", retVal.current);
		return retVal;
	}
	//------------------------------------- SITE LIST ------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ IMAGE BOARD -----------------------------------//
	function initImageBoard( d )
	{
		var imgBrdCl = initImageBoardClasses(d),
			imgBrdDt = initImageBoardDataset(d),
			siteList = initSiteList(),
			imgBrdDw = initImageBoardDownloader(d),
			imgBrdId = 'image-board-div-' + RANDOM;
		var retVal = {
			get siteList(){return siteList;},
			get imgBrdCl(){return imgBrdCl;},
			get imgBrdDt(){return imgBrdDt;},
			get imgBrdId(){return imgBrdId;},
			get imgBrdDw(){return imgBrdDw;},
			get images(){return this.data.images;},
			get downloader(){return this.data.downloader;},
			data: {
				'images': {
					list: null,
					init: function( doc, type ){
						clog("init");
						siteList.init(type);
						imgBrdDt.init(doc);
						imgBrdCl.init(doc);
						this.list = this.list || [];
						this.doc = doc || document;
						var siteObj = siteList.val(type),
							isPost = siteObj.isPost(),
							imgD;
						function addNewImage( list, img, isPost )
						{
							list.push({});
							var imgD = last(list);
							imgD.state = 'empty';
							imgD.index = list.length - 1;
							imgD.type = siteObj.name;
							if( isPost )
							{
								imgD.postId = siteObj.getPostId();
								imgD.postUrl = window.location.href;
								siteObj.setImageDataDoc(imgD);
							}else
								siteObj.setImageDataThumb( imgD, img );
							imgBrdDt.val( img, 'index', imgD.index);
							imgBrdCl.addClass( img, 'counted' );
							if( imgD.state === 'ready' )
							{
								siteObj.createDiv( imgBrdId, this.doc);
								imgBrdDw.init(imgBrdId, this.doc);
								setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw );
							}
							return imgD;
						}
						if( isPost )
						{
							var img = siteObj.getPostImage();
							if( img && !imgBrdCl.hasClass( img, 'counted') )
								addNewImage( this.list, img, true );
						}
						var thumbs = this.doc.querySelectorAll('img.preview');
						if( thumbs && thumbs.length === 0 )
							thumbs = this.doc.querySelectorAll('img[itemprop="thumbnailUrl"]');// donmai
						clog("thumbs.length: ", thumbs.length);
						for( var i = 0, len = thumbs.length, thumb; i < len; ++i )
						{
							thumb = thumbs[i];
							if( imgBrdCl.hasClass( thumb, 'counted' ) )
								continue;
							addNewImage( this.list, thumb );
						}
					},
					getEmpty: function(){
						var empty = [];
						for( var i = 0; i < this.list.length; ++i )
						{
							if( this.list[i].state === 'empty' )
								empty.push(i);
						}
						return empty;
					},
					fix: function()
					{
						var empty = this.getEmpty();
						for( var i = 0, idx, imgD; i < empty.length; ++i )
						{
							idx = empty[i];
							imgD = this.list[idx];
							imgD.state = 'busy';
							this.getImageData(imgD);
						}
					},
					getImageData: function(imgD)
					{
						if( siteList.needXHR(imgD.type) )
						{
							GM_xmlhttpRequest({
								url: imgD.postUrl,
								method: 'GET',
								context: {
									'index': imgD.index,
								},
								onload: xhrImageData,
							});
						}else{
							console.log("TODO :D");
							var siteObj = siteList.val(imgD.type);
							//siteObj.setImageDataFull(imgD);// TODO (yande.re, donmai)
						}
					},
				},
				'downloader': {
					init: function(doc, type){
						clog("downloader init..");
						siteList.init(type);
						var siteObj = siteList.val(type);
						siteObj.createDiv( imgBrdId, doc);
						imgBrdDw.init(imgBrdId, doc);
					},
					isActive: function(){
						return imgBrdDw && imgBrdDw.isActive() || false;
					},
					activate: function(doc){
						clog("activate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var thumbs = imgBrdCl.queryAll('counted');
						for( var i = 0, len = thumbs.length, thumb, a; i < len; ++i )
						{
							thumb = thumbs[i];
							a = thumb.parentNode;
							if( !imgBrdCl.hasClass(thumb, 'ready' ) )
								continue;
							else if( !imgBrdCl.hasClass( a, 'downloadAttach' ) )
							{
								a.addEventListener('click', handleDownloadEvent, false);
								imgBrdCl.addClass( a, 'downloadAttach' );
							}
							imgBrdCl.addClass( a, 'downloadActive' );
						}
						imgBrdDw.downloadOn();
					},
					deactivate: function(doc){
						clog("deactivate");
						doc = doc || document;
						imgBrdCl.init(doc);
						var activ = imgBrdCl.queryAll('downloadActive');
						clog("active.length: ", activ.length);
						for( var i = 0, len = activ.length; i < len; ++i )
							imgBrdCl.removeClass( activ[i], 'downloadActive' );
						imgBrdDw.downloadOff();
					},
					downloadAll: function(){
						imgBrdDw.downloadAll.click();// =)
					},
				},
			},
			init: function(doc){
				for( var key in this.data )
					this.data[key].init(doc);
			},
			fix: function(){
				this.data.images.fix();
			},
		};
		retVal.init(d);
		retVal.fix();
		return retVal;
	}
	//------------------------------------ IMAGE BOARD -----------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	function xhrImageData(xhr)
	{
		var imgD = imageBoard.images.list[xhr.context.index];
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText );
			console.error("index: ", xhr.context ? xhr.context.index : null);
			console.error("postUrl: ", this.url );
			if( imgD.state !== 'ready' )
				imgD.state = 'empty';
			return;
		}
		if( !imgD || imgD.state === 'ready' )
		{
			console.error("invalid context: ", imgD);
			return;
		}
		var siteObj = imageBoard.siteList.val(imgD.type);
		if( !siteObj )
		{
			console.error("invalid site type: ", imgD.type);
			return;
		}
		var doc = document.implementation.createHTMLDocument("");
		doc.documentElement.innerHTML = xhr.response;
		siteObj.setImageDataDoc(imgD, doc);
		clog("xhrImageData[" + imgD.index + "].state : " + imgD.state);
		if( imgD.state === 'ready' )
		{
			setReadyImage( imgD );
		}
	}
	function setReadyImage( imgD, imgBrdCl, imgBrdDt, imgBrdDw )
	{
		if( (!imgBrdCl || !imgBrdDt || !imgBrdDw) && imageBoard )
		{
			imgBrdCl = imageBoard.imgBrdCl;
			imgBrdDt = imageBoard.imgBrdDt;
			imgBrdDw = imageBoard.imgBrdDw;
		}
		var thumb = imgBrdDt.query('index', imgD.index + '');
		imgBrdCl.addClass( thumb, 'ready' );
		imgBrdDt.val( thumb, 'source', imgD.source );
		if( imgD.bytes ) imgBrdDt.val( thumb, 'bytes', imgD.bytes );
		imgBrdDw.total += 1;
		clog("name: " + imgD.name);
		if( imageBoard && imageBoard.downloader.isActive() )
		{
			imageBoard.downloader.activate();// TODO: activate only current image
		}
	}
	//----------------------------------- XRH IMAGE DATA ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- GELBOORU ------------------------------------//
	function initGelbooruObject()
	{
		var retVal = {
			data: {
				name: 'gelbooru',
				hostname: 'gelbooru.com',
				imageDir: '/images',
				imageUri: 'simg3.gelbooru.com//images/',// TODO
				//imageSubDomain: 'simg3',
				postDivInsertionPlace: 'h4 > a*showCommentBox',
				divInsertionPlace: '.contain-push > div',
			},
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get needXHR(){return true;},
			methodsMap: {
				// href
				isPost: 'booru',
				getPostId: 'booru',
				getPostUrl: 'booru',
				// thumb
				setImageDataThumb: 'booru',
				// post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// button insertion div
				getPostDivInsertionPlace: 'booru',
				getDivInsertionPlace: 'booru',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			}
		};
		retVal.init();
		return retVal;
	}
	//-------------------------------------- GELBOORU ------------------------------------//
	//------------------------------------------------------------------------------------//
	//--------------------------------------- RULE34 -------------------------------------//
	function initRule34Object()
	{
		var retVal = {
			get needXHR(){return true;},
			data: {
				name: 'rule34',
				hostname: 'rule34.xxx',
				imageDir: '/images',
				imageUrl: 'img.rule34.xxx//images/',
				//imageSubDomain: 'img',
				postDivInsertionPlace: 'h4 > a',
				divInsertionPlace: 'div#top',
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			
			methodsMap: {
				// href
				isPost: 'booru',
				getPostId: 'booru',
				getPostUrl: 'booru',
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				//
				getPostDivInsertionPlace: 'booru',
				getDivInsertionPlace: 'booru',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			}
		};
		retVal.init();
		return retVal;
	}
	//--------------------------------------- RULE34 -------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- YANDE.RE -------------------------------------//
	function initYandereObject()
	{
		var retVal = {
			//get needXHR(){return false;},
			get needXHR(){return true;},
			data: {
				name: 'yande.re',
				hostname: 'yande.re',
				imageDir: 'image',
				//imageUri: 'files.yande.re/image/',
				postDivInsertionPlace: 'h4 > a',
				//postDivInsertionPlace: 'a.js-posts-show-comments-tab',
				divInsertionPlace: '#post-list-posts',
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			methodsMap: {
				// href methods
				isPost: 'yande.re',// if getPostId success then true else false
				getPostId: 'yande.re',// get post id from href
				getPostUrl: 'yande.re',// get post url by postId
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// methods of image data getting from image post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// create place for buttons insertion
				getPostDivInsertionPlace: 'yande.re',
				getDivInsertionPlace: 'yande.re',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			init: function(){
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			},
		};
		retVal.init();
		return retVal;
	}
	//------------------------------------- YANDE.RE -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- DONMAI --------------------------------------//
	function initDonmaiObject()
	{
		var retVal = {
			//get needXHR(){return false;},
			get needXHR(){return true;},
			data: {
				name: 'donmai',
				get hostname(){return this.sub_domains[this.sub_index] + '.donmai.us';},
				imageDir: 'data',
				postDivInsertionPlace: '#post-sections > li > a',
				divInsertionPlace: '#posts',
				get sub_domains(){return this.sub.domains;},
				get sub_index(){return this.sub.index;},
				sub: {
					domains: ['safebooru', 'danbooru', 'sonohara', 'hijiribe'],
					index: null,
					init: function( sub_dom ){
						var p;
						if( sub_dom !== undefined )
						{
							p = this.domains.indexOf(sub_dom);
							if( p != -1 )
								return (this.index = p);
						}
						var host = window.location.hostname;
						p = host.indexOf('donmai.us');
						if( p == -1 )
							return -1;
						for( var i = 0; i < this.domains.length; ++i )
						{
							p = host.indexOf(this.domains[i]);
							if( p != -1 )
								return (this.index = p);
						}
						return -1;
					},
				},
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			methodsMap: {
				// href methods
				isPost: 'donmai',// if getPostId success then true else false
				getPostId: 'donmai',// get post id from href
				getPostUrl: 'donmai',// get post url by postId
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// methods of image data getting from image post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// create place for buttons insertion
				getPostDivInsertionPlace: 'yande.re',
				getDivInsertionPlace: 'yande.re',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			sub_init: function(domain){
				return this.data.sub.init(domain);
			},
			init: function(){
				this.sub_init();
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			},
		};
		retVal.init();
		return retVal;
	}
	//-------------------------------------- DONMAI --------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- SANKAKU -------------------------------------//
	function initSankakuObject()
	{
		var retVal = {
			get needXHR(){return true;},
			data: {
				name: 'sankaku',
				get hostname(){return this.current_sub_domain + '.sankakucomplex.com';},
				get imageHostname(){return this.current_sub_domain[0] + 's.sankakucomplex.com';},
				imageDir: 'data',
				postDivInsertionPlace: '#post-content',
				divInsertionPlace: '#content',
				get sub_domains(){return this.sub.domains;},
				get sub_index(){return this.sub.index;},
				get current_sub_domain(){return this.sub_domains[this.sub_index] || 'c';},
				sub: {
					domains: ['chan', 'idol'],
					index: null,
					init: function( sub_dom ){
						var p;
						if( sub_dom !== undefined )
						{
							p = this.domains.indexOf(sub_dom);
							if( p != -1 )
								return (this.index = p);
						}
						var host = window.location.hostname;
						p = host.indexOf('sankakucomplex.com');
						if( p == -1 )
							return (this.index = -1);
						for( var i = 0; i < this.domains.length; ++i )
						{
							p = host.indexOf(this.domains[i]);
							if( p != -1 )
								return (this.index = p);
						}
						return (this.index = -1);
					},
				},
			},
			get name(){ return this.data.name; },
			get hostname(){return this.data.hostname; },
			get imageDir(){return this.data.imageDir; },
			get postDivInsertionPlace(){return this.data.postDivInsertionPlace;},
			get divInsertionPlace(){return this.data.divInsertionPlace;},
			methodsMap: {
				// href methods
				isPost: 'yande.re',// if getPostId success then true else false
				getPostId: 'yande.re',// get post id from href
				getPostUrl: 'yande.re',// get post url by postId
				// method of thumbnail data grabbing (thumbSource, )
				setImageDataThumb: 'booru',
				// methods of image data getting from image post page
				getPostImage: 'booru',
				setImageOriginalResolution: 'booru',
				setImageDataSize: 'booru',
				setImageDataSourceLowres: 'booru',
				setImageDataSourceHighres: 'booru',
				setImageDataTags: 'booru',
				setImageDataName: 'general',
				setImageDataExtension: 'general',
				setImageDataBytes: null,
				setImageDataDoc: 'general',
				// create place for buttons insertion
				getPostDivInsertionPlace: 'sankaku',
				getDivInsertionPlace: 'sankaku',
				keyboardDiv: 'booru',
				createDiv: 'booru',
			},
			sub_init: function(domain){
				return this.data.sub.init(domain);
			},
			init: function(){
				this.sub_init();
				var name, type;
				for( name in this.methodsMap )
				{
					type = this.methodsMap[name];
					if( type )
						this[name] = methodsObject.method(type, name);
				}
			},
		};
		retVal.init();
		return retVal;
	}
	//-------------------------------------- SANKAKU -------------------------------------//
	//------------------------------------------------------------------------------------//
	//----------------------------------- METHODS OBJECT ---------------------------------//
	function initMethodsObject()
	{
		var retVal = {
			data: {
				'booru': {
					val: null,
					init: function(){
						this.val = this.val || getBooruMethodsObject();
					},
				},
				'yande.re': {
					val: null,
					init: function(){
						this.val = this.val || getYandereMethodsObject();
					},
				},
				'general': {
					val: null,
					init: function(){
						this.val = this.val || getGeneralMethodsObject();
					},
				},
				'donmai': {
					val: null,
					init: function(){
						this.val = this.val || getDonmaiMethodsObject();
					},
				},
				'sankaku': {
					val: null,
					init: function(){
						this.val = this.val || getSankakuMethodsObject();
					},
				},
			},
			init: function(){
				for( var type in this.data )
					this.data[type].init();
			},
			method: function( type, name ){
				if( this.data[type] )
				{
					if( name )
						return this.data[type].val[name];
					return this.data[type].val;
				}
				return null;
			},
		};
		retVal.init();
		return retVal;
	}
	//----------------------------------- METHODS OBJECT ---------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------- BOORU METHODS OBJECT ------------------------------//
	function getBooruMethodsObject()
	{
		var retVal = {
			getPostImage: function(doc){
				return (doc || document).querySelector('#image');
			},
			setImageDataThumb: function( imgD, thumb ){
				if( thumb && imgD )
				{
					if( thumb.dataset && thumb.dataset.original )
						imgD.thumbSource = thumb.dataset.original;
					else
						imgD.thumbSource = thumb.src;
					imgD.postUrl = thumb.parentNode.href;
					if( thumb.parentNode.id )
						imgD.postId = thumb.parentNode.id.slice(1);
					else
						imgD.postId = this.getPostId( imgD.postUrl );
				}
			},
			setImageDataSourceLowres: function( imgD, doc ){
				var img = this.getPostImage(doc);
				if( img )
					imgD.lowresSource = img.src;
				else
					return 1;
				return 0;
			},
			setImageOriginalResolution: function( imgD, img ){
				if( !img )
					return false;
				var width, height;
				width = img.getAttribute('large_width');
				height = img.getAttribute('large_height');
				if( !width || !height )
				{
					width = img.getAttribute('data-original-width');
					height = img.getAttribute('data-original-height');
				}
				if( !width || !height )
				{
					// sankaku
					width = img.getAttribute('orig_width');
					height = img.getAttribute('orig_height');
				}
				if( width && height )
				{
					imgD.width = width;
					imgD.height = height;
					return true;
				}
				return false;
			},
			setImageDataSize: function( imgD, doc ){
				doc = doc || document;
				var img = this.getPostImage(doc), res;
				if( this.setImageOriginalResolution )
					res = this.setImageOriginalResolution( imgD, img );
				if( res )
					return;
				var lis = doc.querySelectorAll('li'), i, li, len = lis.length;
				for( i = 0; i < len; ++i )
				{
					li = lis[i];
					if( li.innerHTML.indexOf('Size:') != -1 )
						break;
				}
				var match = li.innerHTML.match(/(\d+)x(\d+)/);
				if( i < len && match )
				{
					imgD.width = match[1];
					imgD.height = match[2];
				}else
					console.error("[setImageDataSize] can't find image size (width x height)");
			},
			setImageDataSourceHighres: function( imgD, doc ){
				doc = doc || document;
				var link = doc.querySelector('li > a[href*="' + (this.imageHostname || this.hostname) + '/' + 
					this.imageDir + '/"]');
				if( !link && this.name === 'donmai' )// same origin for donmai
					link = doc.querySelector('li > a[href*="/' + this.imageDir + '/"]');
				//var link = doc.querySelector('li > a[href*="' + this.imageUri + '"]');
				if( link )
					imgD.source = link.href;
				else if( imgD.lowresSource )
					imgD.source = imgD.lowresSource;
				else{
					console.error("[setImageDataSourceHighres] no image source found");
					return 1;
				}
				// + yande.re:
				var jpeg = doc.querySelector('li > a[href*="' + this.hostname + '/jpeg/"]');
				if( jpeg )
					imgD.jpegSource = jpeg.href;
				return 0;
			},
			setImageDataTags: function( imgD, doc ){
				doc = doc || document;
				var getTagName = function( tagElm, fl)
				{
					if( fl )
						return tagElm.querySelectorAll('a')[0].innerText.trim().replace(/\s+/g, '_');
					return last(tagElm.querySelectorAll('a')).innerText.trim().replace(/\s+/g, '_');
				};
				var tagsType = ['character', 'copyright', 'artist', 'circle', 'medium', 'general', 'faults'],
					tagsMap = ['4', '3', '1', '-1', '-1', '0', '-1'];// donmai
				imgD.tags = imgD.tags || [];
				imgD.tags.length = 0;
				for( var i = 0, _fl = (this.name === 'sankaku'), tags; i < tagsType.length; ++i )
				{
					tags = doc.querySelectorAll('li.tag-type-' + tagsType[i] );
					if( tags.length === 0 )
						tags = doc.querySelectorAll('li.category-' + tagsMap[i]);// donmai
					for( var k = 0, tagEl; tags && k < tags.length; ++k )
						imgD.tags.push( getTagName(tags[k], _fl) );
				}
			},
			isPost: function( url ){
				url = url || window.location.href;
				if( this.getPostId(url) )
					return true;
				return false;
			},
			getPostId: function( postUrl ){
				postUrl = postUrl || window.location.href;
				var srch = getLocation( postUrl, 'search' ),
					keys = getSearchObject( srch );
				if( keys.s === 'view' && keys.page === 'post' )
					return keys.id;
				else
					return null;
			},
			getPostUrl: function( postId ){
				return 'https://' + this.hostname + '/index.php?page=post&s=view&id=' + postId;
			},
			getPostDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector( this.postDivInsertionPlace );
				if( insertPlace )
					return insertPlace.parentNode;
				return null;
			},
			getDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector( this.divInsertionPlace );
				if( insertPlace )
					return insertPlace.nextSibling;
				return null;
			},
			createDiv: function(id, doc){
				doc = doc || document;
				var div = doc.querySelector('#' + id);
				if( div )
					return div;
				div = document.createElement('div');
				var insertPlace;
				if( this.isPost() )
					insertPlace = this.getPostDivInsertionPlace(doc);
				else
					insertPlace = this.getDivInsertionPlace(doc);
				if( !insertPlace )
					return null;
				div.setAttribute('id', id);
				div = insertPlace.parentNode.insertBefore( div, insertPlace);
				if( typeof this.keyboardDiv === 'function' )
					this.keyboardDiv( id, doc );
				return div;
			},
			keyboardDiv: function( id, doc ){
				/*
				clog("keyboard init..");
				doc = doc || document;
				var kbId = 'keyboard-div-' + RANDOM,
					div = doc.querySelector('#' + kbId);
				if( div )
					return div;
				var insPlace = doc.querySelector('#' + id);
				if( !insPlace )
				{
					console.error("can't create keyboard div");
					return;
				}
				div = document.createElement('div');
				//div = document.createElement('button');
				div.setAttribute('id', kbId);
				div.setAttribute('class', 'keyboar-inactive');
				div.innerHTML = '<button class="keyboard-inactive">Keyboard</button>';
				//div.innerHTML = 'Keyboard';
				div = insPlace.appendChild(div);
				div.addEventListener('click', handleKeyboardBtnEvent, false);
				clog("keyboard: ", div);
				return div;
				*/
			},
		};
		return retVal;
	}
	/*
	function handleKeyboardBtnEvent(event)
	{
		var t = event.target;
		if( t.tagName !== 'BUTTON' )
			return;
		else if( hasClass( t, 'keyboard-inactive') )
		{
			toggleClass( t, 'keyboard-active', 'keyboard-inactive');
			activateKeyboard();
		}
		else if( hasClass( t, 'keyboard-active' ) )
		{
			toggleClass( t, 'keyboard-inactive', 'keyboard-active');
			deactivateKeyboard();
		}
	}
	*/
	//-------------------------------- BOORU METHODS OBJECT ------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------- GENERAL METHODS OBJECT -----------------------------//
	function getGeneralMethodsObject()
	{
		var retVal = {
			setImageDataDoc: function( imgD, doc ){
				if( !imgD || imgD.state === 'ready' )
					return 1;
				doc = doc || document;
				// size
				this.setImageDataSize( imgD, doc );
				// lowres
				var errN = this.setImageDataSourceLowres( imgD, doc );
				// highres
				errN += this.setImageDataSourceHighres( imgD, doc );
				if( errN > 1 )
					return errN;
				if( !imgD.lowresSource )
				{
					imgD.lowresSource = imgD.source;
					//imgD.lowresSource = (imgD.jpegSource || imgD.source);
				}
				// tags
				this.setImageDataTags( imgD, doc );
				// name
				this.setImageDataName( imgD );
				// extension
				this.setImageDataExtension( imgD );
				imgD.state = 'ready';
				//clog("imgD[" + imgD.index + "]: ", imgD.source, imgD.state);
				return 0;
			},
			setImageDataName: function( imgD ){
				var tagsLen = imgD.tags.length,
					uLen = userOptions.val('maxTagsInName'),
					tagsDelim = userOptions.val('tagsDelim');
				imgD.name = '';
				for( var i = 0; i < tagsLen && i < uLen; ++i )
					imgD.name += imgD.tags[i] + tagsDelim;
				if( userOptions.val('addImgBrdName') )
					imgD.name += this.name + tagsDelim;
				imgD.name += imgD.postId;
			},
			setImageDataExtension: function( imgD ){
				var pathname = getLocation( imgD.source, 'pathname' );
				imgD.extension = pathname.match(/\.([a-z\d]+)$/)[1];
				if( imgD.jpegSource )
				{
					pathname = getLocation( imgD.jpegSource, 'pathname' );
					imgD.jpegExtension = pathname.match(/\.([a-z\d]+)$/)[1];
				}
			},
		};
		return retVal;
	}
	//------------------------------- GENERAL METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------- YANDERE METHODS OBJECT -----------------------------//
	function getYandereMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.pathname || window.location.href;
				return /\/post\/show\/\d+/.test(url);
			},
			getPostId: function(url){
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/\d+/)[0];
				return null;
			},
			getPostUrl: function(postId){
				return 'https://' + this.hostname + '/post/show/' + postId;
			},
			getPostDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.postDivInsertionPlace);
				if( insertPlace )
					return insertPlace.parentNode.parentNode;
				return null;
			},
			getDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.divInsertionPlace);
				if( insertPlace )
					return insertPlace;
				return null;
			},
		};
		return retVal;
	}
	//------------------------------- YANDERE METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------- DONMAI METHODS OBJECT -----------------------------//
	function getDonmaiMethodsObject()
	{
		var retVal = {
			isPost: function(url){
				url = url || window.location.href;
				return /\/posts\/\d+/.test(url);
			},
			getPostId: function(url){
				if( this.isPost(url) )
					return getLocation(url, 'pathname').match(/(\/posts\/)?(\d+)?/)[2];
				return null;
			},
			getPostUrl: function(postId){
				return 'https://' + this.hostname + '/posts/' + postId;
			},
		};
		return retVal;
	}
	//-------------------------------- DONMAI METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------- SANKAKU METHODS OBJECT -----------------------------//
	function getSankakuMethodsObject()
	{
		var retVal = {
			getPostDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.postDivInsertionPlace);
				if( insertPlace )
					return insertPlace.nextSibling;
				return null;
			},
			// TODO
			getDivInsertionPlace: function(doc){
				doc = doc || document;
				var insertPlace = doc.querySelector(this.divInsertionPlace);
				if( insertPlace )
					return insertPlace.firstChild;
				return null;
			},
		};
		return retVal;
	}
	//------------------------------- SANKAKU METHODS OBJECT -----------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- DATASET -------------------------------------//
	function initImageBoardDataset(d)
	{
		var retVal = {
			data: {
				source: 'data-image-board-source',
				index: 'data-image-board-index',
				extension: 'data-image-board-extension',
				bytes: 'data-image-board-bytes',
			},
			val: function(elm, propName, v){
				if( this.data[propName] )
				{
					if( v !== undefined )
						elm.setAttribute(this.data[propName], v);
					else
						return elm.getAttribute(this.data[propName]);
				}
				return null;
			},
			init: function(doc){
				this.doc = doc || document;
			},
			getSelector: function(propName, v){
				var sel = this.data[propName];
				if( sel )
				{
					if( v !== undefined )
					{
						var pos = v.indexOf('=');
						if( pos > -1 && pos < 2 )
							sel += v;
						else
							sel += '="' + v + '"';
					}
					return '[' + sel + ']';
				}
				return null;
			},
			query: function(propName, v){
				var sel = this.getSelector(propName, v);
				if( sel )
					return this.doc.querySelector(sel);
				return null;
			},
			queryAll: function(propName, v){
				var sel = this.getSelector(propName, v);
				if( sel )
					return this.doc.querySelectorAll(sel);
				return null;
			},
		};
		retVal.init(d);
		return retVal;
	}
	//-------------------------------------- DATASET -------------------------------------//
	//------------------------------------------------------------------------------------//
	//-------------------------------------- CLASSES -------------------------------------//
	function initImageBoardClasses(d)
	{
		var retVal = {
			get counted(){return this.data.counted;},
			get viewActive(){return this.data.viewActive;},
			get viewAttach(){return this.data.viewAttach;},
			get ready(){return this.data.ready;},
			get downloaded(){return this.data.downloaded;},
			get downloadAttach(){ return this.data.downloadAttach;},
			get downloadActive(){ return this.data.downloadActive;},
			data: {
				counted: 'image-board-counted',
				viewAttach: 'image-board-attach-view-event',
				viewActive: 'image-board-active-for-view',
				ready: 'image-board-has-original-source',
				downloaded: 'image-board-downloaded-original',
				downloadAttach: 'image-board-attach-download-event',
				downloadActive: 'image-board-active-for-download',
			},
			hasClass: function(elm, propName){
				if( this.data[propName] )
					return hasClass(elm, this.data[propName]);
				return false;
			},
			addClass: function(elm, propName){
				if( this.data[propName] )
					addClass(elm, this.data[propName]);
			},
			removeClass: function(elm, propName){
				if( this.data[propName] )
					removeClass(elm, this.data[propName]);
			},
			toggleClass: function(elm, newPropName, oldPropName){
				if( oldPropName && !this.data[oldPropName] )
					return;
				else if( !newPropName || !this.data[newPropName] )
					return;
				toggleClass( elm, this.data[newPropName], this.data[oldPropName] );
			},
			queryAll: function(propName){
				if( this.data[propName] )
					return this.doc.querySelectorAll('.' + this.data[propName]);
				return null;
			},
			init: function(doc){this.doc = doc || document;},
		};
		retVal.init(d);
		return retVal;
	}
	//-------------------------------------- CLASSES -------------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------- DOWNLOADER -----------------------------------//
	function initImageBoardDownloader(d)
	{
		var iter = {
			total : 0,
			done: 0,
		};
		var retVal = {
			get total(){return iter.total;},
			set total(n){
				iter.total = n;
				this.downloadAllHtml(iter.total, iter.done);
			},
			get done(){return iter.done;},
			set done(n){
				iter.done = n;
				this.downloadAllHtml(iter.total, iter.done);
			},
			id: {
				downloader: 'image-board-downloader-' + RANDOM,
				downloadAll: 'image-board-download-all-' + RANDOM,
				downloadSwitch: 'image-board-download-switch-' + RANDOM,
			},
			'class': {
				on: 'image-board-downloader-on',
				off: 'image-board-downloader-off',
				active: 'image-board-downloader-active',
			},
			init: function(id, doc){
				doc = doc || document;
				clog("imgBrdDw.init, doc: ", doc);
				var div = doc.querySelector('div#' + id);
				clog("div: ", div, id);
				if( !div )
				{
					console.error("[initImageBoardDownloader] can't init div");
					return;
				}
				this.val = div.querySelector('#' + this.id.downloader );
				if( !this.val )
				{
					this.val = document.createElement('div');
					this.val.setAttribute('id', this.id.downloader);
					var html = '' +
					'<button id="' + this.id.downloadSwitch + '" class="' + this.class.off + '" ' +
					'title="Press \'Shift+D\' to switch download mode on/off">Download Mode</button>' +
					'<button id="' + this.id.downloadAll + '" class="">Download All (0)</button>' +
					'';
					this.val.innerHTML = html;
					this.val = div.appendChild(this.val);
				}
				this.downloadAll = this.val.querySelector('#' + this.id.downloadAll);
				if( !this.downloadAll.classList.contains(this.class.active) )
				{
					this.downloadAll.addEventListener('click', handleDownloadAllEvent, false);
					this.downloadAll.classList.add(this.class.active);
				}
				this.downloadSwitch = this.val.querySelector('#' + this.id.downloadSwitch);
				if( !this.downloadSwitch.classList.contains(this.class.active) )
				{
					activateKeyboard();
					this.downloadSwitch.addEventListener('click', handleDownloadSwitchEvent, false);
					this.downloadSwitch.classList.add(this.class.active);
				}
				return this.val;
			},
			downloadAllHtml: function( total, loaded ){
				if( !this.val )
					return;
				this.downloadAll.innerHTML = '' +
				'Download All (' + (loaded ? loaded + ' / ': '') + (total ? total : 0) + ')';
			},
			downloadOn: function(){
				toggleClass( this.downloadSwitch, this.class.on, this.class.off );
			},
			downloadOff: function(){
				toggleClass( this.downloadSwitch, this.class.off, this.class.on );
			},
			isActive: function(){
				if( this.downloadSwitch )
					return this.downloadSwitch.classList.contains(this.class.on);
				return false;
			},
		};
		return retVal;
	}
	//------------------------------------- DOWNLOADER -----------------------------------//
	//------------------------------------------------------------------------------------//
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	function handleDownloadEvent(event)
	{
		if( !imageBoard.imgBrdCl.hasClass( this, 'downloadActive' ) )
			return;
		event.preventDefault();
		var thumb = event.target,
			index = imageBoard.imgBrdDt.val( thumb, 'index' ),
			imgD = imageBoard.images.list[index];
		downloadFile( imgD );
	}
	function handleDownloadAllEvent(event)
	{
		var list = imageBoard.images.list;
		for( var i = 0, len = list.length, imgD; i < len; ++i )
		{
			imgD = list[i];
			downloadFile( imgD );
		}
	}
	function fileDownloader( name, resource )
	{
		var a = document.createElement('a'),
			body = document.body || document.getElementsByTagName('body')[0];
		a.setAttribute('download', name);
		a.href = resource;
		body.appendChild(a);
		a.click();
		body.removeChild(a);
	}
	function downloadFile( imgD )
	{
		if( imgD.state !== 'ready' || imgD.downloadState === 'downloaded' || imgD.downloadState === 'inProgress' )
			return;
		imgD.downloadState = 'inProgress';
		var hostname = getLocation(imgD.source, 'hostname'), source;
		if( userOptions.val('downloadJPEG') && imgD.jpegSource )
			source = imgD.jpegSource;
		else
			source = imgD.source;
		if( hostname === window.location.hostname )
		{
			imageBoardDownloader( imgD, source );
			return;
		}
		GM_xmlhttpRequest({
			url: source,
			method: 'GET',
			context: {
				'index': imgD.index,
			},
			responseType: 'blob',
			onload: blibBlobDownloader,
		});
	}
	function blibBlobDownloader( xhr )
	{
		if( xhr.status !== 200 )
		{
			console.error("xhr.status: ", xhr.status, xhr.statusText);
			console.erorr("url: " + this.url);
			return;
		}
		var wndURL = window.webkitURL || window.URL,
			resource = wndURL.createObjectURL(xhr.response),
			imgD = imageBoard.images.list[xhr.context.index];
		imageBoardDownloader( imgD, resource );
		wndURL.revokeObjectURL( resource );
	}
	function imageBoardDownloader( imgD, resource )
	{
		var name = imgD.name + '.' + imgD.extension;
		fileDownloader( name, resource );
		var thumb = imageBoard.imgBrdDt.query('index', imgD.index + '');
		imageBoard.imgBrdCl.addClass( thumb, 'downloaded' );
		imgD.downloadState = 'downloaded';
		imageBoard.imgBrdDw.done += 1;
	}
	function handleDownloadSwitchEvent(event)
	{
		if( imageBoard.imgBrdDw.isActive() )
			imageBoard.downloader.deactivate();
		else
			imageBoard.downloader.activate();
	}
	function activateKeyboard()
	{
		window.addEventListener('keydown', handleKeyboardEvent, false);
	}
	function deactivateKeyboard()
	{
		window.removeEventListener('keydown', handleKeyboardEvent, false);
	}
	function handleKeyboardEvent(event)
	{
		var charCode = event.keyCode || event.which,
			str = String.fromCharCode(charCode).toLowerCase();
		if( !event.shiftKey || event.ctrlKey || event.altKey )
			return;
		else if( str === 'd' )
		{
			handleDownloadSwitchEvent();
		}
		/*
		else if( str === 'v' )
		{
			handleViewerSwitchEvent();// dummy function
		}
		else if( str === 's' )
		{
			if( imageBoard )
			{
				imageBoard.init();
				imageBoard.fix();
			}
		}
		*/
	}
	//------------------------------------ DOWNLOADER-2 ----------------------------------//
	//------------------------------------------------------------------------------------//
	function handleViewerSwitchEvent(event){}// TODO
	//------------------------------------------------------------------------------------//
	//------------------------------------ USER OPTIONS ----------------------------------//
	function initOptions()
	{
		function _setDef(){this.val = this.def;}
		var retVal = {
			data: {
				'maxTagsInName': {
					val: null,
					def: 10,
					setDef: _setDef,
				},
				'tagsDelim': {
					val: null,
					def: '-',
					setDef: _setDef,
				},
				'addImgBrdName': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				'downloadJPEG': {
					val: null,
					def: false,
					setDef: _setDef,
				},
				/*
				'autoRunSource': {
					val: null,
					def: true,
					setDef: _setDef,
				},
				'autoRunDownloadOn': {
					val: null,
					def: false,
					setDef: _setDef,
				},
				*/
			},
			val: function( opt, v )
			{
				if( this.data[opt] )
				{
					if( v === undefined )
						return this.data[opt].val;
					else
						this.data[opt].val = v;
				}else
					return null;
			},
			setDefs: function(){
				for( var key in this.data )
					this.data[key].setDef();
			},
			set: function(obj){
				for( var key in obj )
					this.val(key, obj[key]);
			},
		};
		retVal.setDefs();
		return retVal;
	}
	//------------------------------------ USER OPTIONS ----------------------------------//
	//------------------------------------------------------------------------------------//
	function newCssClasses()
	{
		addCssClass(`
			#image-board-downloader-${RANDOM} {
				position: relative;
				//float: right;
				text-align: right;
				top: 2px;
				right: 10px;
				bottom: 2px;
				//display: inline-block;
			}
			#keyboard-div-${RANDOM} {
				position: relative;
				display: inline-block;
			}
			#image-board-div-${RANDOM} button {
				margin: 3px 10px;
				color: ${imageBoard.siteList.style().color};
				font-weight: bold;
				width: 180px;
				border: 0px;
				padding: 5px;
				background: ${imageBoard.siteList.style().background};
			}
			#image-board-div-${RANDOM} button:hover {
				background: ${imageBoard.siteList.style().backgroundHover};
				color: ${imageBoard.siteList.style().colorHover};
			}
			.keyboard-inactive::after,
			.image-board-downloader-off::after {
				content: " [off]";
			}
			.keyboard-active::after,
			.image-board-downloader-on::after {
				content: " [on]";
			}
			img.image-board-has-original-source {
				border-bottom: 5px solid green !important;
			}
			.image-board-active-for-download {
				cursor: default;
			}
		`);
	}
	function addCssClass(cssClass)
	{
		var style = document.createElement('style'),
			head = document.querySelector('head');
		style.type = 'text/css';
		if( style.styleSheets )
			style.styleSheets.cssText = cssClass;
		else
			style.appendChild(document.createTextNode(cssClass));
		return head.appendChild(style);
	}
	function addClass( elm, name )
	{
		if( elm && name )
			elm.classList.add(name);
	}
	function removeClass( elm, name )
	{
		if( elm && name )
			elm.classList.remove(name);
	}
	function hasClass( elm, name )
	{
		if( elm && name )
			return elm.classList.contains(name);
		return false;
	}
	function toggleClass( elm, newClass, oldClass )
	{
		if( !elm || !newClass )
			return;
		if( oldClass )
		{
			elm.classList.remove(oldClass);
			elm.classList.add(newClass);
		}
		else if( elm.classList.contains(newClass) )
			elm.classList.remove(newClass);
		else
			elm.classList.add(newClass);
	}
	function getLocation( url, attr )
	{
		if( !attr )
			return null;
		url = url || window.location.href;
		this.link = this.link || document.createElement('a');
		this.link.href = url;
		return this.link[attr];
	}
	function getSearchObject( search )
	{
		var keys = {}
		if( search )
		{
			search = search.replace(/^\?/, '');
			search.split('&').forEach(function(item){
				item = item.split('=');
				keys[item[0]] = item[1];
			});
		}
		return keys;
	}
	function last( arr )
	{
		if( arr && arr.length > 0 )
			return arr[arr.length-1];
		return null;
	}
	function nodeWalk()
	{
		var len = arguments.length, obj = this, i, arg;
		for( i = 0; i < len; ++i )
		{
			arg = arguments[i];
			if( arg === undefined )
				return obj;
			else if( obj[arg] === undefined )
				return null;
			obj = obj[arg];
		}
		return obj;
	}
})();