Initial repository population
authordan@fuhry
Wed, 13 Jun 2007 22:33:54 -0400
changeset 0 0417a5a0c7be
child 1 6f8b7c6fac02
Initial repository population
decir/bbcode.php
decir/common.php
decir/constants.php
decir/forum_index.php
decir/index.php
decir/install.php
decir/install.sql
decir/js/bbcedit.css
decir/js/bbcedit.js
decir/js/colorpick/farbtastic.css
decir/js/colorpick/farbtastic.js
decir/js/colorpick/jquery.js
decir/js/colorpick/marker.png
decir/js/colorpick/mask.png
decir/js/colorpick/wheel.png
decir/notes.sql
decir/posting.php
decir/viewforum.php
decir/viewtopic.php
plugins/Decir.php
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/bbcode.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,98 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * bbcode.php - BBcode-to-HTML renderer
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+ 
+function str_replace_once($needle1, $needle2, $haystack)
+{
+  $len_h = strlen($haystack);
+  $len_1 = strlen($needle1);
+  $len_2 = strlen($needle2);
+  if ( $len_h < $len_1 )
+    return $haystack;
+  if ( $needle1 == $haystack )
+    return $needle1;
+  for ( $i = 0; $i < $len_h; $i++ )
+  {
+    if ( substr($haystack, $i, $len_1) == $needle1 )
+    {
+      $haystack = substr($haystack, 0, $i) .
+                  $needle2 .
+                  substr($haystack, $i + $len_1);
+      return $haystack;
+    }
+  }
+}
+
+function render_bbcode($text, $bbcode_uid)
+{
+  // First things first, strip out all [code] sections
+  $text = decir_bbcode_strip_code($text, $bbcode_uid, $_code);
+  
+  // Bold text
+  $text = preg_replace("/\[b:$bbcode_uid\](.*?)\[\/b:$bbcode_uid\]/is", '<b>\\1</b>', $text);
+  
+  // Italicized text
+  $text = preg_replace("/\[i:$bbcode_uid\](.*?)\[\/i:$bbcode_uid\]/is", '<i>\\1</i>', $text);
+  
+  // Uunderlined text
+  $text = preg_replace("/\[u:$bbcode_uid\](.*?)\[\/u:$bbcode_uid\]/is", '<u>\\1</u>', $text);
+  
+  // Colored text
+  $text = preg_replace("/\[color=\#([A-F0-9]*){3,6}:$bbcode_uid\](.*?)\[\/color:$bbcode_uid\]/is", '<span style="color: #\\1">\\2</span>', $text);
+  
+  // Quotes
+  $text = preg_replace("/\[quote:$bbcode_uid\](.*?)\[\/quote:$bbcode_uid\]/is", '<blockquote>\\1</blockquote>', $text);
+  
+  // Newlines
+  $text = str_replace("\n", "<br />\n", $text);
+  
+  // Restore [code] blocks
+  $text = decir_bbcode_restore_code($text, $bbcode_uid, $_code);
+  
+  // Code
+  $text = preg_replace("/\[code:$bbcode_uid\](.*?)\[\/code:$bbcode_uid\]/is", '<pre>\\1</pre>', $text);
+  
+  return $text;
+}
+
+function decir_bbcode_strip_code($text, $uid, &$code_secs)
+{
+  preg_match_all("/\[code:$uid\](.*?)\[\/code:$uid\]/is", $text, $matches);
+  foreach ( $matches[1] as $i => $m )
+  {
+    $text = str_replace_once($m, "{CODE_SECTION|$i:$uid}", $text);
+    $code_secs[$i] = $m;
+  }
+  return $text;
+}
+
+function decir_bbcode_restore_code($text, $uid, $code_secs)
+{
+  foreach ( $code_secs as $i => $code )
+  {
+    $text = str_replace("{CODE_SECTION|$i:$uid}", $code, $text);
+  }
+  return $text;
+}
+
+function bbcode_strip_uid($bbcode, $uid)
+{
+  // BBcode tags with attributes
+  $bbcode = preg_replace("/\[([a-z]+?):{$uid}=([^\]]+?)\](.*?)\[\/\\1:{$uid}\]/is", '[\\1=\\2]\\3[/\\1]', $bbcode);
+  
+  // BBcode tags without attributes
+  $bbcode = preg_replace("/\[([a-z]+?):{$uid}\](.*?)\[\/\\1:{$uid}\]/is", '[\\1]\\2[/\\1]', $bbcode);
+  
+  return $bbcode;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/common.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,45 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * common.php - Loader and common basic functions
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+if(!defined('DECIR_ROOT'))
+{
+  $_GET['title'] = 'null';
+  header('HTTP/1.1 403 Forbidden');
+  require('../includes/common.php');
+  die_friendly('Access denied', '<p>This script cannot be run outside of Enano.</p>');
+}
+
+require('constants.php');
+
+$html = '    <!-- Decir\'s updated namespace extractor function -->
+    <script type="text/javascript">
+      function strToPageID(string)
+      {
+        var ret;
+        for(var i in namespace_list)
+          if(namespace_list[i] != \'\')
+            if(namespace_list[i] == string.substr(0, namespace_list[i].length))
+              ret = [string.substr(namespace_list[i].length), i];
+        
+        if ( ret )
+          return ret;
+        
+        return [string, \'Article\'];
+      }
+    </script>
+  ';
+  
+$template->add_header($html);
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/constants.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,25 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * constants.php - important values
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+if(!defined('ENANO_DECIR_VERSION'))
+  die('Hacking attempt');
+
+define('FORUM_FORUM', 1);
+define('FORUM_CATEGORY', 2);
+
+define('TOPIC_NORMAL', 1);
+define('TOPIC_STICKY', 4);
+define('TOPIC_ANNOUNCE', 5);
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/forum_index.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,87 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * install.php - Database installation wizard
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+require('common.php');
+
+$template->header();
+
+// Not much left now but to just do it...
+$q = $db->sql_query('SELECT f.forum_id,f.forum_type,f.forum_name,f.forum_desc,f.num_topics,f.num_posts,
+       p.post_id,t.topic_id,t.topic_title,u.username,u.user_level,p.timestamp FROM '.table_prefix.'decir_forums AS f
+  LEFT JOIN '.table_prefix.'decir_topics AS t
+    ON (t.forum_id=f.forum_id)
+  LEFT JOIN '.table_prefix.'decir_posts AS p
+    ON (p.topic_id=t.topic_id)
+  LEFT JOIN '.table_prefix.'users AS u
+    ON (u.user_id=f.last_post_user OR f.last_post_user IS NULL)
+  WHERE ( t.topic_id=f.last_post_topic AND p.post_id=f.last_post_id ) OR ( f.last_post_topic IS NULL AND f.last_post_id IS NULL )
+    GROUP BY f.parent,f.forum_id
+    ORDER BY f.forum_order;');
+
+if (!$q)
+  $db->_die();
+
+echo '<div class="tblholder">
+      <table border="0" cellspacing="1" cellpadding="4">
+        <tr>
+          <th colspan="2">Forum</th>
+          <th style="max-width: 50px;">Topics</th>
+          <th style="max-width: 50px;">Posts</th>
+          <th>Last post</th>
+        </tr>';
+$cat_open = false;
+if ( $row = $db->fetchrow($q) )
+{
+  do {
+    switch ( $row['forum_type'] )
+    {
+      case FORUM_FORUM:
+        $color = ( $row['user_level'] >= USER_LEVEL_ADMIN ) ? 'AA0000' : ( ( $row['user_level'] >= USER_LEVEL_MOD ) ? '00AA00' : '0000AA' );
+        // Forum
+        echo '<tr><td class="row3" style="text-align: center;">&lt;icon&gt;</td><td class="row2"><b><a href="' . makeUrlNS('DecirForum', $row['forum_id']) . '">'
+             . $row['forum_name'] . '</a></b><br />' . $row['forum_desc'].'</td>
+             <td class="row3" style="text-align: center;">' . $row['num_topics'] . '</td>
+             <td class="row3" style="text-align: center;">' . $row['num_posts'] . '</td>
+             <td class="row1" style="text-align: center;">
+               <small>
+                 <a href="' . makeUrlNS('DecirTopic', $row['topic_id']) . '#post' . $row['post_id'] . '">' . $row['topic_title'] . '</a><br />
+                 ' . date('d M Y h:i a', $row['timestamp']) . '<br />
+                 by <b><a style="color: #' . $color . '" href="' . makeUrlNS('User', $row['username']) . '">' . $row['username'] . '</a></b>
+               </small>
+             </td>
+             </tr>';
+        break;
+      case FORUM_CATEGORY:
+        // Category
+        if ( $cat_open )
+          echo '</tbody>';
+        echo '<tr><td class="row1" colspan="2"><h3 style="margin: 0; padding: 0;">' . $row['forum_name'] . '</h3></td><td class="row2" colspan="3"></td></tr>
+              <tbody id="forum_cat_' . $row['forum_id'] . '">';
+        $cat_open = true;
+        break;
+    }
+  } while ( $row = $db->fetchrow($q) );
+}
+else
+{
+  echo '<td class="row1" colspan="4">This board has no forums.</td>';
+}
+if ( $cat_open )
+  echo '</tbody>';
+echo '</table>
+      </div>';
+
+$template->footer();
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/index.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,13 @@
+<?php
+
+$_GET['title'] = 'Enano:Access_denied';
+require('../includes/common.php');
+header('HTTP/1.1 403 Forbidden');
+$session->perms['edit_page'] = AUTH_DENY;
+$session->perms['view_source'] = AUTH_DENY;
+$template->tpl_strings['PAGE_NAME'] = 'Access denied';
+
+$template->header();
+echo '<p>The administrator has flagged the page "' . $_SERVER['REQUEST_URI'] . '" so that it cannot be accessed from the web. Perhaps this is because this is a cache or includes directory and only needs to be accessed by scripts.</p><p>HTTP error: 403 Forbidden</p>';
+$template->footer();
+$db->close();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/install.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,61 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * install.php - Database installation wizard
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+require('common.php');
+
+if ( $session->auth_level < USER_LEVEL_ADMIN )
+{
+  header('Location: ' . makeUrlComplete('Special', 'Login/' . $paths->page, 'level=9', true));
+  exit;
+}
+
+if ( $v = getConfig('decir_version') )
+{
+  $mode = 'upgrade';
+  $upg_ver = $v;
+}
+else
+{
+  $mode = 'install';
+}
+
+$page = ( isset($_POST['step']) && in_array($_POST['step'], array('welcome', 'install', 'finish')) ) ? $_POST['step'] : 'welcome';
+
+$template->header();
+
+switch($page)
+{
+  case 'welcome':
+    ?>
+    <h3>Welcome to Decir, the Enano bulletin board suite.</h3>
+    <p>Before you can use your forum, we'll need to run a few database queries to get the forum set up.</p>
+    <form action="<?php echo makeUrl($paths->page); ?>" method="post">
+      <input type="hidden" name="step" value="install" />
+      <input type="submit" value="Continue" style="display: block; margin: 0 auto;" />
+    </form>
+    <?php
+    break;
+  case 'install':
+    setConfig('decir_version', ENANO_DECIR_VERSION);
+    ?>
+    <form action="<?php echo makeUrl($paths->page); ?>" method="post">
+      <input type="hidden" name="step" value="finish" />
+      <input type="submit" name="do_install_finish" value="Next &gt;" style="display: block; margin: 0 auto;" />
+    </form>
+    <?php
+    break;
+}
+
+$template->footer();
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/install.sql	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,51 @@
+CREATE TABLE decir_forums(
+  forum_id int(12) unsigned NOT NULL auto_increment,
+  forum_type tinyint(2) unsigned NOT NULL DEFAULT 1,
+  forum_name varchar(255) NOT NULL,
+  forum_desc text NOT NULL,
+  parent int(12) unsigned NOT NULL DEFAULT 0,
+  forum_order int(12) unsigned NOT NULL DEFAULT 1,
+  last_post_id int(18) unsigned,
+  last_post_topic int(12) unsigned,
+  last_post_user int(12) unsigned,
+  num_topics int(15) unsigned,
+  num_posts int(18) unsigned,
+  forum_extra text,
+  PRIMARY KEY ( forum_id )
+);
+CREATE TABLE decir_topics(
+  topic_id int(15) unsigned NOT NULL auto_increment,
+  forum_id int(12) unsigned NOT NULL,
+  topic_title varchar(255) NOT NULL,
+  topic_icon tinyint(3) unsigned NOT NULL,
+  topic_starter int(12) unsigned NOT NULL,
+  topic_type tinyint(2) unsigned NOT NULL DEFAULT 1,
+  topic_locked tinyint(1) unsigned NOT NULL DEFAULT 0,
+  topic_moved tinyint(1) unsigned NOT NULL DEFAULT 0,
+  timestamp int(11) unsigned NOT NULL,
+  PRIMARY KEY ( topic_id )
+);
+CREATE TABLE decir_posts(
+  post_id bigint(18) unsigned NOT NULL auto_increment,
+  topic_id bigint(15) unsigned NOT NULL,
+  poster_id int(12) unsigned NOT NULL,
+  poster_name varchar(255) NOT NULL,
+  timestamp int(11) unsigned NOT NULL,
+  last_edited_by int(12) unsigned DEFAULT NULL,
+  edit_count int(5) unsigned,
+  edit_reason varchar(255),
+  PRIMARY KEY ( post_id )
+);
+CREATE TABLE decir_posts_text(
+  post_id bigint(18) unsigned NOT NULL,
+  post_text longtext NOT NULL,
+  bbcode_uid varchar(10) NOT NULL,
+  PRIMARY KEY ( post_id )
+);
+CREATE TABLE decir_hits(
+  hit_id bigint(21) unsigned NOT NULL auto_increment,
+  user_id int(12) unsigned NOT NULL DEFAULT 1,
+  topic_id bigint(15) unsigned NOT NULL,
+  timestamp int(11) unsigned NOT NULL,
+  PRIMARY KEY ( hit_id )
+);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/js/bbcedit.css	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,54 @@
+input.bbcbutton {
+  font-family: arial, sans-serif;
+  font-size: 8pt;
+  border: 1px solid #000000;
+  padding: 2px;
+  margin-right: 2px;
+  margin-bottom: 2px;
+}
+
+fieldset {
+  border-color: #000000;
+  border-style: solid;
+  border-width: 1px;
+}
+
+fieldset legend {
+  font-size: 8pt; 
+  font-family: arial, sans-serif;
+}
+
+input.clicksmiley {
+  padding-right: 1px;
+  padding-bottom: 1px;
+  border-width: 0;
+  padding: 0;
+  background-color: transparent;
+}
+
+input.clicksmiley:active {
+  padding-right:  0px;
+  padding-bottom: 0px;
+  padding-left:   1px;
+  padding-top:    1px;
+}
+
+.sizepick_td {
+  font-family: arial, sans-serif;
+  border: 1px solid #CCCCCC;
+  padding: 3px;
+  max-width: 75px;
+  clip: rect(0px,75px,auto,0px);
+  overflow: hidden;
+  white-space: nowrap;
+  cursor: pointer;
+}
+pre.code {
+  border: 1px dotted #cccccc;
+  background-color: #F0F0F0;
+  padding: 10px;
+  white-space: nowrap;
+  color: #00AA00;
+  font-family: deja vu sans mono, courier new, monospace;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/js/bbcedit.js	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,632 @@
+// Client detection from MediaWiki
+var clientPC = navigator.userAgent.toLowerCase(); // Get client info
+var is_gecko = ((clientPC.indexOf('gecko')!=-1) && (clientPC.indexOf('spoofer')==-1)
+                && (clientPC.indexOf('khtml') == -1) && (clientPC.indexOf('netscape/7.0')==-1));
+var is_safari = ((clientPC.indexOf('applewebkit')!=-1) && (clientPC.indexOf('spoofer')==-1));
+var is_khtml = (navigator.vendor == 'KDE' || ( document.childNodes && !document.all && !navigator.taintEnabled ));
+if (clientPC.indexOf('opera') != -1) {
+	var is_opera = true;
+	var is_opera_preseven = (window.opera && !document.childNodes);
+	var is_opera_seven = (window.opera && document.childNodes);
+}
+
+var $_GET=new Object();
+var aParams=document.location.search.substr(1).split('&');
+for ( i = 0; i < aParams.length; i++ ) {
+  var aParam=aParams[i].split('=');
+  var sParamName=aParam[0];
+  var sParamValue=aParam[1];
+  $_GET[sParamName]=sParamValue;
+}
+
+// List of BBcode buttons
+
+var buttons = [
+    {
+      'start' : '[b]',
+      'end'   : '[/b]',
+      'desc'  : 'Bold',
+      'style' : { 'fontWeight' : 'bold' }
+    },
+    {
+      'start' : '[i]',
+      'end'   : '[/i]',
+      'desc'  : 'Italics',
+      'style' : { 'fontStyle' : 'italic' }
+    },
+    {
+      'start' : '[u]',
+      'end'   : '[/u]',
+      'desc'  : 'Underline',
+      'style' : { 'textDecoration' : 'underline' }
+    },
+    {
+      'start' : '[color=black]',
+      'end'   : '[/color]',
+      'custom': true,
+      'func'  : function() { openColorPicker(this); },
+      'desc'  : 'Color',
+      'style' : { 'color' : 'red' }
+    },
+    {
+      'start' : '[size=1]',
+      'end'   : '[/size]',
+      'custom': true,
+      'func'  : function() { openSizePicker(this); },
+      'desc'  : 'Size'
+    },
+    {
+      'start' : '[code]',
+      'end'   : '[/code]',
+      'desc'  : 'Code',
+      'style' : { 'fontFamily' : 'courier new, monospace' }
+    },
+    {
+      'start' : '[quote]',
+      'end'   : '[/quote]',
+      'desc'  : 'Quote'
+    }
+  ];
+
+// List of valid smilies
+var smilies = {
+  'O:-)'        : 'face-angel.png',
+  'O:)'         : 'face-angel.png',
+  'O=)'         : 'face-angel.png',
+  ':-)'         : 'face-smile.png',
+  ':)'          : 'face-smile.png',
+  '=)'          : 'face-smile-big.png',
+  ':-('         : 'face-sad.png',
+  ':('          : 'face-sad.png',
+  ';('          : 'face-sad.png',
+  ':-O'         : 'face-surprise.png',
+  ';-)'         : 'face-wink.png',
+  ';)'          : 'face-wink.png',
+  '8-)'         : 'face-glasses.png',
+  '8)'          : 'face-glasses.png',
+  ':-D'         : 'face-grin.png',
+  ':D'          : 'face-grin.png',
+  '=D'          : 'face-grin.png',
+  ':-*'         : 'face-kiss.png',
+  ':*'          : 'face-kiss.png',
+  '=*'          : 'face-kiss.png',
+  ':\'('        : 'face-crying.png',
+  ':-|'         : 'face-plain.png',
+  ':-\\'        : 'face-plain.png',
+  ':-/'         : 'face-plain.png',
+  ':joke:'      : 'face-plain.png',
+  ']:->'        : 'face-devil-grin.png',
+  ':kiss:'      : 'face-kiss.png',
+  ':-P'         : 'face-tongue-out.png',
+  ':P'          : 'face-tongue-out.png',
+  ':-p'         : 'face-tongue-out.png',
+  ':p'          : 'face-tongue-out.png',
+  ':-X'         : 'face-sick.png',
+  ':X'          : 'face-sick.png',
+  ':sick:'      : 'face-sick.png',
+  ':-]'         : 'face-oops.png',
+  ':]'          : 'face-oops.png',
+  ':oops:'      : 'face-oops.png',
+  ':-['         : 'face-embarassed.png',
+  ':['          : 'face-embarassed.png'
+};
+
+function initBBCodeControls()
+{
+  txtars = getElementsByClassName(document, 'textarea', 'bbcode');
+  for ( i = 0; i < txtars.length; i++ )
+  {
+    convertTextAreaToBBCode(txtars[i]);
+  }
+}
+
+var smileycache = { 'td' : [], 'img' : [] };
+
+function convertTextAreaToBBCode(txtarea)
+{
+  var pn = txtarea.parentNode;
+  
+  var loadingDiv = document.createElement('div');
+  loadingDiv.appendChild(document.createTextNode('Initializing editor...'));
+  pn.appendChild(loadingDiv);
+  
+  if(!IE)
+  {
+  
+    var smileybox = document.createElement('div');
+    smileybox.style.cssFloat = 'left';   // Mozilla
+    smileybox.style.styleFloat = 'left'; // IE
+    smileybox.style.marginRight = '10px';
+    smileybox.style.maxWidth = '220px';
+    smileybox.style.maxHeight = '300px';
+    smileybox.style.clip = 'rect(0px,auto,auto,0px)';
+    smileybox.style.overflow = 'auto';
+    
+    var fl = document.createElement('fieldset');
+    var lb = document.createElement('legend');
+    lb.appendChild(document.createTextNode('Smilies'));
+    fl.appendChild(lb);
+    var used = [];
+    
+    var scriptPath = ''; // REMOVE FOR ENANO IMPLEMENTATION!
+    
+    var frm = document.createElement('form');
+    frm.action='javascript:void(0)';
+    frm.onsubmit = function(){return false;};
+    
+    var tbl = document.createElement('table');
+    tbl.border = '0';
+    tbl.cellspacing = '0';
+    tbl.cellpadding = '0';
+    tbl.width = '100%';
+    
+    var tr = document.createElement('tr');
+    var tick = -1;
+    var apd = false;
+    
+    for ( var i in smilies )
+    {
+      apd = false;
+      if ( in_array(smilies[i], used) )
+        continue;
+      used.push(smilies[i]);
+      
+      tick++;
+      if ( tick == 3 )
+      {
+        tick = 0;
+        tbl.appendChild(tr);
+        tr = document.createElement('tr');
+        apd = true;
+      }
+      
+      var smile = i.replace(/\\/g, '\\\\');
+      
+      var td = document.createElement('td');
+      td.style.textAlign = 'center';
+      td.style.padding = '0';
+      
+      var img = ( IE ) ? new Image() : document.createElement('input');
+      img.type = 'image';
+      img.className = 'clicksmiley';
+      img.src = scriptPath + '/images/smilies/' + smilies[i];
+      img.style.cursor = 'pointer';
+      img.style.margin = '2px';
+      img.onclick = insertSmiley;
+      img.title = i;
+      img.alt = i;
+      if (IE)
+      {
+        // This IE bug (yet another) is stupid BEYOND reason.
+        setTimeout('smileycache.td['+smileycache.td.length+'].appendChild(smileycache.img['+smileycache.img.length+']);', 20);
+        smileycache.img[smileycache.img.length] = img;
+        smileycache.td[smileycache.td.length] = td;
+      }
+      else
+      {
+        td.appendChild(img);
+      }
+      tr.appendChild(td);
+    }
+    
+    if (!apd)
+      tbl.appendChild(tr);
+    
+    frm.appendChild(tbl);
+    fl.appendChild(frm);
+    
+    smileybox.appendChild(fl);
+    pn.insertBefore(smileybox, txtarea);
+    
+  }
+  else
+  {
+    var div = document.createElement('div');
+    var html = '<fieldset style="padding: 10px; display: inline;"><legend>Available smilies:</legend>';
+    var c = 0;
+    for ( var i in smilies )
+    {
+      c++;
+      html += i + '&nbsp;&nbsp;';
+      if ( c == 10 )
+      {
+        html += '<br />';
+        c = 0;
+      }
+    }
+    html += '</fieldset>';
+    div.innerHTML = html;
+    pn.appendChild(div, txtarea);
+  }
+  
+  var toolbar = document.createElement('div');
+  for ( j = 0; j < buttons.length; j++ )
+  {
+    var btn = document.createElement('input');
+    btn.type='button';
+    btn.className = 'bbcbutton';
+    btn.value = buttons[j].desc;
+    if ( buttons[j].custom )
+      btn.onclick = buttons[j].func;
+    else 
+      btn.onclick = BBCodeClickHandler;
+    if ( buttons[j].style )
+    {
+      for ( var k in buttons[j].style )
+      {
+        btn.style[k] = buttons[j].style[k];
+      }
+    }
+    toolbar.appendChild(btn);
+  }
+  
+  pn.insertBefore(toolbar, txtarea);
+  pn.removeChild(loadingDiv);
+}
+
+function insertSmiley()
+{
+  var imgid = this.src;
+  imgid = imgid.split('/');
+  imgid = imgid[imgid.length-1];
+  emot = array_search(imgid, smilies) + ' ';
+  var o = this.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.nextSibling.firstChild;
+  formatBBCode(o, emot, "", "");
+  return false;
+}
+
+function BBCodeClickHandler()
+{
+  var obj = false;
+  for ( i = 0; i < buttons.length; i++ )
+  {
+    if ( buttons[i]['desc'] == this.value )
+    {
+      obj = buttons[i];
+      break;
+    }
+  }
+  if(!obj)
+    return false;
+  formatBBCode(this, obj['start'], obj['end'], obj['desc']);
+  return true;
+}
+
+//
+// COLOR PICKER
+//
+
+function openColorPicker(parent)
+{
+  var off = fetch_offset(parent);
+  var dim = fetch_dimensions(parent);
+  var top = off['top'] + dim['h'] - 1;
+  var left = off['left'];
+  
+  var div = document.createElement('div');
+  div.style.border = '1px solid #000000';
+  div.style.padding = '10px';
+  div.style.position = 'absolute';
+  div.style.top = top + 'px';
+  div.style.left = left + 'px';
+  div.style.backgroundColor = '#ffffff';
+  
+  var cwheel = document.createElement('div');
+  cwheel.id = 'color_wheel';
+  
+  var cinput = document.createElement('input');
+  cinput.size = '7';
+  cinput.id = 'color_val';
+  cinput.value = '#ff0000';
+  
+  var btni = document.createElement('input');
+  btni.type = 'button';
+  btni.value = 'Insert';
+  btni.onclick = finishColorPicker;
+  
+  var btnc = document.createElement('input');
+  btnc.type = 'button';
+  btnc.value = 'Cancel';
+  btnc.onclick = closeColorPicker;
+  
+  div.appendChild(cwheel);
+  div.appendChild(cinput);
+  div.appendChild(btni);
+  div.appendChild(btnc);
+  
+  parent.parentNode.appendChild(div);
+  
+  $jq('#color_wheel').farbtastic('#color_val');
+}
+
+function finishColorPicker()
+{
+  parent = this.parentNode;
+  input = parent.getElementsByTagName('input')[0];
+  color = input.value;
+  formatBBCode(parent, '[color=' + color + ']', '[/color]', 'Colored text');
+  parent.parentNode.removeChild(parent);
+}
+
+function closeColorPicker()
+{
+  parent = this.parentNode;
+  parent.parentNode.removeChild(parent);
+}
+
+//
+// SIZE PICKER
+//
+
+function openSizePicker(parent)
+{
+  
+  var off = fetch_offset(parent);
+  var dim = fetch_dimensions(parent);
+  var top = off['top'] + dim['h'] - 1;
+  var left = off['left'];
+  
+  var div = document.createElement('div');
+  div.style.border = '1px solid #000000';
+  div.style.padding = '3px';
+  div.style.position = 'absolute';
+  div.style.top = top + 'px';
+  div.style.left = left + 'px';
+  div.style.backgroundColor = '#ffffff';
+  div.style.width = '130px';
+  //div.style.maxHeight = '400px';
+  div.style.clip = 'rect(0px,auto,auto,0px)';
+  div.style.overflow = 'hidden';
+  
+  var tbl = document.createElement('table');
+  tbl.border = '0';
+  tbl.cellspacing = '0';
+  tbl.cellpadding = '0';
+  tbl.style.maxWidth = '75px';
+  tbl.style.clip = 'rect(0px,75px,auto,0px)';
+  tbl.style.overflow = 'hidden';
+  
+  var i = 0;
+  
+  for ( i = 0.5; i <= 4; i=i+0.5 )
+  {
+    var tr = document.createElement('tr');
+    var td = document.createElement('td');
+    td.innerHTML = i;
+    tr.appendChild(td);
+    var td = document.createElement('td');
+    td.className = 'sizepick_td';
+    td.style.fontSize = i + 'em';
+    td.innerHTML = 'The quick brown fox jumps over the lazy dog.';
+    td.onclick = function() { sizePickClickHandler(this); }
+    tr.appendChild(td);
+    tbl.appendChild(tr);
+  }
+  
+  var a = document.createElement('a');
+  a.href='#';
+  a.onclick = function() { this.parentNode.parentNode.removeChild(this.parentNode); return false; };
+  a.appendChild(document.createTextNode('Close size picker'));
+  
+  div.appendChild(tbl);
+  div.appendChild(a);
+  parent.parentNode.appendChild(div);
+  
+}
+
+function sizePickClickHandler(parent)
+{
+  size = parent.style.fontSize.substr(0, parent.style.fontSize.length - 2);
+  formatBBCode(parent.parentNode.parentNode.parentNode, '[size=' + size + ']', '[/size]', 'Large/small text');
+  parent.parentNode.parentNode.parentNode.parentNode.removeChild(parent.parentNode.parentNode.parentNode);
+}
+
+//
+// HTML RENDERER
+//
+
+function htmlspecialchars(text)
+{
+  text = text.replace(/</g, '&lt;');
+  text = text.replace(/>/g, '&gt;');
+  return text;
+}
+
+function render_bbcode(text)
+{
+  // Smilies
+  for(var i in smilies)
+  {
+    if ( text.indexOf(i) > -1 )
+    {
+      while ( text.indexOf(i) > -1 )
+      {
+        text = text.replace(i, '<img alt="' + rawhtmlcode(i) + '" src="/images/smilies/' + smilies[i] + '" />');
+      }
+    }
+  }
+  
+  // Destroy (X|HT)ML tags
+  text = htmlspecialchars(text);
+  text = text.replace(/ /g, '&nbsp;');
+  
+  // Bold text
+  text = text.replace(/\[b\]([\w\W]+?)\[\/b\]/g, '<span style="font-weight: bold;">$1</span>');
+  
+  // Italicized text
+  text = text.replace(/\[i\]([\w\W]+?)\[\/i\]/g, '<span style="font-style: italic;">$1</span>');
+  
+  // Underlined text
+  text = text.replace(/\[u\]([\w\W]+?)\[\/u\]/g, '<span style="text-decoration: underline;">$1</span>');
+  
+  // Quotes
+  text = text.replace(/\[quote\]([\w\W]+?)\[\/quote\]/g, '<blockquote>$1</blockquote>');
+  
+  // Colored text
+  text = text.replace(/\[color=#([0-9A-Fa-f]+?)\]([\w\W]*?)\[\/color\]/g, '<span style="color: #$1">$2</span>');
+  
+  // Sized text
+  text = text.replace(/\[size=([0-9\.]+?)\]([\w\W]*?)\[\/size\]/g, '<span style="font-size: $1em">$2</span>');
+  
+  // Newlines
+  var nlre = new RegExp(unescape('%0A'), 'g');
+  text = text.replace(nlre, '<br />' + unescape('%0A'));
+  
+  // Preformatted text
+  text = text.replace(/\[code\]([\w\W]+?)\[\/code\]/gi, '<pre class="code">$1</pre>');
+  text = text.replace(/<pre class=\"code\">([\s]+)/gi, '<pre class="code">');
+  text = text.replace(/([\s]+)<\/pre>/gi, '</pre>');
+  
+  return text;
+}
+
+function rawhtmlcode(text)
+{
+  var ret = '';
+  for ( var i = 0; i < text.length; i++ )
+  {
+    chr = text.charCodeAt(i);
+    chr = '&#' + chr + ';';
+    ret += chr;
+  }
+  return ret;
+}
+
+// Preview function
+function makePreview(obj)
+{
+  obj = document.getElementById(obj);
+  var bbcode = obj.value;
+  var body = document.getElementsByTagName('body')[0];
+  var div = document.createElement('div');
+  div.style.border = '1px solid #000';
+  div.style.padding = '10px';
+  div.innerHTML = render_bbcode(bbcode);
+  //body.insertBefore(div, body.firstChild);
+  body.appendChild(div);
+}
+
+function fetch_offset(obj) {
+  var left_offset = obj.offsetLeft;
+  var top_offset = obj.offsetTop;
+  while ((obj = obj.offsetParent) != null) {
+    left_offset += obj.offsetLeft;
+    top_offset += obj.offsetTop;
+  }
+  return { 'left' : left_offset, 'top' : top_offset };
+}
+
+function fetch_dimensions(o) {
+  var w = o.offsetWidth;
+  var h = o.offsetHeight;
+  return { 'w' : w, 'h' : h };
+}
+
+function getElementsByClassName(parent, type, cls) {
+  if(!type)
+    type = '*';
+  if(!parent)
+    parent = document;
+  ret = new Array();
+  el = parent.getElementsByTagName(type);
+  for ( var i in el )
+  {
+    if(el[i].className)
+    {
+      if(el[i].className.indexOf(' ') > 0)
+      {
+        classes = el[i].className.split(' ');
+      }
+      else
+      {
+        classes = new Array();
+        classes.push(el[i].className);
+      }
+      if ( in_array(cls, classes) )
+        ret.push(el[i]);
+    }
+  }
+  return ret;
+}
+
+function in_array(needle, haystack)
+{
+  for( var i in haystack )
+  {
+    if(haystack[i] == needle)
+      return true;
+  }
+  return false;
+}
+
+function array_search(needle, haystack)
+{
+  for( var i in haystack )
+  {
+    if(haystack[i] == needle)
+      return i;
+  }
+  return false;
+}
+
+document.getElementsByClassName = function(type, cls) {
+  return getElementsByClassName(document, type, cls);
+}
+
+// Function adapted from MediaWiki/phpBB
+function formatBBCode(parent, tagOpen, tagClose, sampleText)
+{
+  var txtarea = parent.parentNode.nextSibling;
+  
+  // IE
+	if (document.selection  && !is_gecko) {
+		var theSelection = document.selection.createRange().text;
+		if (!theSelection)
+			theSelection=sampleText;
+		txtarea.focus();
+		if (theSelection.charAt(theSelection.length - 1) == " ") { // exclude ending space char, if any
+			theSelection = theSelection.substring(0, theSelection.length - 1);
+			document.selection.createRange().text = tagOpen + theSelection + tagClose + " ";
+		} else {
+			document.selection.createRange().text = tagOpen + theSelection + tagClose;
+		}
+
+	// Mozilla
+	} else if(txtarea.selectionStart || txtarea.selectionStart == '0') {
+		var replaced = false;
+		var startPos = txtarea.selectionStart;
+		var endPos = txtarea.selectionEnd;
+		if (endPos-startPos)
+			replaced = true;
+		var scrollTop = txtarea.scrollTop;
+		var myText = (txtarea.value).substring(startPos, endPos);
+		if (!myText)
+			myText=sampleText;
+		if (myText.charAt(myText.length - 1) == " ") { // exclude ending space char, if any
+			subst = tagOpen + myText.substring(0, (myText.length - 1)) + tagClose + " ";
+		} else {
+			subst = tagOpen + myText + tagClose;
+		}
+		txtarea.value = txtarea.value.substring(0, startPos) + subst +
+			txtarea.value.substring(endPos, txtarea.value.length);
+		txtarea.focus();
+		//set new selection
+		if (replaced) {
+			var cPos = startPos+(tagOpen.length+myText.length+tagClose.length);
+			txtarea.selectionStart = cPos;
+			txtarea.selectionEnd = cPos;
+		} else {
+			txtarea.selectionStart = startPos+tagOpen.length;
+			txtarea.selectionEnd = startPos+tagOpen.length+myText.length;
+		}
+		txtarea.scrollTop = scrollTop;
+
+	// All other browsers get no toolbar.
+	}
+	// reposition cursor if possible
+	if (txtarea.createTextRange)
+		txtarea.caretPos = document.selection.createRange().duplicate();
+}
+
+addOnloadHook(initBBCodeControls);
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/js/colorpick/farbtastic.css	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,33 @@
+.farbtastic {
+  position: relative;
+}
+.farbtastic * {
+  position: absolute;
+  cursor: crosshair;
+}
+.farbtastic, .farbtastic .wheel {
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .color, .farbtastic .overlay {
+  top: 47px;
+  left: 47px;
+  width: 101px;
+  height: 101px;
+}
+.farbtastic .wheel {
+  background: url(wheel.png) no-repeat;
+  width: 195px;
+  height: 195px;
+}
+.farbtastic .overlay {
+  background: url(mask.png) no-repeat;
+}
+.farbtastic .marker {
+  width: 17px;
+  height: 17px;
+  margin: -8px 0 0 -8px;
+  overflow: hidden; 
+  background: url(marker.png) no-repeat;
+}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/js/colorpick/farbtastic.js	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,331 @@
+// $Id: farbtastic.js,v 1.2 2007/01/08 22:53:01 unconed Exp $
+// Farbtastic 1.2
+
+// This code has been modified to not break Dynano
+
+jQuery.fn.farbtastic = function (callback) {
+  $jq.farbtastic(this, callback);
+  return this;
+};
+
+jQuery.farbtastic = function (container, callback) {
+  var container = $jq(container).get(0);
+  return container.farbtastic || (container.farbtastic = new jQuery._farbtastic(container, callback));
+}
+
+jQuery._farbtastic = function (container, callback) {
+  // Store farbtastic object
+  var fb = this;
+
+  // Insert markup
+  $jq(container).html('<div class="farbtastic"><div class="color"></div><div class="wheel"></div><div class="overlay"></div><div class="h-marker marker"></div><div class="sl-marker marker"></div></div>');
+  var e = $jq('.farbtastic', container);
+  fb.wheel = $jq('.wheel', container).get(0);
+  // Dimensions
+  fb.radius = 84;
+  fb.square = 100;
+  fb.width = 194;
+
+  // Fix background PNGs in IE6
+  if (navigator.appVersion.match(/MSIE [0-6]\./)) {
+    $jq('*', e).each(function () {
+      if (this.currentStyle.backgroundImage != 'none') {
+        var image = this.currentStyle.backgroundImage;
+        image = this.currentStyle.backgroundImage.substring(5, image.length - 2);
+        $jq(this).css({
+          'backgroundImage': 'none',
+          'filter': "progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=crop, src='" + image + "')"
+        });
+      }
+    });
+  }
+
+  /**
+   * Link to the given element(s) or callback.
+   */
+  fb.linkTo = function (callback) {
+    // Unbind previous nodes
+    if (typeof fb.callback == 'object') {
+      $jq(fb.callback).unbind('keyup', fb.updateValue);
+    }
+
+    // Reset color
+    fb.color = null;
+
+    // Bind callback or elements
+    if (typeof callback == 'function') {
+      fb.callback = callback;
+    }
+    else if (typeof callback == 'object' || typeof callback == 'string') {
+      fb.callback = $jq(callback);
+      fb.callback.bind('keyup', fb.updateValue);
+      if (fb.callback.get(0).value) {
+        fb.setColor(fb.callback.get(0).value);
+      }
+    }
+    return this;
+  }
+  fb.updateValue = function (event) {
+    if (this.value && this.value != fb.color) {
+      fb.setColor(this.value);
+    }
+  }
+
+  /**
+   * Change color with HTML syntax #123456
+   */
+  fb.setColor = function (color) {
+    var unpack = fb.unpack(color);
+    if (fb.color != color && unpack) {
+      fb.color = color;
+      fb.rgb = unpack;
+      fb.hsl = fb.RGBToHSL(fb.rgb);
+      fb.updateDisplay();
+    }
+    return this;
+  }
+
+  /**
+   * Change color with HSL triplet [0..1, 0..1, 0..1]
+   */
+  fb.setHSL = function (hsl) {
+    fb.hsl = hsl;
+    fb.rgb = fb.HSLToRGB(hsl);
+    fb.color = fb.pack(fb.rgb);
+    fb.updateDisplay();
+    return this;
+  }
+
+  /////////////////////////////////////////////////////
+
+  /**
+   * Retrieve the coordinates of the given event relative to the center
+   * of the widget.
+   */
+  fb.widgetCoords = function (event) {
+    var x, y;
+    var el = event.target || event.srcElement;
+    var reference = fb.wheel;
+
+    if (typeof event.offsetX != 'undefined') {
+      // Use offset coordinates and find common offsetParent
+      var pos = { x: event.offsetX, y: event.offsetY };
+
+      // Send the coordinates upwards through the offsetParent chain.
+      var e = el;
+      while (e) {
+        e.mouseX = pos.x;
+        e.mouseY = pos.y;
+        pos.x += e.offsetLeft;
+        pos.y += e.offsetTop;
+        e = e.offsetParent;
+      }
+
+      // Look for the coordinates starting from the wheel widget.
+      var e = reference;
+      var offset = { x: 0, y: 0 }
+      while (e) {
+        if (typeof e.mouseX != 'undefined') {
+          x = e.mouseX - offset.x;
+          y = e.mouseY - offset.y;
+          break;
+        }
+        offset.x += e.offsetLeft;
+        offset.y += e.offsetTop;
+        e = e.offsetParent;
+      }
+
+      // Reset stored coordinates
+      e = el;
+      while (e) {
+        e.mouseX = undefined;
+        e.mouseY = undefined;
+        e = e.offsetParent;
+      }
+    }
+    else {
+      // Use absolute coordinates
+      var pos = fb.absolutePosition(reference);
+      x = (event.pageX || 0*(event.clientX + $jq('html').get(0).scrollLeft)) - pos.x;
+      y = (event.pageY || 0*(event.clientY + $jq('html').get(0).scrollTop)) - pos.y;
+    }
+    // Subtract distance to middle
+    return { x: x - fb.width / 2, y: y - fb.width / 2 };
+  }
+
+  /**
+   * Mousedown handler
+   */
+  fb.mousedown = function (event) {
+    // Capture mouse
+    if (!document.dragging) {
+      $jq(document).bind('mousemove', fb.mousemove).bind('mouseup', fb.mouseup);
+      document.dragging = true;
+    }
+
+    // Check which area is being dragged
+    var pos = fb.widgetCoords(event);
+    fb.circleDrag = Math.max(Math.abs(pos.x), Math.abs(pos.y)) * 2 > fb.square;
+
+    // Process
+    fb.mousemove(event);
+    return false;
+  }
+
+  /**
+   * Mousemove handler
+   */
+  fb.mousemove = function (event) {
+    // Get coordinates relative to color picker center
+    var pos = fb.widgetCoords(event);
+
+    // Set new HSL parameters
+    if (fb.circleDrag) {
+      var hue = Math.atan2(pos.x, -pos.y) / 6.28;
+      if (hue < 0) hue += 1;
+      fb.setHSL([hue, fb.hsl[1], fb.hsl[2]]);
+    }
+    else {
+      var sat = Math.max(0, Math.min(1, -(pos.x / fb.square) + .5));
+      var lum = Math.max(0, Math.min(1, -(pos.y / fb.square) + .5));
+      fb.setHSL([fb.hsl[0], sat, lum]);
+    }
+    return false;
+  }
+
+  /**
+   * Mouseup handler
+   */
+  fb.mouseup = function () {
+    // Uncapture mouse
+    $jq(document).unbind('mousemove', fb.mousemove);
+    $jq(document).unbind('mouseup', fb.mouseup);
+    document.dragging = false;
+  }
+
+  /**
+   * Update the markers and styles
+   */
+  fb.updateDisplay = function () {
+    // Markers
+    var angle = fb.hsl[0] * 6.28;
+    $jq('.h-marker', e).css({
+      left: Math.round(Math.sin(angle) * fb.radius + fb.width / 2) + 'px',
+      top: Math.round(-Math.cos(angle) * fb.radius + fb.width / 2) + 'px'
+    });
+
+    $jq('.sl-marker', e).css({
+      left: Math.round(fb.square * (.5 - fb.hsl[1]) + fb.width / 2) + 'px',
+      top: Math.round(fb.square * (.5 - fb.hsl[2]) + fb.width / 2) + 'px'
+    });
+
+    // Saturation/Luminance gradient
+    $jq('.color', e).css('backgroundColor', fb.pack(fb.HSLToRGB([fb.hsl[0], 1, 0.5])));
+
+    // Linked elements or callback
+    if (typeof fb.callback == 'object') {
+      // Set background/foreground color
+      $jq(fb.callback).css({
+        backgroundColor: fb.color,
+        color: fb.hsl[2] > 0.5 ? '#000' : '#fff'
+      });
+
+      // Change linked value
+      $jq(fb.callback).each(function() {
+        if (this.value && this.value != fb.color) {
+          this.value = fb.color;
+        }
+      });
+    }
+    else if (typeof fb.callback == 'function') {
+      fb.callback.call(fb, fb.color);
+    }
+  }
+
+  /**
+   * Get absolute position of element
+   */
+  fb.absolutePosition = function (el) {
+    var r = { x: el.offsetLeft, y: el.offsetTop };
+    // Resolve relative to offsetParent
+    if (el.offsetParent) {
+      var tmp = fb.absolutePosition(el.offsetParent);
+      r.x += tmp.x;
+      r.y += tmp.y;
+    }
+    return r;
+  };
+
+  /* Various color utility functions */
+  fb.pack = function (rgb) {
+    var r = Math.round(rgb[0] * 255);
+    var g = Math.round(rgb[1] * 255);
+    var b = Math.round(rgb[2] * 255);
+    return '#' + (r < 16 ? '0' : '') + r.toString(16) +
+           (g < 16 ? '0' : '') + g.toString(16) +
+           (b < 16 ? '0' : '') + b.toString(16);
+  }
+
+  fb.unpack = function (color) {
+    if (color.length == 7) {
+      return [parseInt('0x' + color.substring(1, 3)) / 255,
+        parseInt('0x' + color.substring(3, 5)) / 255,
+        parseInt('0x' + color.substring(5, 7)) / 255];
+    }
+    else if (color.length == 4) {
+      return [parseInt('0x' + color.substring(1, 2)) / 15,
+        parseInt('0x' + color.substring(2, 3)) / 15,
+        parseInt('0x' + color.substring(3, 4)) / 15];
+    }
+  }
+
+  fb.HSLToRGB = function (hsl) {
+    var m1, m2, r, g, b;
+    var h = hsl[0], s = hsl[1], l = hsl[2];
+    m2 = (l <= 0.5) ? l * (s + 1) : l + s - l*s;
+    m1 = l * 2 - m2;
+    return [this.hueToRGB(m1, m2, h+0.33333),
+        this.hueToRGB(m1, m2, h),
+        this.hueToRGB(m1, m2, h-0.33333)];
+  }
+
+  fb.hueToRGB = function (m1, m2, h) {
+    h = (h < 0) ? h + 1 : ((h > 1) ? h - 1 : h);
+    if (h * 6 < 1) return m1 + (m2 - m1) * h * 6;
+    if (h * 2 < 1) return m2;
+    if (h * 3 < 2) return m1 + (m2 - m1) * (0.66666 - h) * 6;
+    return m1;
+  }
+
+  fb.RGBToHSL = function (rgb) {
+    var min, max, delta, h, s, l;
+    var r = rgb[0], g = rgb[1], b = rgb[2];
+    min = Math.min(r, Math.min(g, b));
+    max = Math.max(r, Math.max(g, b));
+    delta = max - min;
+    l = (min + max) / 2;
+    s = 0;
+    if (l > 0 && l < 1) {
+      s = delta / (l < 0.5 ? (2 * l) : (2 - 2 * l));
+    }
+    h = 0;
+    if (delta > 0) {
+      if (max == r && max != g) h += (g - b) / delta;
+      if (max == g && max != b) h += (2 + (b - r) / delta);
+      if (max == b && max != r) h += (4 + (r - g) / delta);
+      h /= 6;
+    }
+    return [h, s, l];
+  }
+
+  // Install mousedown handler (the others are set on the document on-demand)
+  $jq('*', e).mousedown(fb.mousedown);
+
+    // Init color
+  fb.setColor('#000000');
+
+  // Set linked elements/callback
+  if (callback) {
+    fb.linkTo(callback);
+  }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/js/colorpick/jquery.js	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,2246 @@
+/* prevent execution of jQuery if included more than once */
+if(typeof window.jQuery == "undefined") {
+/*
+ * jQuery 1.1.2 - New Wave Javascript
+ *
+ * Copyright (c) 2007 John Resig (jquery.com)
+ * Dual licensed under the MIT (MIT-LICENSE.txt)
+ * and GPL (GPL-LICENSE.txt) licenses.
+ *
+ * $Date: 2007-02-28 12:03:00 -0500 (Wed, 28 Feb 2007) $
+ * $Rev: 1465 $
+ */
+
+// Global undefined variable
+window.undefined = window.undefined;
+var jQuery = function(a,c) {
+	// If the context is global, return a new object
+	if ( window == this )
+		return new jQuery(a,c);
+
+	// Make sure that a selection was provided
+	a = a || document;
+	
+	// HANDLE: $(function)
+	// Shortcut for document ready
+	if ( jQuery.isFunction(a) )
+		return new jQuery(document)[ jQuery.fn.ready ? "ready" : "load" ]( a );
+	
+	// Handle HTML strings
+	if ( typeof a  == "string" ) {
+		// HANDLE: $(html) -> $(array)
+		var m = /^[^<]*(<(.|\s)+>)[^>]*$/.exec(a);
+		if ( m )
+			a = jQuery.clean( [ m[1] ] );
+		
+		// HANDLE: $(expr)
+		else
+			return new jQuery( c ).find( a );
+	}
+	
+	return this.setArray(
+		// HANDLE: $(array)
+		a.constructor == Array && a ||
+
+		// HANDLE: $(arraylike)
+		// Watch for when an array-like object is passed as the selector
+		(a.jquery || a.length && a != window && !a.nodeType && a[0] != undefined && a[0].nodeType) && jQuery.makeArray( a ) ||
+
+		// HANDLE: $(*)
+		[ a ] );
+};
+
+// Map over the $ in case of overwrite
+if ( typeof $ != "undefined" )
+	jQuery._$ = $;
+	
+// Map the jQuery namespace to the '$' one
+var $jq = jQuery;
+
+jQuery.fn = jQuery.prototype = {
+	jquery: "1.1.2",
+
+	size: function() {
+		return this.length;
+	},
+	
+	length: 0,
+
+	get: function( num ) {
+		return num == undefined ?
+
+			// Return a 'clean' array
+			jQuery.makeArray( this ) :
+
+			// Return just the object
+			this[num];
+	},
+	pushStack: function( a ) {
+		var ret = jQuery(a);
+		ret.prevObject = this;
+		return ret;
+	},
+	setArray: function( a ) {
+		this.length = 0;
+		[].push.apply( this, a );
+		return this;
+	},
+	each: function( fn, args ) {
+		return jQuery.each( this, fn, args );
+	},
+	index: function( obj ) {
+		var pos = -1;
+		this.each(function(i){
+			if ( this == obj ) pos = i;
+		});
+		return pos;
+	},
+
+	attr: function( key, value, type ) {
+		var obj = key;
+		
+		// Look for the case where we're accessing a style value
+		if ( key.constructor == String )
+			if ( value == undefined )
+				return this.length && jQuery[ type || "attr" ]( this[0], key ) || undefined;
+			else {
+				obj = {};
+				obj[ key ] = value;
+			}
+		
+		// Check to see if we're setting style values
+		return this.each(function(index){
+			// Set all the styles
+			for ( var prop in obj )
+				jQuery.attr(
+					type ? this.style : this,
+					prop, jQuery.prop(this, obj[prop], type, index, prop)
+				);
+		});
+	},
+
+	css: function( key, value ) {
+		return this.attr( key, value, "curCSS" );
+	},
+
+	text: function(e) {
+		if ( typeof e == "string" )
+			return this.empty().append( document.createTextNode( e ) );
+
+		var t = "";
+		jQuery.each( e || this, function(){
+			jQuery.each( this.childNodes, function(){
+				if ( this.nodeType != 8 )
+					t += this.nodeType != 1 ?
+						this.nodeValue : jQuery.fn.text([ this ]);
+			});
+		});
+		return t;
+	},
+
+	wrap: function() {
+		// The elements to wrap the target around
+		var a = jQuery.clean(arguments);
+
+		// Wrap each of the matched elements individually
+		return this.each(function(){
+			// Clone the structure that we're using to wrap
+			var b = a[0].cloneNode(true);
+
+			// Insert it before the element to be wrapped
+			this.parentNode.insertBefore( b, this );
+
+			// Find the deepest point in the wrap structure
+			while ( b.firstChild )
+				b = b.firstChild;
+
+			// Move the matched element to within the wrap structure
+			b.appendChild( this );
+		});
+	},
+	append: function() {
+		return this.domManip(arguments, true, 1, function(a){
+			this.appendChild( a );
+		});
+	},
+	prepend: function() {
+		return this.domManip(arguments, true, -1, function(a){
+			this.insertBefore( a, this.firstChild );
+		});
+	},
+	before: function() {
+		return this.domManip(arguments, false, 1, function(a){
+			this.parentNode.insertBefore( a, this );
+		});
+	},
+	after: function() {
+		return this.domManip(arguments, false, -1, function(a){
+			this.parentNode.insertBefore( a, this.nextSibling );
+		});
+	},
+	end: function() {
+		return this.prevObject || jQuery([]);
+	},
+	find: function(t) {
+		return this.pushStack( jQuery.map( this, function(a){
+			return jQuery.find(t,a);
+		}), t );
+	},
+	clone: function(deep) {
+		return this.pushStack( jQuery.map( this, function(a){
+			var a = a.cloneNode( deep != undefined ? deep : true );
+			a.$events = null; // drop $events expando to avoid firing incorrect events
+			return a;
+		}) );
+	},
+
+	filter: function(t) {
+		return this.pushStack(
+			jQuery.isFunction( t ) &&
+			jQuery.grep(this, function(el, index){
+				return t.apply(el, [index])
+			}) ||
+
+			jQuery.multiFilter(t,this) );
+	},
+
+	not: function(t) {
+		return this.pushStack(
+			t.constructor == String &&
+			jQuery.multiFilter(t, this, true) ||
+
+			jQuery.grep(this, function(a) {
+				return ( t.constructor == Array || t.jquery )
+					? jQuery.inArray( a, t ) < 0
+					: a != t;
+			})
+		);
+	},
+
+	add: function(t) {
+		return this.pushStack( jQuery.merge(
+			this.get(),
+			t.constructor == String ?
+				jQuery(t).get() :
+				t.length != undefined && (!t.nodeName || t.nodeName == "FORM") ?
+					t : [t] )
+		);
+	},
+	is: function(expr) {
+		return expr ? jQuery.filter(expr,this).r.length > 0 : false;
+	},
+
+	val: function( val ) {
+		return val == undefined ?
+			( this.length ? this[0].value : null ) :
+			this.attr( "value", val );
+	},
+
+	html: function( val ) {
+		return val == undefined ?
+			( this.length ? this[0].innerHTML : null ) :
+			this.empty().append( val );
+	},
+	domManip: function(args, table, dir, fn){
+		var clone = this.length > 1; 
+		var a = jQuery.clean(args);
+		if ( dir < 0 )
+			a.reverse();
+
+		return this.each(function(){
+			var obj = this;
+
+			if ( table && jQuery.nodeName(this, "table") && jQuery.nodeName(a[0], "tr") )
+				obj = this.getElementsByTagName("tbody")[0] || this.appendChild(document.createElement("tbody"));
+
+			jQuery.each( a, function(){
+				fn.apply( obj, [ clone ? this.cloneNode(true) : this ] );
+			});
+
+		});
+	}
+};
+
+jQuery.extend = jQuery.fn.extend = function() {
+	// copy reference to target object
+	var target = arguments[0],
+		a = 1;
+
+	// extend jQuery itself if only one argument is passed
+	if ( arguments.length == 1 ) {
+		target = this;
+		a = 0;
+	}
+	var prop;
+	while (prop = arguments[a++])
+		// Extend the base object
+		for ( var i in prop ) target[i] = prop[i];
+
+	// Return the modified object
+	return target;
+};
+
+jQuery.extend({
+	noConflict: function() {
+		if ( jQuery._$ )
+			$ = jQuery._$;
+		return jQuery;
+	},
+
+	// This may seem like some crazy code, but trust me when I say that this
+	// is the only cross-browser way to do this. --John
+	isFunction: function( fn ) {
+		return !!fn && typeof fn != "string" && !fn.nodeName && 
+			typeof fn[0] == "undefined" && /function/i.test( fn + "" );
+	},
+	
+	// check if an element is in a XML document
+	isXMLDoc: function(elem) {
+		return elem.tagName && elem.ownerDocument && !elem.ownerDocument.body;
+	},
+
+	nodeName: function( elem, name ) {
+		return elem.nodeName && elem.nodeName.toUpperCase() == name.toUpperCase();
+	},
+	// args is for internal usage only
+	each: function( obj, fn, args ) {
+		if ( obj.length == undefined )
+			for ( var i in obj )
+				fn.apply( obj[i], args || [i, obj[i]] );
+		else
+			for ( var i = 0, ol = obj.length; i < ol; i++ )
+				if ( fn.apply( obj[i], args || [i, obj[i]] ) === false ) break;
+		return obj;
+	},
+	
+	prop: function(elem, value, type, index, prop){
+			// Handle executable functions
+			if ( jQuery.isFunction( value ) )
+				value = value.call( elem, [index] );
+				
+			// exclude the following css properties to add px
+			var exclude = /z-?index|font-?weight|opacity|zoom|line-?height/i;
+
+			// Handle passing in a number to a CSS property
+			return value && value.constructor == Number && type == "curCSS" && !exclude.test(prop) ?
+				value + "px" :
+				value;
+	},
+
+	className: {
+		// internal only, use addClass("class")
+		add: function( elem, c ){
+			jQuery.each( c.split(/\s+/), function(i, cur){
+				if ( !jQuery.className.has( elem.className, cur ) )
+					elem.className += ( elem.className ? " " : "" ) + cur;
+			});
+		},
+
+		// internal only, use removeClass("class")
+		remove: function( elem, c ){
+			elem.className = c ?
+				jQuery.grep( elem.className.split(/\s+/), function(cur){
+					return !jQuery.className.has( c, cur );	
+				}).join(" ") : "";
+		},
+
+		// internal only, use is(".class")
+		has: function( t, c ) {
+			t = t.className || t;
+			// escape regex characters
+			c = c.replace(/([\.\\\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
+			return t && new RegExp("(^|\\s)" + c + "(\\s|$)").test( t );
+		}
+	},
+	swap: function(e,o,f) {
+		for ( var i in o ) {
+			e.style["old"+i] = e.style[i];
+			e.style[i] = o[i];
+		}
+		f.apply( e, [] );
+		for ( var i in o )
+			e.style[i] = e.style["old"+i];
+	},
+
+	css: function(e,p) {
+		if ( p == "height" || p == "width" ) {
+			var old = {}, oHeight, oWidth, d = ["Top","Bottom","Right","Left"];
+
+			jQuery.each( d, function(){
+				old["padding" + this] = 0;
+				old["border" + this + "Width"] = 0;
+			});
+
+			jQuery.swap( e, old, function() {
+				if (jQuery.css(e,"display") != "none") {
+					oHeight = e.offsetHeight;
+					oWidth = e.offsetWidth;
+				} else {
+					e = jQuery(e.cloneNode(true))
+						.find(":radio").removeAttr("checked").end()
+						.css({
+							visibility: "hidden", position: "absolute", display: "block", right: "0", left: "0"
+						}).appendTo(e.parentNode)[0];
+
+					var parPos = jQuery.css(e.parentNode,"position");
+					if ( parPos == "" || parPos == "static" )
+						e.parentNode.style.position = "relative";
+
+					oHeight = e.clientHeight;
+					oWidth = e.clientWidth;
+
+					if ( parPos == "" || parPos == "static" )
+						e.parentNode.style.position = "static";
+
+					e.parentNode.removeChild(e);
+				}
+			});
+
+			return p == "height" ? oHeight : oWidth;
+		}
+
+		return jQuery.curCSS( e, p );
+	},
+
+	curCSS: function(elem, prop, force) {
+		var ret;
+		
+		if (prop == "opacity" && jQuery.browser.msie)
+			return jQuery.attr(elem.style, "opacity");
+			
+		if (prop == "float" || prop == "cssFloat")
+		    prop = jQuery.browser.msie ? "styleFloat" : "cssFloat";
+
+		if (!force && elem.style[prop])
+			ret = elem.style[prop];
+
+		else if (document.defaultView && document.defaultView.getComputedStyle) {
+
+			if (prop == "cssFloat" || prop == "styleFloat")
+				prop = "float";
+
+			prop = prop.replace(/([A-Z])/g,"-$1").toLowerCase();
+			var cur = document.defaultView.getComputedStyle(elem, null);
+
+			if ( cur )
+				ret = cur.getPropertyValue(prop);
+			else if ( prop == "display" )
+				ret = "none";
+			else
+				jQuery.swap(elem, { display: "block" }, function() {
+				    var c = document.defaultView.getComputedStyle(this, "");
+				    ret = c && c.getPropertyValue(prop) || "";
+				});
+
+		} else if (elem.currentStyle) {
+
+			var newProp = prop.replace(/\-(\w)/g,function(m,c){return c.toUpperCase();});
+			ret = elem.currentStyle[prop] || elem.currentStyle[newProp];
+			
+		}
+
+		return ret;
+	},
+	
+	clean: function(a) {
+		var r = [];
+
+		jQuery.each( a, function(i,arg){
+			if ( !arg ) return;
+
+			if ( arg.constructor == Number )
+				arg = arg.toString();
+			
+			 // Convert html string into DOM nodes
+			if ( typeof arg == "string" ) {
+				// Trim whitespace, otherwise indexOf won't work as expected
+				var s = jQuery.trim(arg), div = document.createElement("div"), tb = [];
+
+				var wrap =
+					 // option or optgroup
+					!s.indexOf("<opt") &&
+					[1, "<select>", "</select>"] ||
+					
+					(!s.indexOf("<thead") || !s.indexOf("<tbody") || !s.indexOf("<tfoot")) &&
+					[1, "<table>", "</table>"] ||
+					
+					!s.indexOf("<tr") &&
+					[2, "<table><tbody>", "</tbody></table>"] ||
+					
+				 	// <thead> matched above
+					(!s.indexOf("<td") || !s.indexOf("<th")) &&
+					[3, "<table><tbody><tr>", "</tr></tbody></table>"] ||
+					
+					[0,"",""];
+
+				// Go to html and back, then peel off extra wrappers
+				div.innerHTML = wrap[1] + s + wrap[2];
+				
+				// Move to the right depth
+				while ( wrap[0]-- )
+					div = div.firstChild;
+				
+				// Remove IE's autoinserted <tbody> from table fragments
+				if ( jQuery.browser.msie ) {
+					
+					// String was a <table>, *may* have spurious <tbody>
+					if ( !s.indexOf("<table") && s.indexOf("<tbody") < 0 ) 
+						tb = div.firstChild && div.firstChild.childNodes;
+						
+					// String was a bare <thead> or <tfoot>
+					else if ( wrap[1] == "<table>" && s.indexOf("<tbody") < 0 )
+						tb = div.childNodes;
+
+					for ( var n = tb.length-1; n >= 0 ; --n )
+						if ( jQuery.nodeName(tb[n], "tbody") && !tb[n].childNodes.length )
+							tb[n].parentNode.removeChild(tb[n]);
+					
+				}
+				
+				arg = [];
+				for (var i=0, l=div.childNodes.length; i<l; i++)
+					arg.push(div.childNodes[i]);
+			}
+
+			if ( arg.length === 0 && !jQuery.nodeName(arg, "form") )
+				return;
+			
+			if ( arg[0] == undefined || jQuery.nodeName(arg, "form") )
+				r.push( arg );
+			else
+				r = jQuery.merge( r, arg );
+
+		});
+
+		return r;
+	},
+	
+	attr: function(elem, name, value){
+		var fix = jQuery.isXMLDoc(elem) ? {} : {
+			"for": "htmlFor",
+			"class": "className",
+			"float": jQuery.browser.msie ? "styleFloat" : "cssFloat",
+			cssFloat: jQuery.browser.msie ? "styleFloat" : "cssFloat",
+			innerHTML: "innerHTML",
+			className: "className",
+			value: "value",
+			disabled: "disabled",
+			checked: "checked",
+			readonly: "readOnly",
+			selected: "selected"
+		};
+		
+		// IE actually uses filters for opacity ... elem is actually elem.style
+		if ( name == "opacity" && jQuery.browser.msie && value != undefined ) {
+			// IE has trouble with opacity if it does not have layout
+			// Force it by setting the zoom level
+			elem.zoom = 1; 
+
+			// Set the alpha filter to set the opacity
+			return elem.filter = elem.filter.replace(/alpha\([^\)]*\)/gi,"") +
+				( value == 1 ? "" : "alpha(opacity=" + value * 100 + ")" );
+
+		} else if ( name == "opacity" && jQuery.browser.msie )
+			return elem.filter ? 
+				parseFloat( elem.filter.match(/alpha\(opacity=(.*)\)/)[1] ) / 100 : 1;
+		
+		// Mozilla doesn't play well with opacity 1
+		if ( name == "opacity" && jQuery.browser.mozilla && value == 1 )
+			value = 0.9999;
+			
+
+		// Certain attributes only work when accessed via the old DOM 0 way
+		if ( fix[name] ) {
+			if ( value != undefined ) elem[fix[name]] = value;
+			return elem[fix[name]];
+
+		} else if ( value == undefined && jQuery.browser.msie && jQuery.nodeName(elem, "form") && (name == "action" || name == "method") )
+			return elem.getAttributeNode(name).nodeValue;
+
+		// IE elem.getAttribute passes even for style
+		else if ( elem.tagName ) {
+			if ( value != undefined ) elem.setAttribute( name, value );
+			if ( jQuery.browser.msie && /href|src/.test(name) && !jQuery.isXMLDoc(elem) ) 
+				return elem.getAttribute( name, 2 );
+			return elem.getAttribute( name );
+
+		// elem is actually elem.style ... set the style
+		} else {
+			name = name.replace(/-([a-z])/ig,function(z,b){return b.toUpperCase();});
+			if ( value != undefined ) elem[name] = value;
+			return elem[name];
+		}
+	},
+	trim: function(t){
+		return t.replace(/^\s+|\s+$/g, "");
+	},
+
+	makeArray: function( a ) {
+		var r = [];
+
+		if ( a.constructor != Array )
+			for ( var i = 0, al = a.length; i < al; i++ )
+				r.push( a[i] );
+		else
+			r = a.slice( 0 );
+
+		return r;
+	},
+
+	inArray: function( b, a ) {
+		for ( var i = 0, al = a.length; i < al; i++ )
+			if ( a[i] == b )
+				return i;
+		return -1;
+	},
+	merge: function(first, second) {
+		var r = [].slice.call( first, 0 );
+
+		// Now check for duplicates between the two arrays
+		// and only add the unique items
+		for ( var i = 0, sl = second.length; i < sl; i++ )
+			// Check for duplicates
+			if ( jQuery.inArray( second[i], r ) == -1 )
+				// The item is unique, add it
+				first.push( second[i] );
+
+		return first;
+	},
+	grep: function(elems, fn, inv) {
+		// If a string is passed in for the function, make a function
+		// for it (a handy shortcut)
+		if ( typeof fn == "string" )
+			fn = new Function("a","i","return " + fn);
+
+		var result = [];
+
+		// Go through the array, only saving the items
+		// that pass the validator function
+		for ( var i = 0, el = elems.length; i < el; i++ )
+			if ( !inv && fn(elems[i],i) || inv && !fn(elems[i],i) )
+				result.push( elems[i] );
+
+		return result;
+	},
+	map: function(elems, fn) {
+		// If a string is passed in for the function, make a function
+		// for it (a handy shortcut)
+		if ( typeof fn == "string" )
+			fn = new Function("a","return " + fn);
+
+		var result = [], r = [];
+
+		// Go through the array, translating each of the items to their
+		// new value (or values).
+		for ( var i = 0, el = elems.length; i < el; i++ ) {
+			var val = fn(elems[i],i);
+
+			if ( val !== null && val != undefined ) {
+				if ( val.constructor != Array ) val = [val];
+				result = result.concat( val );
+			}
+		}
+
+		var r = result.length ? [ result[0] ] : [];
+
+		check: for ( var i = 1, rl = result.length; i < rl; i++ ) {
+			for ( var j = 0; j < i; j++ )
+				if ( result[i] == r[j] )
+					continue check;
+
+			r.push( result[i] );
+		}
+
+		return r;
+	}
+});
+ 
+/*
+ * Whether the W3C compliant box model is being used.
+ *
+ * @property
+ * @name $.boxModel
+ * @type Boolean
+ * @cat JavaScript
+ */
+new function() {
+	var b = navigator.userAgent.toLowerCase();
+
+	// Figure out what browser is being used
+	jQuery.browser = {
+		safari: /webkit/.test(b),
+		opera: /opera/.test(b),
+		msie: /msie/.test(b) && !/opera/.test(b),
+		mozilla: /mozilla/.test(b) && !/(compatible|webkit)/.test(b)
+	};
+
+	// Check to see if the W3C box model is being used
+	jQuery.boxModel = !jQuery.browser.msie || document.compatMode == "CSS1Compat";
+};
+
+jQuery.each({
+	parent: "a.parentNode",
+	parents: "jQuery.parents(a)",
+	next: "jQuery.nth(a,2,'nextSibling')",
+	prev: "jQuery.nth(a,2,'previousSibling')",
+	siblings: "jQuery.sibling(a.parentNode.firstChild,a)",
+	children: "jQuery.sibling(a.firstChild)"
+}, function(i,n){
+	jQuery.fn[ i ] = function(a) {
+		var ret = jQuery.map(this,n);
+		if ( a && typeof a == "string" )
+			ret = jQuery.multiFilter(a,ret);
+		return this.pushStack( ret );
+	};
+});
+
+jQuery.each({
+	appendTo: "append",
+	prependTo: "prepend",
+	insertBefore: "before",
+	insertAfter: "after"
+}, function(i,n){
+	jQuery.fn[ i ] = function(){
+		var a = arguments;
+		return this.each(function(){
+			for ( var j = 0, al = a.length; j < al; j++ )
+				jQuery(a[j])[n]( this );
+		});
+	};
+});
+
+jQuery.each( {
+	removeAttr: function( key ) {
+		jQuery.attr( this, key, "" );
+		this.removeAttribute( key );
+	},
+	addClass: function(c){
+		jQuery.className.add(this,c);
+	},
+	removeClass: function(c){
+		jQuery.className.remove(this,c);
+	},
+	toggleClass: function( c ){
+		jQuery.className[ jQuery.className.has(this,c) ? "remove" : "add" ](this, c);
+	},
+	remove: function(a){
+		if ( !a || jQuery.filter( a, [this] ).r.length )
+			this.parentNode.removeChild( this );
+	},
+	empty: function() {
+		while ( this.firstChild )
+			this.removeChild( this.firstChild );
+	}
+}, function(i,n){
+	jQuery.fn[ i ] = function() {
+		return this.each( n, arguments );
+	};
+});
+
+jQuery.each( [ "eq", "lt", "gt", "contains" ], function(i,n){
+	jQuery.fn[ n ] = function(num,fn) {
+		return this.filter( ":" + n + "(" + num + ")", fn );
+	};
+});
+
+jQuery.each( [ "height", "width" ], function(i,n){
+	jQuery.fn[ n ] = function(h) {
+		return h == undefined ?
+			( this.length ? jQuery.css( this[0], n ) : null ) :
+			this.css( n, h.constructor == String ? h : h + "px" );
+	};
+});
+jQuery.extend({
+	expr: {
+		"": "m[2]=='*'||jQuery.nodeName(a,m[2])",
+		"#": "a.getAttribute('id')==m[2]",
+		":": {
+			// Position Checks
+			lt: "i<m[3]-0",
+			gt: "i>m[3]-0",
+			nth: "m[3]-0==i",
+			eq: "m[3]-0==i",
+			first: "i==0",
+			last: "i==r.length-1",
+			even: "i%2==0",
+			odd: "i%2",
+
+			// Child Checks
+			"nth-child": "jQuery.nth(a.parentNode.firstChild,m[3],'nextSibling',a)==a",
+			"first-child": "jQuery.nth(a.parentNode.firstChild,1,'nextSibling')==a",
+			"last-child": "jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a",
+			"only-child": "jQuery.sibling(a.parentNode.firstChild).length==1",
+
+			// Parent Checks
+			parent: "a.firstChild",
+			empty: "!a.firstChild",
+
+			// Text Check
+			contains: "jQuery.fn.text.apply([a]).indexOf(m[3])>=0",
+
+			// Visibility
+			visible: 'a.type!="hidden"&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',
+			hidden: 'a.type=="hidden"||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',
+
+			// Form attributes
+			enabled: "!a.disabled",
+			disabled: "a.disabled",
+			checked: "a.checked",
+			selected: "a.selected||jQuery.attr(a,'selected')",
+
+			// Form elements
+			text: "a.type=='text'",
+			radio: "a.type=='radio'",
+			checkbox: "a.type=='checkbox'",
+			file: "a.type=='file'",
+			password: "a.type=='password'",
+			submit: "a.type=='submit'",
+			image: "a.type=='image'",
+			reset: "a.type=='reset'",
+			button: 'a.type=="button"||jQuery.nodeName(a,"button")',
+			input: "/input|select|textarea|button/i.test(a.nodeName)"
+		},
+		".": "jQuery.className.has(a,m[2])",
+		"@": {
+			"=": "z==m[4]",
+			"!=": "z!=m[4]",
+			"^=": "z&&!z.indexOf(m[4])",
+			"$=": "z&&z.substr(z.length - m[4].length,m[4].length)==m[4]",
+			"*=": "z&&z.indexOf(m[4])>=0",
+			"": "z",
+			_resort: function(m){
+				return ["", m[1], m[3], m[2], m[5]];
+			},
+			_prefix: "z=a[m[3]];if(!z||/href|src/.test(m[3]))z=jQuery.attr(a,m[3]);"
+		},
+		"[": "jQuery.find(m[2],a).length"
+	},
+	
+	// The regular expressions that power the parsing engine
+	parse: [
+		// Match: [@value='test'], [@foo]
+		/^\[ *(@)([a-z0-9_-]*) *([!*$^=]*) *('?"?)(.*?)\4 *\]/i,
+
+		// Match: [div], [div p]
+		/^(\[)\s*(.*?(\[.*?\])?[^[]*?)\s*\]/,
+
+		// Match: :contains('foo')
+		/^(:)([a-z0-9_-]*)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/i,
+
+		// Match: :even, :last-chlid
+		/^([:.#]*)([a-z0-9_*-]*)/i
+	],
+
+	token: [
+		/^(\/?\.\.)/, "a.parentNode",
+		/^(>|\/)/, "jQuery.sibling(a.firstChild)",
+		/^(\+)/, "jQuery.nth(a,2,'nextSibling')",
+		/^(~)/, function(a){
+			var s = jQuery.sibling(a.parentNode.firstChild);
+			return s.slice(jQuery.inArray(a,s) + 1);
+		}
+	],
+
+	multiFilter: function( expr, elems, not ) {
+		var old, cur = [];
+
+		while ( expr && expr != old ) {
+			old = expr;
+			var f = jQuery.filter( expr, elems, not );
+			expr = f.t.replace(/^\s*,\s*/, "" );
+			cur = not ? elems = f.r : jQuery.merge( cur, f.r );
+		}
+
+		return cur;
+	},
+	find: function( t, context ) {
+		// Quickly handle non-string expressions
+		if ( typeof t != "string" )
+			return [ t ];
+
+		// Make sure that the context is a DOM Element
+		if ( context && !context.nodeType )
+			context = null;
+
+		// Set the correct context (if none is provided)
+		context = context || document;
+
+		// Handle the common XPath // expression
+		if ( !t.indexOf("//") ) {
+			context = context.documentElement;
+			t = t.substr(2,t.length);
+
+		// And the / root expression
+		} else if ( !t.indexOf("/") ) {
+			context = context.documentElement;
+			t = t.substr(1,t.length);
+			if ( t.indexOf("/") >= 1 )
+				t = t.substr(t.indexOf("/"),t.length);
+		}
+
+		// Initialize the search
+		var ret = [context], done = [], last = null;
+
+		// Continue while a selector expression exists, and while
+		// we're no longer looping upon ourselves
+		while ( t && last != t ) {
+			var r = [];
+			last = t;
+
+			t = jQuery.trim(t).replace( /^\/\//i, "" );
+
+			var foundToken = false;
+
+			// An attempt at speeding up child selectors that
+			// point to a specific element tag
+			var re = /^[\/>]\s*([a-z0-9*-]+)/i;
+			var m = re.exec(t);
+
+			if ( m ) {
+				// Perform our own iteration and filter
+				jQuery.each( ret, function(){
+					for ( var c = this.firstChild; c; c = c.nextSibling )
+						if ( c.nodeType == 1 && ( jQuery.nodeName(c, m[1]) || m[1] == "*" ) )
+							r.push( c );
+				});
+
+				ret = r;
+				t = t.replace( re, "" );
+				if ( t.indexOf(" ") == 0 ) continue;
+				foundToken = true;
+			} else {
+				// Look for pre-defined expression tokens
+				for ( var i = 0; i < jQuery.token.length; i += 2 ) {
+					// Attempt to match each, individual, token in
+					// the specified order
+					var re = jQuery.token[i];
+					var m = re.exec(t);
+
+					// If the token match was found
+					if ( m ) {
+						// Map it against the token's handler
+						r = ret = jQuery.map( ret, jQuery.isFunction( jQuery.token[i+1] ) ?
+							jQuery.token[i+1] :
+							function(a){ return eval(jQuery.token[i+1]); });
+
+						// And remove the token
+						t = jQuery.trim( t.replace( re, "" ) );
+						foundToken = true;
+						break;
+					}
+				}
+			}
+
+			// See if there's still an expression, and that we haven't already
+			// matched a token
+			if ( t && !foundToken ) {
+				// Handle multiple expressions
+				if ( !t.indexOf(",") ) {
+					// Clean the result set
+					if ( ret[0] == context ) ret.shift();
+
+					// Merge the result sets
+					jQuery.merge( done, ret );
+
+					// Reset the context
+					r = ret = [context];
+
+					// Touch up the selector string
+					t = " " + t.substr(1,t.length);
+
+				} else {
+					// Optomize for the case nodeName#idName
+					var re2 = /^([a-z0-9_-]+)(#)([a-z0-9\\*_-]*)/i;
+					var m = re2.exec(t);
+					
+					// Re-organize the results, so that they're consistent
+					if ( m ) {
+					   m = [ 0, m[2], m[3], m[1] ];
+
+					} else {
+						// Otherwise, do a traditional filter check for
+						// ID, class, and element selectors
+						re2 = /^([#.]?)([a-z0-9\\*_-]*)/i;
+						m = re2.exec(t);
+					}
+
+					// Try to do a global search by ID, where we can
+					if ( m[1] == "#" && ret[ret.length-1].getElementById ) {
+						// Optimization for HTML document case
+						var oid = ret[ret.length-1].getElementById(m[2]);
+						
+						// Do a quick check for the existence of the actual ID attribute
+						// to avoid selecting by the name attribute in IE
+						if ( jQuery.browser.msie && oid && oid.id != m[2] )
+							oid = jQuery('[@id="'+m[2]+'"]', ret[ret.length-1])[0];
+
+						// Do a quick check for node name (where applicable) so
+						// that div#foo searches will be really fast
+						ret = r = oid && (!m[3] || jQuery.nodeName(oid, m[3])) ? [oid] : [];
+
+					} else {
+						// Pre-compile a regular expression to handle class searches
+						if ( m[1] == "." )
+							var rec = new RegExp("(^|\\s)" + m[2] + "(\\s|$)");
+
+						// We need to find all descendant elements, it is more
+						// efficient to use getAll() when we are already further down
+						// the tree - we try to recognize that here
+						jQuery.each( ret, function(){
+							// Grab the tag name being searched for
+							var tag = m[1] != "" || m[0] == "" ? "*" : m[2];
+
+							// Handle IE7 being really dumb about <object>s
+							if ( jQuery.nodeName(this, "object") && tag == "*" )
+								tag = "param";
+
+							jQuery.merge( r,
+								m[1] != "" && ret.length != 1 ?
+									jQuery.getAll( this, [], m[1], m[2], rec ) :
+									this.getElementsByTagName( tag )
+							);
+						});
+
+						// It's faster to filter by class and be done with it
+						if ( m[1] == "." && ret.length == 1 )
+							r = jQuery.grep( r, function(e) {
+								return rec.test(e.className);
+							});
+
+						// Same with ID filtering
+						if ( m[1] == "#" && ret.length == 1 ) {
+							// Remember, then wipe out, the result set
+							var tmp = r;
+							r = [];
+
+							// Then try to find the element with the ID
+							jQuery.each( tmp, function(){
+								if ( this.getAttribute("id") == m[2] ) {
+									r = [ this ];
+									return false;
+								}
+							});
+						}
+
+						ret = r;
+					}
+
+					t = t.replace( re2, "" );
+				}
+
+			}
+
+			// If a selector string still exists
+			if ( t ) {
+				// Attempt to filter it
+				var val = jQuery.filter(t,r);
+				ret = r = val.r;
+				t = jQuery.trim(val.t);
+			}
+		}
+
+		// Remove the root context
+		if ( ret && ret[0] == context ) ret.shift();
+
+		// And combine the results
+		jQuery.merge( done, ret );
+
+		return done;
+	},
+
+	filter: function(t,r,not) {
+		// Look for common filter expressions
+		while ( t && /^[a-z[({<*:.#]/i.test(t) ) {
+
+			var p = jQuery.parse, m;
+
+			jQuery.each( p, function(i,re){
+		
+				// Look for, and replace, string-like sequences
+				// and finally build a regexp out of it
+				m = re.exec( t );
+
+				if ( m ) {
+					// Remove what we just matched
+					t = t.substring( m[0].length );
+
+					// Re-organize the first match
+					if ( jQuery.expr[ m[1] ]._resort )
+						m = jQuery.expr[ m[1] ]._resort( m );
+
+					return false;
+				}
+			});
+
+			// :not() is a special case that can be optimized by
+			// keeping it out of the expression list
+			if ( m[1] == ":" && m[2] == "not" )
+				r = jQuery.filter(m[3], r, true).r;
+
+			// Handle classes as a special case (this will help to
+			// improve the speed, as the regexp will only be compiled once)
+			else if ( m[1] == "." ) {
+
+				var re = new RegExp("(^|\\s)" + m[2] + "(\\s|$)");
+				r = jQuery.grep( r, function(e){
+					return re.test(e.className || "");
+				}, not);
+
+			// Otherwise, find the expression to execute
+			} else {
+				var f = jQuery.expr[m[1]];
+				if ( typeof f != "string" )
+					f = jQuery.expr[m[1]][m[2]];
+
+				// Build a custom macro to enclose it
+				eval("f = function(a,i){" +
+					( jQuery.expr[ m[1] ]._prefix || "" ) +
+					"return " + f + "}");
+
+				// Execute it against the current filter
+				r = jQuery.grep( r, f, not );
+			}
+		}
+
+		// Return an array of filtered elements (r)
+		// and the modified expression string (t)
+		return { r: r, t: t };
+	},
+	
+	getAll: function( o, r, token, name, re ) {
+		for ( var s = o.firstChild; s; s = s.nextSibling )
+			if ( s.nodeType == 1 ) {
+				var add = true;
+
+				if ( token == "." )
+					add = s.className && re.test(s.className);
+				else if ( token == "#" )
+					add = s.getAttribute("id") == name;
+	
+				if ( add )
+					r.push( s );
+
+				if ( token == "#" && r.length ) break;
+
+				if ( s.firstChild )
+					jQuery.getAll( s, r, token, name, re );
+			}
+
+		return r;
+	},
+	parents: function( elem ){
+		var matched = [];
+		var cur = elem.parentNode;
+		while ( cur && cur != document ) {
+			matched.push( cur );
+			cur = cur.parentNode;
+		}
+		return matched;
+	},
+	nth: function(cur,result,dir,elem){
+		result = result || 1;
+		var num = 0;
+		for ( ; cur; cur = cur[dir] ) {
+			if ( cur.nodeType == 1 ) num++;
+			if ( num == result || result == "even" && num % 2 == 0 && num > 1 && cur == elem ||
+				result == "odd" && num % 2 == 1 && cur == elem ) return cur;
+		}
+	},
+	sibling: function( n, elem ) {
+		var r = [];
+
+		for ( ; n; n = n.nextSibling ) {
+			if ( n.nodeType == 1 && (!elem || n != elem) )
+				r.push( n );
+		}
+
+		return r;
+	}
+});
+/*
+ * A number of helper functions used for managing events.
+ * Many of the ideas behind this code orignated from 
+ * Dean Edwards' addEvent library.
+ */
+jQuery.event = {
+
+	// Bind an event to an element
+	// Original by Dean Edwards
+	add: function(element, type, handler, data) {
+		// For whatever reason, IE has trouble passing the window object
+		// around, causing it to be cloned in the process
+		if ( jQuery.browser.msie && element.setInterval != undefined )
+			element = window;
+
+		// if data is passed, bind to handler
+		if( data ) 
+			handler.data = data;
+
+		// Make sure that the function being executed has a unique ID
+		if ( !handler.guid )
+			handler.guid = this.guid++;
+
+		// Init the element's event structure
+		if (!element.$events)
+			element.$events = {};
+
+		// Get the current list of functions bound to this event
+		var handlers = element.$events[type];
+
+		// If it hasn't been initialized yet
+		if (!handlers) {
+			// Init the event handler queue
+			handlers = element.$events[type] = {};
+
+			// Remember an existing handler, if it's already there
+			if (element["on" + type])
+				handlers[0] = element["on" + type];
+		}
+
+		// Add the function to the element's handler list
+		handlers[handler.guid] = handler;
+
+		// And bind the global event handler to the element
+		element["on" + type] = this.handle;
+
+		// Remember the function in a global list (for triggering)
+		if (!this.global[type])
+			this.global[type] = [];
+		this.global[type].push( element );
+	},
+
+	guid: 1,
+	global: {},
+
+	// Detach an event or set of events from an element
+	remove: function(element, type, handler) {
+		if (element.$events) {
+			var i,j,k;
+			if ( type && type.type ) { // type is actually an event object here
+				handler = type.handler;
+				type    = type.type;
+			}
+			
+			if (type && element.$events[type])
+				// remove the given handler for the given type
+				if ( handler )
+					delete element.$events[type][handler.guid];
+					
+				// remove all handlers for the given type
+				else
+					for ( i in element.$events[type] )
+						delete element.$events[type][i];
+						
+			// remove all handlers		
+			else
+				for ( j in element.$events )
+					this.remove( element, j );
+			
+			// remove event handler if no more handlers exist
+			for ( k in element.$events[type] )
+				if (k) {
+					k = true;
+					break;
+				}
+			if (!k) element["on" + type] = null;
+		}
+	},
+
+	trigger: function(type, data, element) {
+		// Clone the incoming data, if any
+		data = jQuery.makeArray(data || []);
+
+		// Handle a global trigger
+		if ( !element )
+			jQuery.each( this.global[type] || [], function(){
+				jQuery.event.trigger( type, data, this );
+			});
+
+		// Handle triggering a single element
+		else {
+			var handler = element["on" + type ], val,
+				fn = jQuery.isFunction( element[ type ] );
+
+			if ( handler ) {
+				// Pass along a fake event
+				data.unshift( this.fix({ type: type, target: element }) );
+	
+				// Trigger the event
+				if ( (val = handler.apply( element, data )) !== false )
+					this.triggered = true;
+			}
+
+			if ( fn && val !== false )
+				element[ type ]();
+
+			this.triggered = false;
+		}
+	},
+
+	handle: function(event) {
+		// Handle the second event of a trigger and when
+		// an event is called after a page has unloaded
+		if ( typeof jQuery == "undefined" || jQuery.event.triggered ) return;
+
+		// Empty object is for triggered events with no data
+		event = jQuery.event.fix( event || window.event || {} ); 
+
+		// returned undefined or false
+		var returnValue;
+
+		var c = this.$events[event.type];
+
+		var args = [].slice.call( arguments, 1 );
+		args.unshift( event );
+
+		for ( var j in c ) {
+			// Pass in a reference to the handler function itself
+			// So that we can later remove it
+			args[0].handler = c[j];
+			args[0].data = c[j].data;
+
+			if ( c[j].apply( this, args ) === false ) {
+				event.preventDefault();
+				event.stopPropagation();
+				returnValue = false;
+			}
+		}
+
+		// Clean up added properties in IE to prevent memory leak
+		if (jQuery.browser.msie) event.target = event.preventDefault = event.stopPropagation = event.handler = event.data = null;
+
+		return returnValue;
+	},
+
+	fix: function(event) {
+		// Fix target property, if necessary
+		if ( !event.target && event.srcElement )
+			event.target = event.srcElement;
+
+		// Calculate pageX/Y if missing and clientX/Y available
+		if ( event.pageX == undefined && event.clientX != undefined ) {
+			var e = document.documentElement, b = document.body;
+			event.pageX = event.clientX + (e.scrollLeft || b.scrollLeft);
+			event.pageY = event.clientY + (e.scrollTop || b.scrollTop);
+		}
+				
+		// check if target is a textnode (safari)
+		if (jQuery.browser.safari && event.target.nodeType == 3) {
+			// store a copy of the original event object 
+			// and clone because target is read only
+			var originalEvent = event;
+			event = jQuery.extend({}, originalEvent);
+			
+			// get parentnode from textnode
+			event.target = originalEvent.target.parentNode;
+			
+			// add preventDefault and stopPropagation since 
+			// they will not work on the clone
+			event.preventDefault = function() {
+				return originalEvent.preventDefault();
+			};
+			event.stopPropagation = function() {
+				return originalEvent.stopPropagation();
+			};
+		}
+		
+		// fix preventDefault and stopPropagation
+		if (!event.preventDefault)
+			event.preventDefault = function() {
+				this.returnValue = false;
+			};
+			
+		if (!event.stopPropagation)
+			event.stopPropagation = function() {
+				this.cancelBubble = true;
+			};
+			
+		return event;
+	}
+};
+
+jQuery.fn.extend({
+	bind: function( type, data, fn ) {
+		return this.each(function(){
+			jQuery.event.add( this, type, fn || data, data );
+		});
+	},
+	one: function( type, data, fn ) {
+		return this.each(function(){
+			jQuery.event.add( this, type, function(event) {
+				jQuery(this).unbind(event);
+				return (fn || data).apply( this, arguments);
+			}, data);
+		});
+	},
+	unbind: function( type, fn ) {
+		return this.each(function(){
+			jQuery.event.remove( this, type, fn );
+		});
+	},
+	trigger: function( type, data ) {
+		return this.each(function(){
+			jQuery.event.trigger( type, data, this );
+		});
+	},
+	toggle: function() {
+		// Save reference to arguments for access in closure
+		var a = arguments;
+
+		return this.click(function(e) {
+			// Figure out which function to execute
+			this.lastToggle = this.lastToggle == 0 ? 1 : 0;
+			
+			// Make sure that clicks stop
+			e.preventDefault();
+			
+			// and execute the function
+			return a[this.lastToggle].apply( this, [e] ) || false;
+		});
+	},
+	hover: function(f,g) {
+		
+		// A private function for handling mouse 'hovering'
+		function handleHover(e) {
+			// Check if mouse(over|out) are still within the same parent element
+			var p = (e.type == "mouseover" ? e.fromElement : e.toElement) || e.relatedTarget;
+	
+			// Traverse up the tree
+			while ( p && p != this ) try { p = p.parentNode } catch(e) { p = this; };
+			
+			// If we actually just moused on to a sub-element, ignore it
+			if ( p == this ) return false;
+			
+			// Execute the right function
+			return (e.type == "mouseover" ? f : g).apply(this, [e]);
+		}
+		
+		// Bind the function to the two event listeners
+		return this.mouseover(handleHover).mouseout(handleHover);
+	},
+	ready: function(f) {
+		// If the DOM is already ready
+		if ( jQuery.isReady )
+			// Execute the function immediately
+			f.apply( document, [jQuery] );
+			
+		// Otherwise, remember the function for later
+		else {
+			// Add the function to the wait list
+			jQuery.readyList.push( function() { return f.apply(this, [jQuery]) } );
+		}
+	
+		return this;
+	}
+});
+
+jQuery.extend({
+	/*
+	 * All the code that makes DOM Ready work nicely.
+	 */
+	isReady: false,
+	readyList: [],
+	
+	// Handle when the DOM is ready
+	ready: function() {
+		// Make sure that the DOM is not already loaded
+		if ( !jQuery.isReady ) {
+			// Remember that the DOM is ready
+			jQuery.isReady = true;
+			
+			// If there are functions bound, to execute
+			if ( jQuery.readyList ) {
+				// Execute all of them
+				jQuery.each( jQuery.readyList, function(){
+					this.apply( document );
+				});
+				
+				// Reset the list of functions
+				jQuery.readyList = null;
+			}
+			// Remove event lisenter to avoid memory leak
+			if ( jQuery.browser.mozilla || jQuery.browser.opera )
+				document.removeEventListener( "DOMContentLoaded", jQuery.ready, false );
+		}
+	}
+});
+
+new function(){
+
+	jQuery.each( ("blur,focus,load,resize,scroll,unload,click,dblclick," +
+		"mousedown,mouseup,mousemove,mouseover,mouseout,change,select," + 
+		"submit,keydown,keypress,keyup,error").split(","), function(i,o){
+		
+		// Handle event binding
+		jQuery.fn[o] = function(f){
+			return f ? this.bind(o, f) : this.trigger(o);
+		};
+			
+	});
+	
+	// If Mozilla is used
+	if ( jQuery.browser.mozilla || jQuery.browser.opera )
+		// Use the handy event callback
+		document.addEventListener( "DOMContentLoaded", jQuery.ready, false );
+	
+	// If IE is used, use the excellent hack by Matthias Miller
+	// http://www.outofhanwell.com/blog/index.php?title=the_window_onload_problem_revisited
+	else if ( jQuery.browser.msie ) {
+	
+		// Only works if you document.write() it
+		document.write("<scr" + "ipt id=__ie_init defer=true " + 
+			"src=//:><\/script>");
+	
+		// Use the defer script hack
+		var script = document.getElementById("__ie_init");
+		
+		// script does not exist if jQuery is loaded dynamically
+		if ( script ) 
+			script.onreadystatechange = function() {
+				if ( this.readyState != "complete" ) return;
+				this.parentNode.removeChild( this );
+				jQuery.ready();
+			};
+	
+		// Clear from memory
+		script = null;
+	
+	// If Safari  is used
+	} else if ( jQuery.browser.safari )
+		// Continually check to see if the document.readyState is valid
+		jQuery.safariTimer = setInterval(function(){
+			// loaded and complete are both valid states
+			if ( document.readyState == "loaded" || 
+				document.readyState == "complete" ) {
+	
+				// If either one are found, remove the timer
+				clearInterval( jQuery.safariTimer );
+				jQuery.safariTimer = null;
+	
+				// and execute any waiting functions
+				jQuery.ready();
+			}
+		}, 10); 
+
+	// A fallback to window.onload, that will always work
+	jQuery.event.add( window, "load", jQuery.ready );
+	
+};
+
+// Clean up after IE to avoid memory leaks
+if (jQuery.browser.msie)
+	jQuery(window).one("unload", function() {
+		var global = jQuery.event.global;
+		for ( var type in global ) {
+			var els = global[type], i = els.length;
+			if ( i && type != 'unload' )
+				do
+					jQuery.event.remove(els[i-1], type);
+				while (--i);
+		}
+	});
+jQuery.fn.extend({
+	loadIfModified: function( url, params, callback ) {
+		this.load( url, params, callback, 1 );
+	},
+	load: function( url, params, callback, ifModified ) {
+		if ( jQuery.isFunction( url ) )
+			return this.bind("load", url);
+
+		callback = callback || function(){};
+
+		// Default to a GET request
+		var type = "GET";
+
+		// If the second parameter was provided
+		if ( params )
+			// If it's a function
+			if ( jQuery.isFunction( params ) ) {
+				// We assume that it's the callback
+				callback = params;
+				params = null;
+
+			// Otherwise, build a param string
+			} else {
+				params = jQuery.param( params );
+				type = "POST";
+			}
+
+		var self = this;
+
+		// Request the remote document
+		jQuery.ajax({
+			url: url,
+			type: type,
+			data: params,
+			ifModified: ifModified,
+			complete: function(res, status){
+				if ( status == "success" || !ifModified && status == "notmodified" )
+					// Inject the HTML into all the matched elements
+					self.attr("innerHTML", res.responseText)
+					  // Execute all the scripts inside of the newly-injected HTML
+					  .evalScripts()
+					  // Execute callback
+					  .each( callback, [res.responseText, status, res] );
+				else
+					callback.apply( self, [res.responseText, status, res] );
+			}
+		});
+		return this;
+	},
+	serialize: function() {
+		return jQuery.param( this );
+	},
+	evalScripts: function() {
+		return this.find("script").each(function(){
+			if ( this.src )
+				jQuery.getScript( this.src );
+			else
+				jQuery.globalEval( this.text || this.textContent || this.innerHTML || "" );
+		}).end();
+	}
+
+});
+
+// If IE is used, create a wrapper for the XMLHttpRequest object
+if ( !window.XMLHttpRequest )
+	XMLHttpRequest = function(){
+		return new ActiveXObject("Microsoft.XMLHTTP");
+	};
+
+// Attach a bunch of functions for handling common AJAX events
+
+jQuery.each( "ajaxStart,ajaxStop,ajaxComplete,ajaxError,ajaxSuccess,ajaxSend".split(","), function(i,o){
+	jQuery.fn[o] = function(f){
+		return this.bind(o, f);
+	};
+});
+
+jQuery.extend({
+	get: function( url, data, callback, type, ifModified ) {
+		// shift arguments if data argument was ommited
+		if ( jQuery.isFunction( data ) ) {
+			callback = data;
+			data = null;
+		}
+		
+		return jQuery.ajax({
+			url: url,
+			data: data,
+			success: callback,
+			dataType: type,
+			ifModified: ifModified
+		});
+	},
+	getIfModified: function( url, data, callback, type ) {
+		return jQuery.get(url, data, callback, type, 1);
+	},
+	getScript: function( url, callback ) {
+		return jQuery.get(url, null, callback, "script");
+	},
+	getJSON: function( url, data, callback ) {
+		return jQuery.get(url, data, callback, "json");
+	},
+	post: function( url, data, callback, type ) {
+		if ( jQuery.isFunction( data ) ) {
+			callback = data;
+			data = {};
+		}
+
+		return jQuery.ajax({
+			type: "POST",
+			url: url,
+			data: data,
+			success: callback,
+			dataType: type
+		});
+	},
+
+	// timeout (ms)
+	//timeout: 0,
+	ajaxTimeout: function( timeout ) {
+		jQuery.ajaxSettings.timeout = timeout;
+	},
+	ajaxSetup: function( settings ) {
+		jQuery.extend( jQuery.ajaxSettings, settings );
+	},
+
+	ajaxSettings: {
+		global: true,
+		type: "GET",
+		timeout: 0,
+		contentType: "application/x-www-form-urlencoded",
+		processData: true,
+		async: true,
+		data: null
+	},
+	
+	// Last-Modified header cache for next request
+	lastModified: {},
+	ajax: function( s ) {
+		// TODO introduce global settings, allowing the client to modify them for all requests, not only timeout
+		s = jQuery.extend({}, jQuery.ajaxSettings, s);
+
+		// if data available
+		if ( s.data ) {
+			// convert data if not already a string
+			if (s.processData && typeof s.data != "string")
+    			s.data = jQuery.param(s.data);
+			// append data to url for get requests
+			if( s.type.toLowerCase() == "get" ) {
+				// "?" + data or "&" + data (in case there are already params)
+				s.url += ((s.url.indexOf("?") > -1) ? "&" : "?") + s.data;
+				// IE likes to send both get and post data, prevent this
+				s.data = null;
+			}
+		}
+
+		// Watch for a new set of requests
+		if ( s.global && ! jQuery.active++ )
+			jQuery.event.trigger( "ajaxStart" );
+
+		var requestDone = false;
+
+		// Create the request object
+		var xml = new XMLHttpRequest();
+
+		// Open the socket
+		xml.open(s.type, s.url, s.async);
+
+		// Set the correct header, if data is being sent
+		if ( s.data )
+			xml.setRequestHeader("Content-Type", s.contentType);
+
+		// Set the If-Modified-Since header, if ifModified mode.
+		if ( s.ifModified )
+			xml.setRequestHeader("If-Modified-Since",
+				jQuery.lastModified[s.url] || "Thu, 01 Jan 1970 00:00:00 GMT" );
+
+		// Set header so the called script knows that it's an XMLHttpRequest
+		xml.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+
+		// Make sure the browser sends the right content length
+		if ( xml.overrideMimeType )
+			xml.setRequestHeader("Connection", "close");
+			
+		// Allow custom headers/mimetypes
+		if( s.beforeSend )
+			s.beforeSend(xml);
+			
+		if ( s.global )
+		    jQuery.event.trigger("ajaxSend", [xml, s]);
+
+		// Wait for a response to come back
+		var onreadystatechange = function(isTimeout){
+			// The transfer is complete and the data is available, or the request timed out
+			if ( xml && (xml.readyState == 4 || isTimeout == "timeout") ) {
+				requestDone = true;
+				
+				// clear poll interval
+				if (ival) {
+					clearInterval(ival);
+					ival = null;
+				}
+				
+				var status;
+				try {
+					status = jQuery.httpSuccess( xml ) && isTimeout != "timeout" ?
+						s.ifModified && jQuery.httpNotModified( xml, s.url ) ? "notmodified" : "success" : "error";
+					// Make sure that the request was successful or notmodified
+					if ( status != "error" ) {
+						// Cache Last-Modified header, if ifModified mode.
+						var modRes;
+						try {
+							modRes = xml.getResponseHeader("Last-Modified");
+						} catch(e) {} // swallow exception thrown by FF if header is not available
+	
+						if ( s.ifModified && modRes )
+							jQuery.lastModified[s.url] = modRes;
+	
+						// process the data (runs the xml through httpData regardless of callback)
+						var data = jQuery.httpData( xml, s.dataType );
+	
+						// If a local callback was specified, fire it and pass it the data
+						if ( s.success )
+							s.success( data, status );
+	
+						// Fire the global callback
+						if( s.global )
+							jQuery.event.trigger( "ajaxSuccess", [xml, s] );
+					} else
+						jQuery.handleError(s, xml, status);
+				} catch(e) {
+					status = "error";
+					jQuery.handleError(s, xml, status, e);
+				}
+
+				// The request was completed
+				if( s.global )
+					jQuery.event.trigger( "ajaxComplete", [xml, s] );
+
+				// Handle the global AJAX counter
+				if ( s.global && ! --jQuery.active )
+					jQuery.event.trigger( "ajaxStop" );
+
+				// Process result
+				if ( s.complete )
+					s.complete(xml, status);
+
+				// Stop memory leaks
+				if(s.async)
+					xml = null;
+			}
+		};
+		
+		// don't attach the handler to the request, just poll it instead
+		var ival = setInterval(onreadystatechange, 13); 
+
+		// Timeout checker
+		if ( s.timeout > 0 )
+			setTimeout(function(){
+				// Check to see if the request is still happening
+				if ( xml ) {
+					// Cancel the request
+					xml.abort();
+
+					if( !requestDone )
+						onreadystatechange( "timeout" );
+				}
+			}, s.timeout);
+			
+		// Send the data
+		try {
+			xml.send(s.data);
+		} catch(e) {
+			jQuery.handleError(s, xml, null, e);
+		}
+		
+		// firefox 1.5 doesn't fire statechange for sync requests
+		if ( !s.async )
+			onreadystatechange();
+		
+		// return XMLHttpRequest to allow aborting the request etc.
+		return xml;
+	},
+
+	handleError: function( s, xml, status, e ) {
+		// If a local callback was specified, fire it
+		if ( s.error ) s.error( xml, status, e );
+
+		// Fire the global callback
+		if ( s.global )
+			jQuery.event.trigger( "ajaxError", [xml, s, e] );
+	},
+
+	// Counter for holding the number of active queries
+	active: 0,
+
+	// Determines if an XMLHttpRequest was successful or not
+	httpSuccess: function( r ) {
+		try {
+			return !r.status && location.protocol == "file:" ||
+				( r.status >= 200 && r.status < 300 ) || r.status == 304 ||
+				jQuery.browser.safari && r.status == undefined;
+		} catch(e){}
+		return false;
+	},
+
+	// Determines if an XMLHttpRequest returns NotModified
+	httpNotModified: function( xml, url ) {
+		try {
+			var xmlRes = xml.getResponseHeader("Last-Modified");
+
+			// Firefox always returns 200. check Last-Modified date
+			return xml.status == 304 || xmlRes == jQuery.lastModified[url] ||
+				jQuery.browser.safari && xml.status == undefined;
+		} catch(e){}
+		return false;
+	},
+
+	/* Get the data out of an XMLHttpRequest.
+	 * Return parsed XML if content-type header is "xml" and type is "xml" or omitted,
+	 * otherwise return plain text.
+	 * (String) data - The type of data that you're expecting back,
+	 * (e.g. "xml", "html", "script")
+	 */
+	httpData: function( r, type ) {
+		var ct = r.getResponseHeader("content-type");
+		var data = !type && ct && ct.indexOf("xml") >= 0;
+		data = type == "xml" || data ? r.responseXML : r.responseText;
+
+		// If the type is "script", eval it in global context
+		if ( type == "script" )
+			jQuery.globalEval( data );
+
+		// Get the JavaScript object, if JSON is used.
+		if ( type == "json" )
+			eval( "data = " + data );
+
+		// evaluate scripts within html
+		if ( type == "html" )
+			jQuery("<div>").html(data).evalScripts();
+
+		return data;
+	},
+
+	// Serialize an array of form elements or a set of
+	// key/values into a query string
+	param: function( a ) {
+		var s = [];
+
+		// If an array was passed in, assume that it is an array
+		// of form elements
+		if ( a.constructor == Array || a.jquery )
+			// Serialize the form elements
+			jQuery.each( a, function(){
+				s.push( encodeURIComponent(this.name) + "=" + encodeURIComponent( this.value ) );
+			});
+
+		// Otherwise, assume that it's an object of key/value pairs
+		else
+			// Serialize the key/values
+			for ( var j in a )
+				// If the value is an array then the key names need to be repeated
+				if ( a[j] && a[j].constructor == Array )
+					jQuery.each( a[j], function(){
+						s.push( encodeURIComponent(j) + "=" + encodeURIComponent( this ) );
+					});
+				else
+					s.push( encodeURIComponent(j) + "=" + encodeURIComponent( a[j] ) );
+
+		// Return the resulting serialization
+		return s.join("&");
+	},
+	
+	// evalulates a script in global context
+	// not reliable for safari
+	globalEval: function( data ) {
+		if ( window.execScript )
+			window.execScript( data );
+		else if ( jQuery.browser.safari )
+			// safari doesn't provide a synchronous global eval
+			window.setTimeout( data, 0 );
+		else
+			eval.call( window, data );
+	}
+
+});
+jQuery.fn.extend({
+
+	show: function(speed,callback){
+		var hidden = this.filter(":hidden");
+		speed ?
+			hidden.animate({
+				height: "show", width: "show", opacity: "show"
+			}, speed, callback) :
+			
+			hidden.each(function(){
+				this.style.display = this.oldblock ? this.oldblock : "";
+				if ( jQuery.css(this,"display") == "none" )
+					this.style.display = "block";
+			});
+		return this;
+	},
+
+	hide: function(speed,callback){
+		var visible = this.filter(":visible");
+		speed ?
+			visible.animate({
+				height: "hide", width: "hide", opacity: "hide"
+			}, speed, callback) :
+			
+			visible.each(function(){
+				this.oldblock = this.oldblock || jQuery.css(this,"display");
+				if ( this.oldblock == "none" )
+					this.oldblock = "block";
+				this.style.display = "none";
+			});
+		return this;
+	},
+
+	// Save the old toggle function
+	_toggle: jQuery.fn.toggle,
+	toggle: function( fn, fn2 ){
+		var args = arguments;
+		return jQuery.isFunction(fn) && jQuery.isFunction(fn2) ?
+			this._toggle( fn, fn2 ) :
+			this.each(function(){
+				jQuery(this)[ jQuery(this).is(":hidden") ? "show" : "hide" ]
+					.apply( jQuery(this), args );
+			});
+	},
+	slideDown: function(speed,callback){
+		return this.animate({height: "show"}, speed, callback);
+	},
+	slideUp: function(speed,callback){
+		return this.animate({height: "hide"}, speed, callback);
+	},
+	slideToggle: function(speed, callback){
+		return this.each(function(){
+			var state = jQuery(this).is(":hidden") ? "show" : "hide";
+			jQuery(this).animate({height: state}, speed, callback);
+		});
+	},
+	fadeIn: function(speed, callback){
+		return this.animate({opacity: "show"}, speed, callback);
+	},
+	fadeOut: function(speed, callback){
+		return this.animate({opacity: "hide"}, speed, callback);
+	},
+	fadeTo: function(speed,to,callback){
+		return this.animate({opacity: to}, speed, callback);
+	},
+	animate: function( prop, speed, easing, callback ) {
+		return this.queue(function(){
+		
+			this.curAnim = jQuery.extend({}, prop);
+			var opt = jQuery.speed(speed, easing, callback);
+			
+			for ( var p in prop ) {
+				var e = new jQuery.fx( this, opt, p );
+				if ( prop[p].constructor == Number )
+					e.custom( e.cur(), prop[p] );
+				else
+					e[ prop[p] ]( prop );
+			}
+			
+		});
+	},
+	queue: function(type,fn){
+		if ( !fn ) {
+			fn = type;
+			type = "fx";
+		}
+	
+		return this.each(function(){
+			if ( !this.queue )
+				this.queue = {};
+	
+			if ( !this.queue[type] )
+				this.queue[type] = [];
+	
+			this.queue[type].push( fn );
+		
+			if ( this.queue[type].length == 1 )
+				fn.apply(this);
+		});
+	}
+
+});
+
+jQuery.extend({
+	
+	speed: function(speed, easing, fn) {
+		var opt = speed && speed.constructor == Object ? speed : {
+			complete: fn || !fn && easing || 
+				jQuery.isFunction( speed ) && speed,
+			duration: speed,
+			easing: fn && easing || easing && easing.constructor != Function && easing
+		};
+
+		opt.duration = (opt.duration && opt.duration.constructor == Number ? 
+			opt.duration : 
+			{ slow: 600, fast: 200 }[opt.duration]) || 400;
+	
+		// Queueing
+		opt.old = opt.complete;
+		opt.complete = function(){
+			jQuery.dequeue(this, "fx");
+			if ( jQuery.isFunction( opt.old ) )
+				opt.old.apply( this );
+		};
+	
+		return opt;
+	},
+	
+	easing: {},
+	
+	queue: {},
+	
+	dequeue: function(elem,type){
+		type = type || "fx";
+	
+		if ( elem.queue && elem.queue[type] ) {
+			// Remove self
+			elem.queue[type].shift();
+	
+			// Get next function
+			var f = elem.queue[type][0];
+		
+			if ( f ) f.apply( elem );
+		}
+	},
+
+	/*
+	 * I originally wrote fx() as a clone of moo.fx and in the process
+	 * of making it small in size the code became illegible to sane
+	 * people. You've been warned.
+	 */
+	
+	fx: function( elem, options, prop ){
+
+		var z = this;
+
+		// The styles
+		var y = elem.style;
+		
+		// Store display property
+		var oldDisplay = jQuery.css(elem, "display");
+
+		// Make sure that nothing sneaks out
+		y.overflow = "hidden";
+
+		// Simple function for setting a style value
+		z.a = function(){
+			if ( options.step )
+				options.step.apply( elem, [ z.now ] );
+
+			if ( prop == "opacity" )
+				jQuery.attr(y, "opacity", z.now); // Let attr handle opacity
+			else if ( parseInt(z.now) ) // My hate for IE will never die
+				y[prop] = parseInt(z.now) + "px";
+			
+			y.display = "block"; // Set display property to block for animation
+		};
+
+		// Figure out the maximum number to run to
+		z.max = function(){
+			return parseFloat( jQuery.css(elem,prop) );
+		};
+
+		// Get the current size
+		z.cur = function(){
+			var r = parseFloat( jQuery.curCSS(elem, prop) );
+			return r && r > -10000 ? r : z.max();
+		};
+
+		// Start an animation from one number to another
+		z.custom = function(from,to){
+			z.startTime = (new Date()).getTime();
+			z.now = from;
+			z.a();
+
+			z.timer = setInterval(function(){
+				z.step(from, to);
+			}, 13);
+		};
+
+		// Simple 'show' function
+		z.show = function(){
+			if ( !elem.orig ) elem.orig = {};
+
+			// Remember where we started, so that we can go back to it later
+			elem.orig[prop] = this.cur();
+
+			options.show = true;
+
+			// Begin the animation
+			z.custom(0, elem.orig[prop]);
+
+			// Stupid IE, look what you made me do
+			if ( prop != "opacity" )
+				y[prop] = "1px";
+		};
+
+		// Simple 'hide' function
+		z.hide = function(){
+			if ( !elem.orig ) elem.orig = {};
+
+			// Remember where we started, so that we can go back to it later
+			elem.orig[prop] = this.cur();
+
+			options.hide = true;
+
+			// Begin the animation
+			z.custom(elem.orig[prop], 0);
+		};
+		
+		//Simple 'toggle' function
+		z.toggle = function() {
+			if ( !elem.orig ) elem.orig = {};
+
+			// Remember where we started, so that we can go back to it later
+			elem.orig[prop] = this.cur();
+
+			if(oldDisplay == "none")  {
+				options.show = true;
+				
+				// Stupid IE, look what you made me do
+				if ( prop != "opacity" )
+					y[prop] = "1px";
+
+				// Begin the animation
+				z.custom(0, elem.orig[prop]);	
+			} else {
+				options.hide = true;
+
+				// Begin the animation
+				z.custom(elem.orig[prop], 0);
+			}		
+		};
+
+		// Each step of an animation
+		z.step = function(firstNum, lastNum){
+			var t = (new Date()).getTime();
+
+			if (t > options.duration + z.startTime) {
+				// Stop the timer
+				clearInterval(z.timer);
+				z.timer = null;
+
+				z.now = lastNum;
+				z.a();
+
+				if (elem.curAnim) elem.curAnim[ prop ] = true;
+
+				var done = true;
+				for ( var i in elem.curAnim )
+					if ( elem.curAnim[i] !== true )
+						done = false;
+
+				if ( done ) {
+					// Reset the overflow
+					y.overflow = "";
+					
+					// Reset the display
+					y.display = oldDisplay;
+					if (jQuery.css(elem, "display") == "none")
+						y.display = "block";
+
+					// Hide the element if the "hide" operation was done
+					if ( options.hide ) 
+						y.display = "none";
+
+					// Reset the properties, if the item has been hidden or shown
+					if ( options.hide || options.show )
+						for ( var p in elem.curAnim )
+							if (p == "opacity")
+								jQuery.attr(y, p, elem.orig[p]);
+							else
+								y[p] = "";
+				}
+
+				// If a callback was provided, execute it
+				if ( done && jQuery.isFunction( options.complete ) )
+					// Execute the complete function
+					options.complete.apply( elem );
+			} else {
+				var n = t - this.startTime;
+				// Figure out where in the animation we are and set the number
+				var p = n / options.duration;
+				
+				// If the easing function exists, then use it 
+				z.now = options.easing && jQuery.easing[options.easing] ?
+					jQuery.easing[options.easing](p, n,  firstNum, (lastNum-firstNum), options.duration) :
+					// else use default linear easing
+					((-Math.cos(p*Math.PI)/2) + 0.5) * (lastNum-firstNum) + firstNum;
+
+				// Perform the next step of the animation
+				z.a();
+			}
+		};
+	
+	}
+});
+}
+
Binary file decir/js/colorpick/marker.png has changed
Binary file decir/js/colorpick/mask.png has changed
Binary file decir/js/colorpick/wheel.png has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/notes.sql	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,43 @@
+SELECT f.forum_id,f.forum_type,f.forum_name,f.forum_desc,
+       COUNT(t.topic_id) AS num_topics, COUNT(p.post_id) AS num_posts,
+       p.post_id,t.topic_id,t.topic_title,u.username,u.user_level,p.timestamp FROM decir_forums AS f
+  LEFT JOIN decir_topics AS t
+    ON (t.forum_id=f.forum_id)
+  LEFT JOIN decir_posts AS p
+    ON (p.topic_id=t.topic_id)
+  LEFT JOIN users AS u
+    ON (u.user_id=f.last_post_user)
+  WHERE ( t.topic_id=f.last_post_topic AND p.post_id=f.last_post_id ) OR ( f.last_post_topic IS NULL AND f.last_post_id IS NULL )
+    GROUP BY f.parent,f.forum_id
+    ORDER BY f.forum_order;
+    
+SELECT COUNT(t.topic_id) AS num_topics, COUNT(p.post_id) AS num_posts FROM decir_forums AS f
+  LEFT JOIN decir_topics AS t
+    ON (t.forum_id=f.forum_id)
+  LEFT JOIN decir_posts AS p
+    ON (p.topic_id=t.topic_id)
+  GROUP BY f.forum_id
+  ORDER BY f.forum_order;
+
+INSERT INTO decir_forums(forum_id,forum_type,forum_name,forum_order) VALUES(1,2,'Test category',1);
+INSERT INTO decir_forums(forum_id,forum_type,forum_name,forum_desc,parent,forum_order,last_post_id,last_post_topic,last_post_user) VALUES(3,1,'Test forum 1','This is just a test forum.',1,2,3,3,2);
+INSERT INTO decir_topics(topic_id,forum_id,topic_title,topic_icon,topic_starter,timestamp) VALUES(1,3,'Test topic 1',1,2,UNIX_TIMESTAMP());
+INSERT INTO decir_posts(post_id,topic_id,poster_id,poster_name,timestamp) VALUES(1,1,2,'Dan',UNIX_TIMESTAMP());
+INSERT INTO decir_posts_text(post_id,post_text,bbcode_uid) VALUES(1,'This post was created manually using SQL queries.
+It is nothing more than a [b:0123456789]proof of concept[/b:0123456789]!
+
+-Dan','0123456789');
+INSERT INTO decir_forums(forum_id,forum_type,forum_name,forum_desc,parent,forum_order,last_post_id,last_post_topic,last_post_user) VALUES(4,1,'Test forum 2','This is just a test forum.',1,3,2,2,2);
+INSERT INTO decir_topics(topic_id,forum_id,topic_title,topic_icon,topic_starter,timestamp) VALUES(2,4,'Test topic 2',1,2,UNIX_TIMESTAMP());
+INSERT INTO decir_posts(post_id,topic_id,poster_id,poster_name,timestamp) VALUES(2,2,2,'Dan',UNIX_TIMESTAMP());
+INSERT INTO decir_posts_text(post_id,post_text,bbcode_uid) VALUES(2,'This post was created manually using SQL queries.
+It is nothing more than a [b:0123456789]proof of concept[/b:0123456789]!
+
+-Dan','0123456789');
+INSERT INTO decir_topics(topic_id,forum_id,topic_title,topic_icon,topic_starter,timestamp) VALUES(3,3,'Test topic 3',1,2,UNIX_TIMESTAMP());
+INSERT INTO decir_posts(post_id,topic_id,poster_id,poster_name,timestamp) VALUES(3,3,2,'Dan',UNIX_TIMESTAMP());
+INSERT INTO decir_posts_text(post_id,post_text,bbcode_uid) VALUES(3,'This post was created manually using SQL queries.
+It is nothing more than a [b:0123456789]proof of concept[/b:0123456789]!
+
+-Dan','0123456789');
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/posting.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,239 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * posting.php - post topics and replies
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+require('common.php');
+require('bbcode.php');
+
+//
+// Set mode and parameters
+//
+
+$mode = 'topic';
+
+if ( $paths->getParam(1) )
+{
+  $n = strtolower($paths->getParam(1));
+  if ( $n == 'reply' || $n == 'post' )
+  {
+    $mode = 'reply';
+  }
+  elseif ( $n == 'quote' )
+  {
+    $mode = 'quote';
+  }
+}
+
+// Set the parameters for posting, then encrypt it so we don't have to do authorization checks again
+// Why? Because it's better than going through some session system for postings where the data is stored on the server
+// We already have AES encryption - might as well use it ;-)
+$aes = new AESCrypt(AES_BITS, AES_BLOCKSIZE);
+
+$do_preview = false;
+
+if ( isset($_GET['act']) && $_GET['act'] == 'post' )
+{
+  if ( !is_array($_POST['do']) )
+    die('Hacking attempt');
+  
+  if ( isset($_POST['do']['preview']) )
+  {
+    $do_preview = true;
+    $parms  = $_POST['authorization'];
+    $parms2 = $aes->decrypt($parms, $session->private_key, ENC_HEX);
+    if ( !$parms2 || substr($parms2, 0, 1) != 'a' )
+    {
+      die('Hacking attempt: ' . $parms2);
+    }
+    $parms2 = unserialize($parms2);
+    $mode = 'already_taken_care_of';
+  }
+  else if ( isset($_POST['do']['post']) )
+  {
+    // Decrypt authorization array
+    $parms = $aes->decrypt($_POST['authorization'], $session->private_key, ENC_HEX);
+    $parms = unserialize($parms);
+    
+    // Perform a little input validation
+    $errors = Array();
+    if ( empty($_POST['post_text']) )
+      $errors[] = 'Please enter a post.';
+    if ( empty($_POST['subject']) && $parms['mode'] == 'topic' )
+      $errors[] = 'Please enter a topic title.';
+    // It's OK to trust this! The auth key is encrypted with the site's private key.
+    if ( !$parms['authorized'] )
+      $errors[] = 'Invalid authorization key';
+    
+    if ( sizeof($errors) > 0 )
+    {
+      // Collect other options
+      
+      // Submit post
+      decir_submit_post();
+      return;
+    }
+  }
+}
+
+if ( $mode == 'reply' || $mode == 'quote' )
+{
+  if ( $mode == 'reply' )
+  {
+    $message = '';
+    // Validate topic ID
+    $topic_id = intval($paths->getParam(2));
+    if ( empty($topic_id) )
+      die_friendly('Error', '<p>Invalid topic ID</p>');
+    $title = 'Reply to topic';
+  }
+  else if ( $mode == 'quote' )
+  {
+    
+    /**
+     * @TODO: validate read permissions
+     */
+    
+    $post_id = intval($paths->getParam(2));
+    if ( empty($post_id) )
+      die_friendly('Error', '<p>Invalid post ID</p>');
+    
+    // Get post text and topic ID
+    $q = $db->sql_query('SELECT p.topic_id,t.post_text,t.bbcode_uid,p.poster_name FROM '.table_prefix.'decir_posts AS p
+                           LEFT JOIN '.table_prefix.'decir_posts_text AS t
+                             ON ( p.post_id = t.post_id )
+                           WHERE p.post_id=' . $post_id . ';');
+    
+    if ( !$q )
+      $db->_die();
+    
+    if ( $db->numrows() < 1 )
+      die_friendly('Error', '<p>The post you requested does not exist.</p>');
+    
+    $row = $db->fetchrow();
+    $db->free_result();
+    
+    $message = '[quote="' . $row['poster_name'] . '"]' . bbcode_strip_uid( $row['post_text'], $row['bbcode_uid'] ) . '[/quote]';
+    $quote_poster = $row['poster_name'];
+    $topic_id = intval($row['topic_id']);
+    
+    $title = 'Reply to topic with quote';
+    
+  }
+  
+  // Topic ID is good, verify topic status
+  $q = $db->sql_query('SELECT topic_id,forum_id,topic_type,topic_locked,topic_moved FROM '.table_prefix.'decir_topics WHERE topic_id=' . $topic_id . ';');
+  
+  if ( !$q )
+    $db->_die();
+  
+  $row = $db->fetchrow();
+  $db->free_result();
+  
+  $forum_perms = $session->fetch_page_acl('DecirForum', $row['forum_id']);
+  $topic_perms = $session->fetch_page_acl('DecirTopic', $row['topic_id']);
+  
+  if ( !$forum_perms->get_permissions('decir_see_forum') )
+    die_friendly('Error', '<p>The forum you requested does not exist.</p>');
+  
+  if ( !$topic_perms->get_permissions('decir_reply') )
+    die_friendly('Access denied', '<p>You are not allowed to post replies in this topic.</p>');
+  
+  $forum_in = intval($row['forum_id']);
+  $topic_in = intval($row['topic_id']);
+  
+  $parms = Array(
+      'mode' => $mode,
+      'forum_in' => $forum_in,
+      'topic_in' => $topic_in,
+      'timestamp' => time(),
+      'authorized' => true
+    );
+  
+  $parms = serialize($parms);
+  $parms = $aes->encrypt($parms, $session->private_key, ENC_HEX);
+  
+}
+else if ( $mode == 'topic' )
+{
+  $message = '';
+  // Validate topic ID
+  $forum_id = intval($paths->getParam(2));
+  if ( empty($forum_id) )
+    die_friendly('Error', '<p>Invalid forum ID</p>');
+  $title = 'Post new topic';
+  
+  // Topic ID is good, verify topic status
+  $q = $db->sql_query('SELECT forum_id FROM '.table_prefix.'decir_forums WHERE forum_id=' . $forum_id . ';');
+  
+  if ( !$q )
+    $db->_die();
+  
+  if ( $db->numrows() < 1 )
+    die_friendly('Error', '<p>The forum you requested does not exist.</p>');
+  
+  $row = $db->fetchrow();
+  $db->free_result();
+  
+  $forum_perms = $session->fetch_page_acl('DecirForum', $row['forum_id']);
+  
+  if ( !$forum_perms->get_permissions('decir_see_forum') )
+    die_friendly('Error', '<p>The forum you requested does not exist.</p>');
+  
+  $parms = Array(
+      'mode' => $mode,
+      'forum_in' => $forum_in,
+      'timestamp' => time(),
+      'authorized' => true
+    );
+  
+  $parms = serialize($parms);
+  $parms = $aes->encrypt($parms, $session->private_key, ENC_HEX);
+  
+}
+else if ( $mode == 'already_taken_care_of' )
+{
+  $mode = $parms2['mode'];
+  $title = ( $mode == 'topic' ) ? 'Post new topic' : ( $mode == 'reply' ) ? 'Reply to topic' : ( $mode  == 'quote' ) ? 'Reply to topic with quote' : 'Duh...';
+}
+else
+{
+  die_friendly('Invalid request', '<p>Invalid action defined</p>');
+}
+
+$template->tpl_strings['PAGE_NAME'] = $title;
+$template->add_header('<!-- DECIR BEGIN -->
+    <script type="text/javascript" src="' . scriptPath . '/decir/js/bbcedit.js"></script>
+    <script type="text/javascript" src="' . scriptPath . '/decir/js/colorpick/jquery.js"></script>
+    <script type="text/javascript" src="' . scriptPath . '/decir/js/colorpick/farbtastic.js"></script>
+    <link rel="stylesheet" type="text/css" href="' . scriptPath . '/decir/js/bbcedit.css" />
+    <link rel="stylesheet" type="text/css" href="' . scriptPath . '/decir/js/colorpick/farbtastic.css" />
+    <!-- DECIR END -->');
+
+$template->header();
+
+if ( $do_preview )
+{
+  echo 'Doing preview';
+}
+
+$url = makeUrlNS('Special', 'Forum/New', 'act=post', true);
+echo '<br />
+      <form action="' . $url . '" method="post" enctype="multipart/form-data">';
+echo '<textarea name="post_text" class="bbcode" rows="20" cols="80">' . $message . '</textarea>';
+echo '<input type="hidden" name="authorization" value="' . $parms . '" />';
+echo '<div style="text-align: center; margin-top: 10px;"><input type="submit" name="do[post]" value="Submit post" style="font-weight: bold;" />&nbsp;<input type="submit" name="do[preview]" value="Show preview" /></div>';
+echo '</form>';
+
+$template->footer();
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/viewforum.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,85 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * install.php - Database installation wizard
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+require('common.php');
+
+$template->header();
+
+$fid = ( $n = $paths->getParam(1) ) ? $n : ( ( isset($_GET['fid']) ) ? $_GET['fid'] : 0 );
+$fid = intval($fid);
+
+if(empty($fid))
+{
+  echo '<p>Invalid forum ID</p>';
+  $template->footer();
+  return;
+}
+
+$perms = $session->fetch_page_acl((string)$fid, 'DecirForum');
+if ( !$perms->get_permissions('decir_view_forum') )
+{
+  die_friendly('Access denied', '<p>You are not authorized to view this forum.</p>');
+}
+
+$sort_column = ( isset($_GET['sort_column']) && in_array($_GET['sort_column'], array('t.timestamp', 't.topic_title')) ) ? $_GET['sort_column'] : 't.timestamp';
+$sort_dir    = ( isset($_GET['sort_dir'])    && in_array($_GET['sort_dir'],    array('ASC', 'DESC')) ) ? $_GET['sort_dir'] : 'DESC';
+
+$q = $db->sql_query('SELECT t.topic_id,t.topic_title,t.topic_type,t.topic_icon,COUNT(p.post_id)-1 AS num_replies,
+                     COUNT(h.hit_id) AS num_views,t.topic_starter AS starter_id, u.username AS topic_starter,
+                     p.poster_name AS last_post_name, p.timestamp AS last_post_time
+                       FROM '.table_prefix.'decir_topics AS t
+                     LEFT JOIN '.table_prefix.'decir_posts AS p
+                       ON (t.last_post=p.post_id)
+                     LEFT JOIN '.table_prefix.'decir_hits AS h
+                       ON (t.topic_id=h.topic_id)
+                     LEFT JOIN '.table_prefix.'users AS u
+                       ON (u.user_id=t.topic_starter)
+                     WHERE t.forum_id='.$fid.'
+                     GROUP BY t.topic_id
+                     ORDER BY '.$sort_column.' '.$sort_dir.';');
+
+if(!$q)
+  $db->_die();
+
+echo '<div class="tblholder">
+      <table border="0" cellspacing="1" cellpadding="4">
+      <tr>
+        <th colspan="3">Topic</th>
+        <th>Author</th>
+        <th>Replies</th>
+        <th>Views</th>
+        <th>Last post</th>
+      </th>';
+
+if ( $row = $db->fetchrow() )
+{
+  do
+  {
+    echo '<tr>
+            <td class="row2"></td>
+            <td class="row2"></td>
+            <td class="row2" style="width: 100%;"><b><a href="' . makeUrlNS('DecirTopic', $row['topic_id']) . '">' . $row['topic_title'] . '</a></b></td>
+            <td class="row3" style="text-align: center; max-width: 100px;">' . $row['topic_starter'] . '</td>
+            <td class="row1" style="text-align: center; width: 50px;">' . $row['num_replies'] . '</td>
+            <td class="row1" style="text-align: center; width: 50px;">' . $row['num_views'] . '</td>
+            <td class="row3" style="text-align: center;"><small style="white-space: nowrap;">' . date('d M Y h:i a', $row['last_post_time']) . '<br />by '.$row['last_post_name'].'</small></td>
+          </tr>';
+  } while ( $row = $db->fetchrow() );
+}
+
+echo '</table></div>';
+
+$template->footer();
+
+?>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/decir/viewtopic.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,223 @@
+<?php
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ * viewtopic.php - Shows individual posts
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+require('common.php');
+require('bbcode.php');
+
+global $whos_online;
+
+$template->header();
+
+if ( strtolower($paths->getParam(0)) == 'post' || isset($_GET['pid']) )
+{
+  $pid = ( $n = $paths->getParam(1) ) ? $n : ( ( isset($_GET['pid']) ) ? $_GET['pid'] : 0 );
+  $pid = intval($pid);
+  
+  if(empty($pid))
+  {
+    echo '<p>Invalid topic ID</p>';
+    $template->footer();
+    return;
+  }
+  
+  $q = $db->sql_query('SELECT topic_id FROM '.table_prefix.'decir_posts WHERE post_id='.$pid.';');
+  if ( !$q )
+    $db->_die();
+  
+  $row = $db->fetchrow();
+}
+else
+{
+  $tid = ( $n = $paths->getParam(1) ) ? $n : ( ( isset($_GET['tid']) ) ? $_GET['tid'] : 0 );
+  $tid = intval($tid);
+  
+  if(empty($tid))
+  {
+    echo '<p>Invalid topic ID</p>';
+    $template->footer();
+    return;
+  }
+}
+
+$q = $db->sql_query('SELECT forum_id,topic_id FROM '.table_prefix.'decir_topics WHERE topic_id='.$tid.';');
+
+if ( !$q )
+  $db->_die();
+
+$topic_exists = true;
+
+if ( $db->numrows() > 0 )
+{
+  $row = $db->fetchrow();
+  $forum_id = $row['forum_id'];
+  $topic_id = $row['topic_id'];
+  $topic_exists = true;
+}
+else
+{
+  $topic_exists = false;
+}
+
+$post_template = <<<TPLCODE
+<a name="{POST_ID}" id="{POST_ID}"></a>
+<div class="post tblholder">
+  <table border="0" cellspacing="1" cellpadding="4" style="width: 100%;">
+    <tr>
+      <th colspan="2" style="text-align: left;">Posted: {TIMESTAMP}</th>
+    </tr>
+    <tr>
+      <td class="row3" valign="top">
+        {POST_TEXT}
+      </td>
+      <td class="row1" style="width: 120px;" valign="top">
+        <div class="menu">
+          {USER_LINK}
+          <ul>
+            <li><a>View profile</a></li>
+            <li><a>Visit homepage</a></li>
+            <li><a href="{QUOTE_LINK}">Quote this post</a></li>
+            <li><a>Vote to ban this user</a></li>
+            <li><a>Send private message</a></li>
+            <li><a>View all messages posted by {USERNAME}</a></li>
+          </ul>
+        </div>
+        <span class="menuclear"></span>
+        {USER_TITLE}<br />
+        <br />
+        Joined: {REG_TIME}
+        <!-- BEGIN whos_online_support -->
+          <br />
+          <!-- BEGIN user_is_online -->
+          <span style="color: #007900;"><b>Online</b></span>
+          <!-- BEGINELSE user_is_online -->
+          <span style="color: #666666;">Offline</span>
+          <!-- END user_is_online -->
+        <!-- END whos_online_support -->
+      </td>
+    </tr>
+  </table>
+</div>
+TPLCODE;
+
+$sql = 'SELECT p.post_id,p.poster_name,p.poster_id,u.username,p.timestamp,u.user_level,u.reg_time,t.post_text,t.bbcode_uid FROM '.table_prefix.'decir_posts AS p
+          LEFT JOIN '.table_prefix.'users AS u
+            ON u.user_id=poster_id
+          LEFT JOIN '.table_prefix.'decir_posts_text AS t
+            ON p.post_id=t.post_id
+          WHERE p.topic_id='.$tid.'
+          ORDER BY p.timestamp ASC;';
+
+$q = $db->sql_query($sql);
+if ( !$q )
+  $db->_die();
+
+if ( $db->numrows() < 1 )
+{
+  die_friendly('Error', '<p>The topic you requested does not exist.</p>');
+}
+
+$parser = $template->makeParserText($post_template);
+
+while ( $row = $db->fetchrow() )
+{
+  $poster_name = ( $row['poster_id'] == 1 ) ? $row['poster_name'] : $row['username'];
+  $datetime = date('F d, Y h:i a', $row['timestamp']);
+  $post_text = render_bbcode($row['post_text'], $row['bbcode_uid']);
+  $regtime = date('F Y', $row['reg_time']);
+  
+  $user_color = '#0000AA';
+  switch ( $row['user_level'] )
+  {
+    case USER_LEVEL_ADMIN: $user_color = '#AA0000'; break;
+    case USER_LEVEL_MOD:   $user_color = '#00AA00'; break;
+  }
+  if ( $row['poster_id'] > 1 )
+  {
+    $user_link = "<a style='color: $user_color; background-color: transparent; display: inline; padding: 0;' href='".makeUrlNS('User', str_replace(' ', '_', $poster_name))."'><big>$poster_name</big></a>";
+  }
+  else
+  {
+    $user_link = '<big>'.$poster_name.'</big>';
+  }
+  $quote_link = makeUrlNS('Special', 'Forum/New/Quote/' . $row['post_id'], false, true);
+  $user_title = 'Anonymous user';
+  switch ( $row['user_level'] )
+  {
+    case USER_LEVEL_ADMIN: $user_title = 'Administrator'; break;
+    case USER_LEVEL_MOD:   $user_title = 'Moderator'; break;
+    case USER_LEVEL_MEMBER:$user_title = 'Member'; break;
+    case USER_LEVEL_GUEST: $user_title = 'Guest'; break;
+  }
+  $parser->assign_vars(Array(
+      'POST_ID' => (string)$row['post_id'],
+      'USERNAME' => $poster_name,
+      'USER_LINK' => $user_link,
+      'REG_TIME' => $regtime,
+      'TIMESTAMP' => $datetime,
+      'POST_TEXT' => $post_text,
+      'USER_TITLE' => $user_title,
+      'QUOTE_LINK' => $quote_link
+    ));
+  // Decir can integrate with the Who's Online plugin
+  $who_support = $plugins->loaded('WhosOnline');
+  $user_online = false;
+  if ( $who_support && in_array($row['username'], $whos_online['users']) )
+  {
+    $user_online = true;
+  }
+  elseif ( $row['poster_id'] < 2 )
+  {
+    $who_support = false;
+  }
+  $parser->assign_bool(Array(
+      'whos_online_support' => $who_support,
+      'user_is_online' => $user_online
+    ));
+  echo $parser->run();
+}
+
+$db->free_result();
+
+if ( $topic_exists )
+{
+  $can_post_replies = false;
+  $can_post_topics  = false;
+  
+  $forum_perms = $session->fetch_page_acl('DecirForum', $forum_id);
+  $topic_perms = $session->fetch_page_acl('DecirTopic', $topic_id);
+  
+  if ( $forum_perms->get_permissions('decir_post') )
+    $can_post_topics = true;
+  
+  if ( $topic_perms->get_permissions('decir_reply') )
+    $can_post_replies = true;
+  
+  echo '<p>';
+  if ( $can_post_topics )
+  {
+    echo '<a href="' . makeUrlNS('Special', 'Forum/New/Topic/' . $forum_id) . '">Post new topic</a>';
+  }
+  if ( $can_post_topics && $can_post_replies )
+  {
+    echo '&nbsp;&nbsp;|&nbsp;&nbsp;';
+  }
+  if ( $can_post_replies )
+  {
+    echo '<a href="' . makeUrlNS('Special', 'Forum/New/Reply/' . $topic_id) . '">Add reply</a>';
+  }
+  echo '</p>';
+}
+
+$template->footer();
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/plugins/Decir.php	Wed Jun 13 22:33:54 2007 -0400
@@ -0,0 +1,92 @@
+<?php
+/*
+Plugin Name: Decir
+Plugin URI: javascript: // No URL yet, stay tuned!
+Description: Decir is an advanced bulletin board system (forum) for Enano. 
+Author: Dan Fuhry
+Version: 0.1
+Author URI: http://www.enanocms.org/
+*/
+
+/*
+ * Decir
+ * Version 0.1
+ * Copyright (C) 2007 Dan Fuhry
+ *
+ * This program is Free Software; you can redistribute and/or modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
+ * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for details.
+ */
+
+define('ENANO_DECIR_VERSION', '0.1');
+define('DECIR_ROOT', ENANO_ROOT . '/decir');
+ 
+$plugins->attachHook('acl_rule_init', 'decir_early_init($this, $session);');
+$plugins->attachHook('base_classes_initted', '
+    $paths->add_page(Array(
+      \'name\'=>\'Forum\',
+      \'urlname\'=>\'Forum\',
+      \'namespace\'=>\'Special\',
+      \'special\'=>0,\'visible\'=>0,\'comments_on\'=>0,\'protected\'=>1,\'delvotes\'=>0,\'delvote_ips\'=>\'\',
+      ));
+  ');
+
+function decir_early_init(&$paths, &$session)
+{
+  $paths->addAdminNode('Decir forum configuration', 'General settings', 'DecirGeneral');
+  $paths->nslist['DecirForum']  = $paths->nslist['Special'] . 'Forum/ViewForum/';
+  $paths->nslist['DecirPost']   = $paths->nslist['Special'] . 'Forum/Post/';
+  $paths->nslist['DecirTopic']  = $paths->nslist['Special'] . 'Forum/Topic/';
+  
+  $session->register_acl_type('decir_see_forum',  AUTH_ALLOW, 'See forum in index', Array('read'),             'DecirForum');
+  $session->register_acl_type('decir_view_forum', AUTH_ALLOW, 'View forum',         Array('decir_see_forum'),  'DecirForum');
+  $session->register_acl_type('decir_post',       AUTH_ALLOW, 'Post new topics',    Array('decir_view_forum'), 'DecirForum');
+  $session->register_acl_type('decir_reply',      AUTH_ALLOW, 'Reply to topics',    Array('decir_post'),       'DecirTopic');
+}
+
+function page_Special_Forum()
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  
+  if ( getConfig('decir_version') != ENANO_DECIR_VERSION || isset($_POST['do_install_finish']) )
+  {
+    require(DECIR_ROOT . '/install.php');
+  }
+  
+  $act = strtolower( ( $n = $paths->getParam(0) ) ? $n : 'Index' );
+  
+  $curdir = getcwd();
+  chdir(DECIR_ROOT);
+  
+  switch($act)
+  {
+    case 'index':
+    default:
+      require('forum_index.php');
+      break;
+    case 'viewforum':
+      require('viewforum.php');
+      break;
+    case 'topic':
+    case 'post':
+    case 'viewtopic':
+      require('viewtopic.php');
+      break;
+    case 'new':
+      require('posting.php');
+      break;
+  }
+  
+  chdir($curdir);
+  
+}
+
+function page_Admin_DecirGeneral()
+{
+  global $db, $session, $paths, $template, $plugins; if($session->auth_level < USER_LEVEL_ADMIN || $session->user_level < USER_LEVEL_ADMIN) { header('Location: '.makeUrl($paths->nslist['Special'].'Administration'.urlSeparator.'noheaders')); die('Hacking attempt'); }
+  echo 'Hello world!';
+}
+
+?>