diff --git a/classes/ComicPressAdmin.inc b/classes/ComicPressAdmin.inc index 9b7d8b4..c2081c2 100644 --- a/classes/ComicPressAdmin.inc +++ b/classes/ComicPressAdmin.inc @@ -80,8 +80,8 @@ class ComicPressAdmin { function admin_menu() { global $plugin_page, $pagenow, $post; - add_theme_page(__("ComicPress", 'comicpress'), __('ComicPress', 'comicpress'), 'edit_themes', 'comicpress/render_admin', array(&$this, 'render_admin')); - add_theme_page(__("ComicPress Documentation", 'comicpress'), __('ComicPress Docs', 'comicpress'), 'edit_themes', 'comicpress/comicpress_docs', array(&$this, 'render_documentation')); + add_menu_page(__("ComicPress Core", 'comicpress'), __('ComicPress', 'comicpress'), 'edit_themes', 'comicpress/render_admin', array(&$this, 'render_admin')); + add_submenu_page('comicpress/render_admin', __("ComicPress Core Documentation", 'comicpress'), __('Documentation', 'comicpress'), 'edit_themes', 'comicpress/comicpress_docs', array(&$this, 'render_documentation')); add_action('admin_enqueue_scripts', array(&$this, 'admin_enqueue_scripts')); @@ -89,6 +89,8 @@ class ComicPressAdmin { case 'comicpress/render_admin': wp_enqueue_style('cp-admin', plugin_dir_url(dirname(__FILE__)) . '/css/cp-admin.css'); wp_enqueue_script('cp-admin', plugin_dir_url(dirname(__FILE__)) . '/js/Storyline.js', array('jquery', 'jquery-ui-sortable', 'jquery-ui-tabs')); + wp_enqueue_script('json', plugin_dir_url(dirname(__FILE__)) . '/js/json2.js'); + wp_enqueue_script('jquery-ui-slider', plugin_dir_url(dirname(__FILE__)) . '/js/jquery-ui-1.7.2.slider.min.js', array('jquery')); add_action('admin_footer', array(&$this, 'admin_footer')); break; case 'comicpress/comicpress_docs': @@ -98,12 +100,6 @@ class ComicPressAdmin { wp_enqueue_script('scriptaculous-effects'); break; } - - if ($plugin_page == 'comicpress/render_admin') { - } - - if ($plugin_page == 'comicpress/comicpress_docs') { - } } function admin_enqueue_scripts($hook_suffix) { @@ -173,6 +169,7 @@ class ComicPressAdmin { function render_admin() { $nonce = wp_create_nonce('comicpress'); $action_nonce = wp_create_nonce('comicpress-comicpress-options'); + $storyline = new ComicPressStoryline(); $storyline->normalize(); $storyline->read_from_options(); @@ -524,6 +521,45 @@ class ComicPressAdmin { } // @codeCoverageIgnoreEnd + function handle_update_retrieve_category_posts($info) { + $this->is_ajax = true; + + $core = new ComicPressTagBuilderFactory(); + + $post_info = array(); + + $factory = new ComicPressBackendFilesystemFactory(); + + foreach ($core->in($info['category'])->all() as $post) { + $annotation = get_post_meta($post->ID, 'comicpress-annotation', true); + + // a way to ensure things are cool + $factory->generate_from_post($post); + + $comic_post = new ComicPressComicPost($post); + $comic_post->normalize_ordering(); + + $current_post = array( + 'id' => $post->ID, + 'title' => $post->post_title, + 'image' => $core->post($post)->media()->url(), + 'date' => date('Y-m-d', strtotime($post->post_date)) + ); + if (is_array($annotation)) { + foreach ($annotation as $category => $annotation_info) { + if ($category == $info['category']) { + $current_post['annotation'] = $annotation_info; + } + } + } + + $post_info[] = $current_post; + } + + echo json_encode($post_info); + exit(0); + } + /** * Update the user's zoom slider metadata. */ diff --git a/classes/ComicPressAnnotatedEntries.inc b/classes/ComicPressAnnotatedEntries.inc new file mode 100644 index 0000000..c1c9c25 --- /dev/null +++ b/classes/ComicPressAnnotatedEntries.inc @@ -0,0 +1,31 @@ +clear_annotations(); + + foreach ($info as $category_grouping => $posts) { + foreach ($posts as $post_id => $annotation) { + $current_annotation = get_post_meta($post_id, 'comicpress-annotation', true); + if (!is_array($current_annotation)) { + $current_annotation = array(); + } + $current_annotation[$category_grouping] = $annotation; + + update_post_meta($post_id, 'comicpress-annotation', $current_annotation); + } + } + } + + function handle_update($info) { + if ($data = json_decode(stripslashes($info['archive_marks']), true)) { + self::save($data); + } + } +} + +add_action('comicpress-handle_update_comicpress_options', 'ComicPressAnnotatedEntries::handle_update', 1, 10); diff --git a/classes/ComicPressDBInterface.inc b/classes/ComicPressDBInterface.inc index fb3f665..2415671 100644 --- a/classes/ComicPressDBInterface.inc +++ b/classes/ComicPressDBInterface.inc @@ -45,7 +45,7 @@ class ComicPressDBInterface { /** * Find the terminal post in a specific category. */ - function get_terminal_post_in_categories($categories, $first = true, $count = false) { + function get_terminal_post_in_categories($categories, $first = true, $count = false, $additional_parameters = array()) { $this->_prepare_wp_query(); $count = $this->ensure_count($count); @@ -53,12 +53,19 @@ class ComicPressDBInterface { if (!is_array($categories)) { $categories = array($categories); } $sort_order = $first ? "asc" : "desc"; $terminal_comic_query = new WP_Query(); - $terminal_comic_query->query(array( + + if (isset($additional_parameters['post_status'])) { + if (is_array($additional_parameters['post_status'])) { + $additional_parameters['post_status'] = implode(',', $additional_parameters['post_status']); + } + } + + $terminal_comic_query->query(array_merge(array( 'showposts' => $count, 'order' => $sort_order, 'category__in' => $categories, 'post_status' => 'publish' - )); + ), $additional_parameters)); $result = false; @@ -77,28 +84,45 @@ class ComicPressDBInterface { /** * Get the first comic in a category. */ - function get_first_post($categories, $reference_post = null, $count = false) { - return $this->get_terminal_post_in_categories($categories, true, $count); + function get_first_post($categories, $reference_post = null, $count = false, $additional = array()) { + return $this->get_terminal_post_in_categories($categories, true, $count, $additional); } /** * Get the last comic in a category. */ - function get_last_post($categories, $reference_post = null, $count = false) { - return $this->get_terminal_post_in_categories($categories, false); + function get_last_post($categories, $reference_post = null, $count = false, $additional = array()) { + return $this->get_terminal_post_in_categories($categories, false, $count, $additional); + } + + function setup_clauses($additional) { + $clauses = array(); + foreach (array_merge(array('post_type' => 'post', 'post_status' => 'publish'), $additional) as $field => $value) { + if (is_array($value)) { + foreach ($value as &$v) { + $v = "'${v}'"; + } + $clauses[] = "AND p.${field} IN (" . join(", ", $value) . ")"; + } else { + $clauses[] = "AND p.${field} = '${value}'"; + } + } + + return $clauses; } /** * Get the comic post adjacent to the current comic. - * Wrapper around get_adjacent_post(). Don't unit test this method. */ - function get_adjacent_post($categories, $next = false, $override_post = null, $count = false) { + function get_adjacent_post($categories, $next = false, $override_post = null, $count = false, $additional = array()) { global $wpdb, $post; $count = $this->ensure_count($count); $post_to_use = (!is_null($override_post)) ? $override_post : $post; + $clauses = $this->setup_clauses($additional); + $op = ($next ? '>' : '<'); $order = ($next ? 'ASC' : 'DESC'); $cats = implode(',', $categories); @@ -106,46 +130,131 @@ class ComicPressDBInterface { $query = $wpdb->prepare("SELECT p.* FROM $wpdb->posts AS p INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id - WHERE p.post_date ${op} %s - AND p.post_type = 'post' - AND p.post_status = 'publish' + WHERE p.post_date ${op} %s" + . implode(" ", $clauses) . " AND tt.taxonomy = 'category' AND tt.term_id IN (${cats}) ORDER BY p.post_date ${order} LIMIT %d", $post_to_use->post_date, $count); - $query_key = 'comicpress_adjacent_post_' . md5($query); - $result = wp_cache_get($query_key, 'counts'); + return $this->do_query($query, 'get_results', function($result, $query_key) use ($count) { + if (!empty($result)) { + if ($count == 1) { $result = $result[0]; } + + wp_cache_set($query_key, $result, 'counts'); + + return $result; + } else { + return ($count == 1) ? false : array(); + } + }); + } + + function do_query($query, $retrieval_method, $callback) { + global $wpdb; + + $query_key = 'comicpress-query' . md5($query); + + $result = wp_cache_get($query_key, 'comicpress'); if ($result !== false) { return $result; } - $result = $wpdb->get_results($query); + $result = $wpdb->{$retrieval_method}($query); - if (!empty($result)) { - if ($count == 1) { $result = $result[0]; } - - wp_cache_set($query_key, $result, 'counts'); - - return $result; - } else { - return ($count == 1) ? false : array(); - } + return $callback($result, $query_key); } /** * Get the previous comic from the current one. */ - function get_previous_post($categories = null, $override_post = null, $count = false) { - return $this->get_adjacent_post($categories, false, $override_post, $count); + function get_previous_post($categories = null, $override_post = null, $count = false, $additional = array()) { + return $this->get_adjacent_post($categories, false, $override_post, $count, $additional); } /** * Get the next comic from the current one. */ - function get_next_post($categories = null, $override_post = null, $count = false) { - return $this->get_adjacent_post($categories, true, $override_post, $count); + function get_next_post($categories = null, $override_post = null, $count = false, $additional = array()) { + return $this->get_adjacent_post($categories, true, $override_post, $count, $additional); + } + + /** + * Get all posts in a particular category. + */ + function get_all_posts($categories = null, $additional = array()) { + if (!is_array($categories)) { $categories = array($categories); } + $sort_order = $first ? "asc" : "desc"; + $all_comic_query = new WP_Query(); + $all_comic_query->query(array_merge(array( + 'nopaging' => true, + 'order' => $sort_order, + 'category__in' => $categories, + 'post_status' => 'publish' + ), $additional)); + + return $all_comic_query->posts; + } + + /** + * Get a count of all available posts. + */ + // TODO Make the additional merges work for SQL queries + function count_all_posts($categories = null, $additional = array()) { + global $wpdb; + + if (!is_array($categories)) { $categories = array($categories); } + + $cats = implode(',', $categories); + + $clauses = $this->setup_clauses($additional); + + $query = $wpdb->prepare("SELECT count(p.ID) FROM $wpdb->posts AS p + INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id + INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + WHERE tt.taxonomy = 'category' + AND tt.term_id IN (${cats})" . + implode(" ", $clauses)); + + return $this->do_query($query, 'get_var', function($result, $query_key) { + if (!empty($result)) { + wp_cache_set($query_key, $result, 'counts'); + + return $result; + } else { + return is_numeric($result) ? $result : false; + } + }); + } + + function get_post_by_index($categories = null, $index = 0, $additional = array()) { + global $wpdb; + + if (!is_array($categories)) { $categories = array($categories); } + + $cats = implode(',', $categories); + + $clauses = $this->setup_clauses($additional); + + $query = $wpdb->prepare("SELECT p.* FROM $wpdb->posts AS p + INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id + INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id + WHERE tt.taxonomy = 'category' + AND tt.term_id IN (${cats})" . + implode(" ", $clauses) . " + ORDER BY p.post_date ASC + LIMIT ${index}, 1"); + + return $this->do_query($query, 'get_row', function($result, $query_key) { + if (!empty($result)) { + wp_cache_set($query_key, $result, 'counts'); + + return $result; + } else { + return false; + } + }); } function get_parent_child_category_ids() { @@ -163,5 +272,32 @@ class ComicPressDBInterface { return $parent_child_categories; } + function clear_annotations() { + global $wpdb; + + $wpdb->query($wpdb->prepare("DELETE FROM $wpdb->postmeta WHERE meta_key = %s", 'comicpress-annotation')); + } + + function get_annotations($category) { + global $wpdb; + + $query = $wpdb->prepare("SELECT pm.post_id, pm.meta_value + FROM $wpdb->postmeta pm + WHERE pm.meta_key = %s", 'comicpress-annotation'); + + $result = $wpdb->get_results($query); + + $matching_annotations = array(); + if (is_array($result)) { + foreach ($result as $row) { + if ($annotation = maybe_unserialize($row->meta_value)) { + $matching_annotations[$row->post_id] = $annotation[$category]; + } + } + } + + return $matching_annotations; + } + // @codeCoverageIgnoreEnd } diff --git a/classes/ComicPressTagBuilder.inc b/classes/ComicPressTagBuilder.inc index 06b2a6f..c9c3ab9 100644 --- a/classes/ComicPressTagBuilder.inc +++ b/classes/ComicPressTagBuilder.inc @@ -3,7 +3,7 @@ require_once('ComicPressStoryline.inc'); class ComicPressTagBuilderFactory { - public $storyline, $dbi; + public $storyline, $dbi, $default_dbparams = array(); public function __construct($dbi = null) { $this->storyline = new ComicPressStoryline(); @@ -40,13 +40,21 @@ class ComicPressTagBuilderFactory { return $is_in; } + public function annotations($category) { + return $this->dbi->get_annotations($category); + } + public function find_file($name, $path = '', $categories = null) { $comicpress = ComicPress::get_instance(); return $comicpress->find_file($name, $path, $categories); } + public function default_dbparams($params = array()) { + $this->default_dbparams = $params; + } + public function _new_comicpresstagbuilder($p, $s, $d) { - return new ComicPressTagBuilder($p, $s, $d); + return new ComicPressTagBuilder($p, $s, $d, $this->default_dbparams); } public function media($index = null) { @@ -101,13 +109,14 @@ class ComicPressTagBuilderFactory { } class ComicPressTagBuilder { - public $categories, $restrictions, $storyline, $dbi, $parent_post, $post, $category; + public $categories, $restrictions, $storyline, $dbi, $parent_post, $post, $category, $dbparams; - public function __construct($parent_post, $storyline, $dbi) { + public function __construct($parent_post, $storyline, $dbi, $dbparams = array()) { $this->restrictions = array(); $this->storyline = $storyline; $this->dbi = $dbi; $this->parent_post = $parent_post; + $this->dbparams = $dbparams; } public function _setup_postdata($p) { @@ -121,22 +130,24 @@ class ComicPressTagBuilder { return new ComicPressComicPost($post); } + function setup($throw_exception_on_invalid = false) { + if (empty($this->post)) { + if ($throw_exception_on_invalid === true) { + throw new ComicPressException('You need to have retrieved a post for setup to work'); + } else { + return false; + } + } + $this->_setup_postdata($this->post); + return $this->post; + } + // TODO filtered versions of template tags public function __call($method, $arguments) { $ok = false; $return = $this; switch ($method) { - case 'setup': - if (empty($this->post)) { - if (isset($arguments[0])) { - throw new ComicPressException('You need to have retrieved a post for setup to work'); - } else { - return false; - } - } - $this->_setup_postdata($this->post); - return $this->post; case 'from': if (!isset($arguments[0])) { throw new ComicPressException('Need to specify a post'); @@ -147,6 +158,12 @@ class ComicPressTagBuilder { $this->parent_post = (object)$arguments[0]; $ok = true; break; + case 'dbparams': + if (!is_array($arguments[0])) { + throw new ComicPressException('dbparams requires an array'); + } + $this->dbparams = $arguments[0]; + break; case 'current': if (isset($this->category)) { if (isset($arguments[0])) { @@ -172,6 +189,9 @@ class ComicPressTagBuilder { case 'previous': case 'first': case 'last': + case 'all': + case 'count': + case 'index': if (isset($this->category)) { switch ($method) { case 'next': @@ -184,6 +204,15 @@ class ComicPressTagBuilder { case 'last': $id = end(array_keys($this->storyline->_structure)); break; + case 'all': + return array_keys($this->storyline->_structure); + case 'count': + return count(array_keys($this->storyline->_structure)); + case 'index': + if (!isset($arguments[0])) { + throw new Exception('You need to provide an index to retrieve.'); + } + return array_slice(array_keys($this->storyline->_structure), (int)$arguments[0]); } return isset($arguments[0]) ? get_category($id) : $id; } else { @@ -198,7 +227,22 @@ class ComicPressTagBuilder { $count = false; } - $result = call_user_func(array($this->dbi, "get_${method}_post"), $this->storyline->build_from_restrictions($this->restrictions), $this->parent_post, $count); + $restriction_categories = $this->storyline->build_from_restrictions($this->restrictions); + + switch ($method) { + case 'all': + return $this->dbi->get_all_posts($restriction_categories, $this->dbparams); + case 'count': + return $this->dbi->count_all_posts($restriction_categories, $this->dbparams); + case 'index': + if (!isset($arguments[0])) { + throw new Exception('You need to provide an index to retrieve.'); + } + return $this->dbi->get_post_by_index($restriction_categories, (int)$arguments[0], $this->dbparams); + default: + $result = call_user_func(array($this->dbi, "get_${method}_post"), $this->storyline->build_from_restrictions($this->restrictions), $this->parent_post, $count, $this->dbparams); + break; + } if ($count > 1) { if (is_array($result)) { diff --git a/classes/backends/ComicPressBackendFilesystem.inc b/classes/backends/ComicPressBackendFilesystem.inc index 638fce0..ae1d2cb 100644 --- a/classes/backends/ComicPressBackendFilesystem.inc +++ b/classes/backends/ComicPressBackendFilesystem.inc @@ -12,7 +12,13 @@ class ComicPressBackendFilesystem extends ComicPressBackend { $this->source_name = __('Filesystem', 'comicpress'); } - function _getimagesize($file) { return getimagesize($file); } + function _getimagesize($file) { + if (file_exists($file)) { + return getimagesize($file); + } else { + return array(0, 0); + } + } function dims($size = null) { $dims = array(); diff --git a/classes/partials/_annotation-editor.inc b/classes/partials/_annotation-editor.inc new file mode 100644 index 0000000..5859e98 --- /dev/null +++ b/classes/partials/_annotation-editor.inc @@ -0,0 +1,254 @@ +

+ +
+

+ +

+ + + +
+ +
+ +
+
+
+
Most Recent Post
+
+ +
+ +
+

+ + + + + + + + + + +
+
+
+ + +
+ + diff --git a/classes/partials/options-admin.inc b/classes/partials/options-admin.inc index ff5c43f..33ecd98 100644 --- a/classes/partials/options-admin.inc +++ b/classes/partials/options-admin.inc @@ -6,6 +6,8 @@
+ +

diff --git a/css/cp-admin.css b/css/cp-admin.css index 06ab42e..3436724 100644 --- a/css/cp-admin.css +++ b/css/cp-admin.css @@ -288,3 +288,109 @@ tr.highlighted td { .comicpress-admin-section h3 { margin-top: 0 } + +#post-marks-ui { + overflow: hidden; + zoom: 1; + display: none; + padding: 10px; +} + +#post-slider { + width: 10px; + border: solid #aaa 1px; + position: relative; + margin-top: 10px; + float: left; + display: inline +} + +#post-marks-loader { + text-align: center +} + +#post-marks-holder { + width: 60px; + margin-right: 10px; + position: relative; + float: left; + display: inline +} + +#post-marks-holder a { + background: #622; + border: solid #a33 1px; + position: absolute; + width: 60px; + line-height: 16px; + font-size: 11px; + color: white; + margin-top: -5px; + padding: 2px; + text-decoration: none; + display: block; +} + +.ui-slider-handle { + position: absolute; + z-index: 2; + height: 12px; + width: 12px; + left: -2px; + margin-bottom: -4px; + border: solid #888 1px; + border-radius: 2px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + background-color: #ccc; +} + +#post-info { + position: absolute; + width: 50%; + border: solid #aaa 1px; + background: #f0f0f0; + padding: 0.5em; + border-radius: 0.5em; + -moz-border-radius: 0.5em; + -webkit-border-radius: 0.5em; + display: none; + overflow: hidden; + zoom: 1 +} + +#post-info img { + float: left; + display: inline; + margin-right: 0.3em +} + +#post-info label { + display: block; +} + +#mark-annotation { + color: red; +} + +#mark-annotation.marked { + color: green; +} + +#annotation-info { + margin-left: 130px +} + +#annotation-info input, #annotation-info textarea { + width: 100% +} + +#annotation-info h4 { + margin: 0.25em 0; +} + +#most-recent-post { + padding: 0.2em 0 0 10%; + border-top: solid #aaa 1px; + font-style: italic; +} diff --git a/images/1.gif b/images/1.gif deleted file mode 100644 index 737e841..0000000 Binary files a/images/1.gif and /dev/null differ diff --git a/images/2.gif b/images/2.gif deleted file mode 100644 index 3234ca1..0000000 Binary files a/images/2.gif and /dev/null differ diff --git a/images/2a.gif b/images/2a.gif deleted file mode 100644 index 5c059b8..0000000 Binary files a/images/2a.gif and /dev/null differ diff --git a/images/3.gif b/images/3.gif deleted file mode 100644 index d0dbea1..0000000 Binary files a/images/3.gif and /dev/null differ diff --git a/images/3a.gif b/images/3a.gif deleted file mode 100644 index 647c3c7..0000000 Binary files a/images/3a.gif and /dev/null differ diff --git a/images/4.gif b/images/4.gif deleted file mode 100644 index 43bc18f..0000000 Binary files a/images/4.gif and /dev/null differ diff --git a/images/loader.gif b/images/loader.gif new file mode 100644 index 0000000..c91aa7d Binary files /dev/null and b/images/loader.gif differ diff --git a/js/jquery-ui-1.7.2.slider.min.js b/js/jquery-ui-1.7.2.slider.min.js new file mode 100755 index 0000000..fc341ba --- /dev/null +++ b/js/jquery-ui-1.7.2.slider.min.js @@ -0,0 +1,22 @@ +/* + * jQuery UI 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI + */ +jQuery.ui||(function(c){var i=c.fn.remove,d=c.browser.mozilla&&(parseFloat(c.browser.version)<1.9);c.ui={version:"1.7.2",plugin:{add:function(k,l,n){var m=c.ui[k].prototype;for(var j in n){m.plugins[j]=m.plugins[j]||[];m.plugins[j].push([l,n[j]])}},call:function(j,l,k){var n=j.plugins[l];if(!n||!j.element[0].parentNode){return}for(var m=0;m0){return true}m[j]=1;l=(m[j]>0);m[j]=0;return l},isOverAxis:function(k,j,l){return(k>j)&&(k<(j+l))},isOver:function(o,k,n,m,j,l){return c.ui.isOverAxis(o,n,j)&&c.ui.isOverAxis(k,m,l)},keyCode:{BACKSPACE:8,CAPS_LOCK:20,COMMA:188,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38}};if(d){var f=c.attr,e=c.fn.removeAttr,h="http://www.w3.org/2005/07/aaa",a=/^aria-/,b=/^wairole:/;c.attr=function(k,j,l){var m=l!==undefined;return(j=="role"?(m?f.call(this,k,j,"wairole:"+l):(f.apply(this,arguments)||"").replace(b,"")):(a.test(j)?(m?k.setAttributeNS(h,j.replace(a,"aaa:"),l):f.call(this,k,j.replace(a,"aaa:"))):f.apply(this,arguments)))};c.fn.removeAttr=function(j){return(a.test(j)?this.each(function(){this.removeAttributeNS(h,j.replace(a,""))}):e.call(this,j))}}c.fn.extend({remove:function(){c("*",this).add(this).each(function(){c(this).triggerHandler("remove")});return i.apply(this,arguments)},enableSelection:function(){return this.attr("unselectable","off").css("MozUserSelect","").unbind("selectstart.ui")},disableSelection:function(){return this.attr("unselectable","on").css("MozUserSelect","none").bind("selectstart.ui",function(){return false})},scrollParent:function(){var j;if((c.browser.msie&&(/(static|relative)/).test(this.css("position")))||(/absolute/).test(this.css("position"))){j=this.parents().filter(function(){return(/(relative|absolute|fixed)/).test(c.curCSS(this,"position",1))&&(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}else{j=this.parents().filter(function(){return(/(auto|scroll)/).test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0)}return(/fixed/).test(this.css("position"))||!j.length?c(document):j}});c.extend(c.expr[":"],{data:function(l,k,j){return !!c.data(l,j[3])},focusable:function(k){var l=k.nodeName.toLowerCase(),j=c.attr(k,"tabindex");return(/input|select|textarea|button|object/.test(l)?!k.disabled:"a"==l||"area"==l?k.href||!isNaN(j):!isNaN(j))&&!c(k)["area"==l?"parents":"closest"](":hidden").length},tabbable:function(k){var j=c.attr(k,"tabindex");return(isNaN(j)||j>=0)&&c(k).is(":focusable")}});function g(m,n,o,l){function k(q){var p=c[m][n][q]||[];return(typeof p=="string"?p.split(/,?\s+/):p)}var j=k("getter");if(l.length==1&&typeof l[0]=="string"){j=j.concat(k("getterSetter"))}return(c.inArray(o,j)!=-1)}c.widget=function(k,j){var l=k.split(".")[0];k=k.split(".")[1];c.fn[k]=function(p){var n=(typeof p=="string"),o=Array.prototype.slice.call(arguments,1);if(n&&p.substring(0,1)=="_"){return this}if(n&&g(l,k,p,o)){var m=c.data(this[0],k);return(m?m[p].apply(m,o):undefined)}return this.each(function(){var q=c.data(this,k);(!q&&!n&&c.data(this,k,new c[l][k](this,p))._init());(q&&n&&c.isFunction(q[p])&&q[p].apply(q,o))})};c[l]=c[l]||{};c[l][k]=function(o,n){var m=this;this.namespace=l;this.widgetName=k;this.widgetEventPrefix=c[l][k].eventPrefix||k;this.widgetBaseClass=l+"-"+k;this.options=c.extend({},c.widget.defaults,c[l][k].defaults,c.metadata&&c.metadata.get(o)[k],n);this.element=c(o).bind("setData."+k,function(q,p,r){if(q.target==o){return m._setData(p,r)}}).bind("getData."+k,function(q,p){if(q.target==o){return m._getData(p)}}).bind("remove",function(){return m.destroy()})};c[l][k].prototype=c.extend({},c.widget.prototype,j);c[l][k].getterSetter="option"};c.widget.prototype={_init:function(){},destroy:function(){this.element.removeData(this.widgetName).removeClass(this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").removeAttr("aria-disabled")},option:function(l,m){var k=l,j=this;if(typeof l=="string"){if(m===undefined){return this._getData(l)}k={};k[l]=m}c.each(k,function(n,o){j._setData(n,o)})},_getData:function(j){return this.options[j]},_setData:function(j,k){this.options[j]=k;if(j=="disabled"){this.element[k?"addClass":"removeClass"](this.widgetBaseClass+"-disabled "+this.namespace+"-state-disabled").attr("aria-disabled",k)}},enable:function(){this._setData("disabled",false)},disable:function(){this._setData("disabled",true)},_trigger:function(l,m,n){var p=this.options[l],j=(l==this.widgetEventPrefix?l:this.widgetEventPrefix+l);m=c.Event(m);m.type=j;if(m.originalEvent){for(var k=c.event.props.length,o;k;){o=c.event.props[--k];m[o]=m.originalEvent[o]}}this.element.trigger(m,n);return !(c.isFunction(p)&&p.call(this.element[0],m,n)===false||m.isDefaultPrevented())}};c.widget.defaults={disabled:false};c.ui.mouse={_mouseInit:function(){var j=this;this.element.bind("mousedown."+this.widgetName,function(k){return j._mouseDown(k)}).bind("click."+this.widgetName,function(k){if(j._preventClickEvent){j._preventClickEvent=false;k.stopImmediatePropagation();return false}});if(c.browser.msie){this._mouseUnselectable=this.element.attr("unselectable");this.element.attr("unselectable","on")}this.started=false},_mouseDestroy:function(){this.element.unbind("."+this.widgetName);(c.browser.msie&&this.element.attr("unselectable",this._mouseUnselectable))},_mouseDown:function(l){l.originalEvent=l.originalEvent||{};if(l.originalEvent.mouseHandled){return}(this._mouseStarted&&this._mouseUp(l));this._mouseDownEvent=l;var k=this,m=(l.which==1),j=(typeof this.options.cancel=="string"?c(l.target).parents().add(l.target).filter(this.options.cancel).length:false);if(!m||j||!this._mouseCapture(l)){return true}this.mouseDelayMet=!this.options.delay;if(!this.mouseDelayMet){this._mouseDelayTimer=setTimeout(function(){k.mouseDelayMet=true},this.options.delay)}if(this._mouseDistanceMet(l)&&this._mouseDelayMet(l)){this._mouseStarted=(this._mouseStart(l)!==false);if(!this._mouseStarted){l.preventDefault();return true}}this._mouseMoveDelegate=function(n){return k._mouseMove(n)};this._mouseUpDelegate=function(n){return k._mouseUp(n)};c(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate);(c.browser.safari||l.preventDefault());l.originalEvent.mouseHandled=true;return true},_mouseMove:function(j){if(c.browser.msie&&!j.button){return this._mouseUp(j)}if(this._mouseStarted){this._mouseDrag(j);return j.preventDefault()}if(this._mouseDistanceMet(j)&&this._mouseDelayMet(j)){this._mouseStarted=(this._mouseStart(this._mouseDownEvent,j)!==false);(this._mouseStarted?this._mouseDrag(j):this._mouseUp(j))}return !this._mouseStarted},_mouseUp:function(j){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;this._preventClickEvent=(j.target==this._mouseDownEvent.target);this._mouseStop(j)}return false},_mouseDistanceMet:function(j){return(Math.max(Math.abs(this._mouseDownEvent.pageX-j.pageX),Math.abs(this._mouseDownEvent.pageY-j.pageY))>=this.options.distance)},_mouseDelayMet:function(j){return this.mouseDelayMet},_mouseStart:function(j){},_mouseDrag:function(j){},_mouseStop:function(j){},_mouseCapture:function(j){return true}};c.ui.mouse.defaults={cancel:null,distance:1,delay:0}})(jQuery);;/* + * jQuery UI Slider 1.7.2 + * + * Copyright (c) 2009 AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT (MIT-LICENSE.txt) + * and GPL (GPL-LICENSE.txt) licenses. + * + * http://docs.jquery.com/UI/Slider + * + * Depends: + * ui.core.js + */ +(function(a){a.widget("ui.slider",a.extend({},a.ui.mouse,{_init:function(){var b=this,c=this.options;this._keySliding=false;this._handleIndex=null;this._detectOrientation();this._mouseInit();this.element.addClass("ui-slider ui-slider-"+this.orientation+" ui-widget ui-widget-content ui-corner-all");this.range=a([]);if(c.range){if(c.range===true){this.range=a("
");if(!c.values){c.values=[this._valueMin(),this._valueMin()]}if(c.values.length&&c.values.length!=2){c.values=[c.values[0],c.values[0]]}}else{this.range=a("
")}this.range.appendTo(this.element).addClass("ui-slider-range");if(c.range=="min"||c.range=="max"){this.range.addClass("ui-slider-range-"+c.range)}this.range.addClass("ui-widget-header")}if(a(".ui-slider-handle",this.element).length==0){a('').appendTo(this.element).addClass("ui-slider-handle")}if(c.values&&c.values.length){while(a(".ui-slider-handle",this.element).length').appendTo(this.element).addClass("ui-slider-handle")}}this.handles=a(".ui-slider-handle",this.element).addClass("ui-state-default ui-corner-all");this.handle=this.handles.eq(0);this.handles.add(this.range).filter("a").click(function(d){d.preventDefault()}).hover(function(){if(!c.disabled){a(this).addClass("ui-state-hover")}},function(){a(this).removeClass("ui-state-hover")}).focus(function(){if(!c.disabled){a(".ui-slider .ui-state-focus").removeClass("ui-state-focus");a(this).addClass("ui-state-focus")}else{a(this).blur()}}).blur(function(){a(this).removeClass("ui-state-focus")});this.handles.each(function(d){a(this).data("index.ui-slider-handle",d)});this.handles.keydown(function(i){var f=true;var e=a(this).data("index.ui-slider-handle");if(b.options.disabled){return}switch(i.keyCode){case a.ui.keyCode.HOME:case a.ui.keyCode.END:case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:f=false;if(!b._keySliding){b._keySliding=true;a(this).addClass("ui-state-active");b._start(i,e)}break}var g,d,h=b._step();if(b.options.values&&b.options.values.length){g=d=b.values(e)}else{g=d=b.value()}switch(i.keyCode){case a.ui.keyCode.HOME:d=b._valueMin();break;case a.ui.keyCode.END:d=b._valueMax();break;case a.ui.keyCode.UP:case a.ui.keyCode.RIGHT:if(g==b._valueMax()){return}d=g+h;break;case a.ui.keyCode.DOWN:case a.ui.keyCode.LEFT:if(g==b._valueMin()){return}d=g-h;break}b._slide(i,e,d);return f}).keyup(function(e){var d=a(this).data("index.ui-slider-handle");if(b._keySliding){b._stop(e,d);b._change(e,d);b._keySliding=false;a(this).removeClass("ui-state-active")}});this._refreshValue()},destroy:function(){this.handles.remove();this.range.remove();this.element.removeClass("ui-slider ui-slider-horizontal ui-slider-vertical ui-slider-disabled ui-widget ui-widget-content ui-corner-all").removeData("slider").unbind(".slider");this._mouseDestroy()},_mouseCapture:function(d){var e=this.options;if(e.disabled){return false}this.elementSize={width:this.element.outerWidth(),height:this.element.outerHeight()};this.elementOffset=this.element.offset();var h={x:d.pageX,y:d.pageY};var j=this._normValueFromMouse(h);var c=this._valueMax()-this._valueMin()+1,f;var k=this,i;this.handles.each(function(l){var m=Math.abs(j-k.values(l));if(c>m){c=m;f=a(this);i=l}});if(e.range==true&&this.values(1)==e.min){f=a(this.handles[++i])}this._start(d,i);k._handleIndex=i;f.addClass("ui-state-active").focus();var g=f.offset();var b=!a(d.target).parents().andSelf().is(".ui-slider-handle");this._clickOffset=b?{left:0,top:0}:{left:d.pageX-g.left-(f.width()/2),top:d.pageY-g.top-(f.height()/2)-(parseInt(f.css("borderTopWidth"),10)||0)-(parseInt(f.css("borderBottomWidth"),10)||0)+(parseInt(f.css("marginTop"),10)||0)};j=this._normValueFromMouse(h);this._slide(d,i,j);return true},_mouseStart:function(b){return true},_mouseDrag:function(d){var b={x:d.pageX,y:d.pageY};var c=this._normValueFromMouse(b);this._slide(d,this._handleIndex,c);return false},_mouseStop:function(b){this.handles.removeClass("ui-state-active");this._stop(b,this._handleIndex);this._change(b,this._handleIndex);this._handleIndex=null;this._clickOffset=null;return false},_detectOrientation:function(){this.orientation=this.options.orientation=="vertical"?"vertical":"horizontal"},_normValueFromMouse:function(d){var c,h;if("horizontal"==this.orientation){c=this.elementSize.width;h=d.x-this.elementOffset.left-(this._clickOffset?this._clickOffset.left:0)}else{c=this.elementSize.height;h=d.y-this.elementOffset.top-(this._clickOffset?this._clickOffset.top:0)}var f=(h/c);if(f>1){f=1}if(f<0){f=0}if("vertical"==this.orientation){f=1-f}var e=this._valueMax()-this._valueMin(),i=f*e,b=i%this.options.step,g=this._valueMin()+i-b;if(b>(this.options.step/2)){g+=this.options.step}return parseFloat(g.toFixed(5))},_start:function(d,c){var b={handle:this.handles[c],value:this.value()};if(this.options.values&&this.options.values.length){b.value=this.values(c);b.values=this.values()}this._trigger("start",d,b)},_slide:function(f,e,d){var g=this.handles[e];if(this.options.values&&this.options.values.length){var b=this.values(e?0:1);if((this.options.values.length==2&&this.options.range===true)&&((e==0&&d>b)||(e==1&&d1){this.options.values[b]=e;this._refreshValue(c);if(!d){this._change(null,b)}}if(arguments.length){if(this.options.values&&this.options.values.length){return this._values(b)}else{return this.value()}}else{return this._values()}},_setData:function(b,d,c){a.widget.prototype._setData.apply(this,arguments);switch(b){case"disabled":if(d){this.handles.filter(".ui-state-focus").blur();this.handles.removeClass("ui-state-hover");this.handles.attr("disabled","disabled")}else{this.handles.removeAttr("disabled")}case"orientation":this._detectOrientation();this.element.removeClass("ui-slider-horizontal ui-slider-vertical").addClass("ui-slider-"+this.orientation);this._refreshValue(c);break;case"value":this._refreshValue(c);break}},_step:function(){var b=this.options.step;return b},_value:function(){var b=this.options.value;if(bthis._valueMax()){b=this._valueMax()}return b},_values:function(b){if(arguments.length){var c=this.options.values[b];if(cthis._valueMax()){c=this._valueMax()}return c}else{return this.options.values}},_valueMin:function(){var b=this.options.min;return b},_valueMax:function(){var b=this.options.max;return b},_refreshValue:function(c){var f=this.options.range,d=this.options,l=this;if(this.options.values&&this.options.values.length){var i,h;this.handles.each(function(p,n){var o=(l.values(p)-l._valueMin())/(l._valueMax()-l._valueMin())*100;var m={};m[l.orientation=="horizontal"?"left":"bottom"]=o+"%";a(this).stop(1,1)[c?"animate":"css"](m,d.animate);if(l.options.range===true){if(l.orientation=="horizontal"){(p==0)&&l.range.stop(1,1)[c?"animate":"css"]({left:o+"%"},d.animate);(p==1)&&l.range[c?"animate":"css"]({width:(o-lastValPercent)+"%"},{queue:false,duration:d.animate})}else{(p==0)&&l.range.stop(1,1)[c?"animate":"css"]({bottom:(o)+"%"},d.animate);(p==1)&&l.range[c?"animate":"css"]({height:(o-lastValPercent)+"%"},{queue:false,duration:d.animate})}}lastValPercent=o})}else{var j=this.value(),g=this._valueMin(),k=this._valueMax(),e=k!=g?(j-g)/(k-g)*100:0;var b={};b[l.orientation=="horizontal"?"left":"bottom"]=e+"%";this.handle.stop(1,1)[c?"animate":"css"](b,d.animate);(f=="min")&&(this.orientation=="horizontal")&&this.range.stop(1,1)[c?"animate":"css"]({width:e+"%"},d.animate);(f=="max")&&(this.orientation=="horizontal")&&this.range[c?"animate":"css"]({width:(100-e)+"%"},{queue:false,duration:d.animate});(f=="min")&&(this.orientation=="vertical")&&this.range.stop(1,1)[c?"animate":"css"]({height:e+"%"},d.animate);(f=="max")&&(this.orientation=="vertical")&&this.range[c?"animate":"css"]({height:(100-e)+"%"},{queue:false,duration:d.animate})}}}));a.extend(a.ui.slider,{getter:"value values",version:"1.7.2",eventPrefix:"slide",defaults:{animate:false,delay:0,distance:0,max:100,min:0,orientation:"horizontal",range:false,step:1,value:0,values:null}})})(jQuery);; \ No newline at end of file diff --git a/js/json2.js b/js/json2.js new file mode 100644 index 0000000..39d8f37 --- /dev/null +++ b/js/json2.js @@ -0,0 +1,481 @@ +/* + http://www.JSON.org/json2.js + 2009-09-29 + + Public Domain. + + NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. + + See http://www.JSON.org/js.html + + + This code should be minified before deployment. + See http://javascript.crockford.com/jsmin.html + + USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO + NOT CONTROL. + + + This file creates a global JSON object containing two methods: stringify + and parse. + + JSON.stringify(value, replacer, space) + value any JavaScript value, usually an object or array. + + replacer an optional parameter that determines how object + values are stringified for objects. It can be a + function or an array of strings. + + space an optional parameter that specifies the indentation + of nested structures. If it is omitted, the text will + be packed without extra whitespace. If it is a number, + it will specify the number of spaces to indent at each + level. If it is a string (such as '\t' or ' '), + it contains the characters used to indent at each level. + + This method produces a JSON text from a JavaScript value. + + When an object value is found, if the object contains a toJSON + method, its toJSON method will be called and the result will be + stringified. A toJSON method does not serialize: it returns the + value represented by the name/value pair that should be serialized, + or undefined if nothing should be serialized. The toJSON method + will be passed the key associated with the value, and this will be + bound to the value + + For example, this would serialize Dates as ISO strings. + + Date.prototype.toJSON = function (key) { + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + return this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z'; + }; + + You can provide an optional replacer method. It will be passed the + key and value of each member, with this bound to the containing + object. The value that is returned from your method will be + serialized. If your method returns undefined, then the member will + be excluded from the serialization. + + If the replacer parameter is an array of strings, then it will be + used to select the members to be serialized. It filters the results + such that only members with keys listed in the replacer array are + stringified. + + Values that do not have JSON representations, such as undefined or + functions, will not be serialized. Such values in objects will be + dropped; in arrays they will be replaced with null. You can use + a replacer function to replace those with JSON values. + JSON.stringify(undefined) returns undefined. + + The optional space parameter produces a stringification of the + value that is filled with line breaks and indentation to make it + easier to read. + + If the space parameter is a non-empty string, then that string will + be used for indentation. If the space parameter is a number, then + the indentation will be that many spaces. + + Example: + + text = JSON.stringify(['e', {pluribus: 'unum'}]); + // text is '["e",{"pluribus":"unum"}]' + + + text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); + // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' + + text = JSON.stringify([new Date()], function (key, value) { + return this[key] instanceof Date ? + 'Date(' + this[key] + ')' : value; + }); + // text is '["Date(---current time---)"]' + + + JSON.parse(text, reviver) + This method parses a JSON text to produce an object or array. + It can throw a SyntaxError exception. + + The optional reviver parameter is a function that can filter and + transform the results. It receives each of the keys and values, + and its return value is used instead of the original value. + If it returns what it received, then the structure is not modified. + If it returns undefined then the member is deleted. + + Example: + + // Parse the text. Values that look like ISO date strings will + // be converted to Date objects. + + myData = JSON.parse(text, function (key, value) { + var a; + if (typeof value === 'string') { + a = +/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); + if (a) { + return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], + +a[5], +a[6])); + } + } + return value; + }); + + myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { + var d; + if (typeof value === 'string' && + value.slice(0, 5) === 'Date(' && + value.slice(-1) === ')') { + d = new Date(value.slice(5, -1)); + if (d) { + return d; + } + } + return value; + }); + + + This is a reference implementation. You are free to copy, modify, or + redistribute. +*/ + +/*jslint evil: true, strict: false */ + +/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, + call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, + getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, + lastIndex, length, parse, prototype, push, replace, slice, stringify, + test, toJSON, toString, valueOf +*/ + + +// Create a JSON object only if one does not already exist. We create the +// methods in a closure to avoid creating global variables. + +if (!this.JSON) { + this.JSON = {}; +} + +(function () { + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + if (typeof Date.prototype.toJSON !== 'function') { + + Date.prototype.toJSON = function (key) { + + return isFinite(this.valueOf()) ? + this.getUTCFullYear() + '-' + + f(this.getUTCMonth() + 1) + '-' + + f(this.getUTCDate()) + 'T' + + f(this.getUTCHours()) + ':' + + f(this.getUTCMinutes()) + ':' + + f(this.getUTCSeconds()) + 'Z' : null; + }; + + String.prototype.toJSON = + Number.prototype.toJSON = + Boolean.prototype.toJSON = function (key) { + return this.valueOf(); + }; + } + + var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, + gap, + indent, + meta = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + escapable.lastIndex = 0; + return escapable.test(string) ? + '"' + string.replace(escapable, function (a) { + var c = meta[a]; + return typeof c === 'string' ? c : + '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }) + '"' : + '"' + string + '"'; + } + + + function str(key, holder) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value && typeof value === 'object' && + typeof value.toJSON === 'function') { + value = value.toJSON(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : + gap ? '[\n' + gap + + partial.join(',\n' + gap) + '\n' + + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + k = rep[i]; + if (typeof k === 'string') { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : + gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + + mind + '}' : '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + if (typeof JSON.stringify !== 'function') { + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + } + + +// If the JSON object does not yet have a parse method, give it one. + + if (typeof JSON.parse !== 'function') { + JSON.parse = function (text, reviver) { + +// The parse method takes a text and an optional reviver function, and returns +// a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + +// The walk method is used to recursively walk the resulting structure so +// that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + +// Parsing happens in four stages. In the first stage, we replace certain +// Unicode characters with escape sequences. JavaScript handles many characters +// incorrectly, either silently deleting them, or treating them as line endings. + + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + +// In the second stage, we run the text against regular expressions that look +// for non-JSON patterns. We are especially concerned with '()' and 'new' +// because they can cause invocation, and '=' because it can cause mutation. +// But just to be safe, we want to reject all unexpected forms. + +// We split the second stage into 4 regexp operations in order to work around +// crippling inefficiencies in IE's and Safari's regexp engines. First we +// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we +// replace all simple value tokens with ']' characters. Third, we delete all +// open brackets that follow a colon or comma or that begin the text. Finally, +// we look to see that the remaining characters are only whitespace or ']' or +// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/. +test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@'). +replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']'). +replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + +// In the third stage we use the eval function to compile the text into a +// JavaScript structure. The '{' operator is subject to a syntactic ambiguity +// in JavaScript: it can begin a block or an object literal. We wrap the text +// in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + +// In the optional fourth stage, we recursively walk the new structure, passing +// each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + +// If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + } +}()); diff --git a/test/ComicPressAnnotatedEntriesTest.php b/test/ComicPressAnnotatedEntriesTest.php new file mode 100644 index 0000000..322c74b --- /dev/null +++ b/test/ComicPressAnnotatedEntriesTest.php @@ -0,0 +1,55 @@ +getMock('ComicPressDBInterface', array('clear_annotations')); + $dbi->expects($this->once())->method('clear_annotations'); + + wp_insert_post(array('ID' => 1)); + wp_insert_post(array('ID' => 2)); + wp_insert_post(array('ID' => 3)); + + ComicPressAnnotatedEntries::save(array( + 'group' => array( + '1' => array( + 'annotated' => true + ) + ), + 'group2' => array( + '1' => array( + 'title' => 'Annotation Title 2', + 'description' => 'Annotation Description 2', + 'annotated' => true + ), + '2' => array( + 'title' => 'Annotation Title 3', + 'description' => 'Annotation Description 3', + 'annotated' => true + ), + ) + ), $dbi); + + $this->assertEquals(array( + 'group' => array( + 'annotated' => true, + ), + 'group2' => array( + 'annotated' => true, + 'title' => 'Annotation Title 2', + 'description' => 'Annotation Description 2' + ), + ), get_post_meta(1, 'comicpress-annotation', true)); + + $this->assertEquals(array( + 'group2' => array( + 'annotated' => true, + 'title' => 'Annotation Title 3', + 'description' => 'Annotation Description 3' + ), + ), get_post_meta(2, 'comicpress-annotation', true)); + } +} diff --git a/test/ComicPressTagBuilderTest.php b/test/ComicPressTagBuilderTest.php index fc17bd4..852f3e5 100644 --- a/test/ComicPressTagBuilderTest.php +++ b/test/ComicPressTagBuilderTest.php @@ -104,6 +104,13 @@ class ComicPressTagBuilderTest extends PHPUnit_Framework_TestCase { ), array('get_first_post', array(1,2,3,4,5), $p) ), + array( + array( + array('in', 'all'), + array('all') + ), + array('get_all_posts', array(1,2,3,4,5)) + ), ); }