<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
Background: #fff
Foreground: #000
PrimaryPale: #8cf
PrimaryLight: #18f
PrimaryMid: #04b
PrimaryDark: #014
SecondaryPale: #ffc
SecondaryLight: #fe8
SecondaryMid: #db4
SecondaryDark: #841
TertiaryPale: #eee
TertiaryLight: #ccc
TertiaryMid: #999
TertiaryDark: #666
Error: #f88
/*{{{*/
body {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}

a {color:[[ColorPalette::PrimaryMid]];}
a:hover {background-color:[[ColorPalette::PrimaryMid]]; color:[[ColorPalette::Background]];}
a img {border:0;}

h1,h2,h3,h4,h5,h6 {color:[[ColorPalette::SecondaryDark]]; background:transparent;}
h1 {border-bottom:2px solid [[ColorPalette::TertiaryLight]];}
h2,h3 {border-bottom:1px solid [[ColorPalette::TertiaryLight]];}

.button {color:[[ColorPalette::PrimaryDark]]; border:1px solid [[ColorPalette::Background]];}
.button:hover {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::SecondaryLight]]; border-color:[[ColorPalette::SecondaryMid]];}
.button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::SecondaryDark]];}

.header {background:[[ColorPalette::PrimaryMid]];}
.headerShadow {color:[[ColorPalette::Foreground]];}
.headerShadow a {font-weight:normal; color:[[ColorPalette::Foreground]];}
.headerForeground {color:[[ColorPalette::Background]];}
.headerForeground a {font-weight:normal; color:[[ColorPalette::PrimaryPale]];}

.tabSelected{color:[[ColorPalette::PrimaryDark]];
	background:[[ColorPalette::TertiaryPale]];
	border-left:1px solid [[ColorPalette::TertiaryLight]];
	border-top:1px solid [[ColorPalette::TertiaryLight]];
	border-right:1px solid [[ColorPalette::TertiaryLight]];
}
.tabUnselected {color:[[ColorPalette::Background]]; background:[[ColorPalette::TertiaryMid]];}
.tabContents {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::TertiaryPale]]; border:1px solid [[ColorPalette::TertiaryLight]];}
.tabContents .button {border:0;}

#sidebar {}
#sidebarOptions input {border:1px solid [[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel {background:[[ColorPalette::PrimaryPale]];}
#sidebarOptions .sliderPanel a {border:none;color:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:hover {color:[[ColorPalette::Background]]; background:[[ColorPalette::PrimaryMid]];}
#sidebarOptions .sliderPanel a:active {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::Background]];}

.wizard {background:[[ColorPalette::PrimaryPale]]; border:1px solid [[ColorPalette::PrimaryMid]];}
.wizard h1 {color:[[ColorPalette::PrimaryDark]]; border:none;}
.wizard h2 {color:[[ColorPalette::Foreground]]; border:none;}
.wizardStep {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];
	border:1px solid [[ColorPalette::PrimaryMid]];}
.wizardStep.wizardStepDone {background:[[ColorPalette::TertiaryLight]];}
.wizardFooter {background:[[ColorPalette::PrimaryPale]];}
.wizardFooter .status {background:[[ColorPalette::PrimaryDark]]; color:[[ColorPalette::Background]];}
.wizard .button {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
	border-color:[[ColorPalette::SecondaryPale]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryDark]] [[ColorPalette::SecondaryPale]];}
.wizard .button:hover {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Background]];}
.wizard .button:active {color:[[ColorPalette::Background]]; background:[[ColorPalette::Foreground]]; border: 1px solid;
	border-color:[[ColorPalette::PrimaryDark]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryPale]] [[ColorPalette::PrimaryDark]];}
	
.wizard .notChanged {background:transparent;}
.wizard .changedLocally {background:#80ff80;}
.wizard .changedServer {background:#8080ff;}
.wizard .changedBoth {background:#ff8080;}
.wizard .notFound {background:#ffff80;}
.wizard .putToServer {background:#ff80ff;}
.wizard .gotFromServer {background:#80ffff;}

#messageArea {border:1px solid [[ColorPalette::SecondaryMid]]; background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]];}
#messageArea .button {color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none;}

.popupTiddler {background:[[ColorPalette::TertiaryPale]]; border:2px solid [[ColorPalette::TertiaryMid]];}

.popup {background:[[ColorPalette::TertiaryPale]]; color:[[ColorPalette::TertiaryDark]]; border-left:1px solid [[ColorPalette::TertiaryMid]]; border-top:1px solid [[ColorPalette::TertiaryMid]]; border-right:2px solid [[ColorPalette::TertiaryDark]]; border-bottom:2px solid [[ColorPalette::TertiaryDark]];}
.popup hr {color:[[ColorPalette::PrimaryDark]]; background:[[ColorPalette::PrimaryDark]]; border-bottom:1px;}
.popup li.disabled {color:[[ColorPalette::TertiaryMid]];}
.popup li a, .popup li a:visited {color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border: none;}
.popup li a:active {background:[[ColorPalette::SecondaryPale]]; color:[[ColorPalette::Foreground]]; border: none;}
.popupHighlight {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.listBreak div {border-bottom:1px solid [[ColorPalette::TertiaryDark]];}

.tiddler .defaultCommand {font-weight:bold;}

.shadow .title {color:[[ColorPalette::TertiaryDark]];}

.title {color:[[ColorPalette::SecondaryDark]];}
.subtitle {color:[[ColorPalette::TertiaryDark]];}

.toolbar {color:[[ColorPalette::PrimaryMid]];}
.toolbar a {color:[[ColorPalette::TertiaryLight]];}
.selected .toolbar a {color:[[ColorPalette::TertiaryMid]];}
.selected .toolbar a:hover {color:[[ColorPalette::Foreground]];}

.tagging, .tagged {border:1px solid [[ColorPalette::TertiaryPale]]; background-color:[[ColorPalette::TertiaryPale]];}
.selected .tagging, .selected .tagged {background-color:[[ColorPalette::TertiaryLight]]; border:1px solid [[ColorPalette::TertiaryMid]];}
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

.footer {color:[[ColorPalette::TertiaryLight]];}
.selected .footer {color:[[ColorPalette::TertiaryMid]];}

.sparkline {background:[[ColorPalette::PrimaryPale]]; border:0;}
.sparktick {background:[[ColorPalette::PrimaryDark]];}

.error, .errorButton {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::Error]];}
.warning {color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryPale]];}
.lowlight {background:[[ColorPalette::TertiaryLight]];}

.zoomer {background:none; color:[[ColorPalette::TertiaryMid]]; border:3px solid [[ColorPalette::TertiaryMid]];}

.imageLink, #displayArea .imageLink {background:transparent;}

.annotation {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; border:2px solid [[ColorPalette::SecondaryMid]];}

.viewer .listTitle {list-style-type:none; margin-left:-2em;}
.viewer .button {border:1px solid [[ColorPalette::SecondaryMid]];}
.viewer blockquote {border-left:3px solid [[ColorPalette::TertiaryDark]];}

.viewer table, table.twtable {border:2px solid [[ColorPalette::TertiaryDark]];}
.viewer th, .viewer thead td, .twtable th, .twtable thead td {background:[[ColorPalette::SecondaryMid]]; border:1px solid [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::Background]];}
.viewer td, .viewer tr, .twtable td, .twtable tr {border:1px solid [[ColorPalette::TertiaryDark]];}

.viewer pre {border:1px solid [[ColorPalette::SecondaryLight]]; background:[[ColorPalette::SecondaryPale]];}
.viewer code {color:[[ColorPalette::SecondaryDark]];}
.viewer hr {border:0; border-top:dashed 1px [[ColorPalette::TertiaryDark]]; color:[[ColorPalette::TertiaryDark]];}

.highlight, .marked {background:[[ColorPalette::SecondaryLight]];}

.editor input {border:1px solid [[ColorPalette::PrimaryMid]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%;}
.editorFooter {color:[[ColorPalette::TertiaryMid]];}

#backstageArea {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::TertiaryMid]];}
#backstageArea a {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstageArea a:hover {background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; }
#backstageArea a.backstageSelTab {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
#backstageButton a {background:none; color:[[ColorPalette::Background]]; border:none;}
#backstageButton a:hover {background:[[ColorPalette::Foreground]]; color:[[ColorPalette::Background]]; border:none;}
#backstagePanel {background:[[ColorPalette::Background]]; border-color: [[ColorPalette::Background]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]] [[ColorPalette::TertiaryDark]];}
.backstagePanelFooter .button {border:none; color:[[ColorPalette::Background]];}
.backstagePanelFooter .button:hover {color:[[ColorPalette::Foreground]];}
#backstageCloak {background:[[ColorPalette::Foreground]]; opacity:0.6; filter:'alpha(opacity:60)';}
/*}}}*/
/*{{{*/
* html .tiddler {height:1%;}

body {font-size:.75em; font-family:arial,helvetica; margin:0; padding:0;}

h1,h2,h3,h4,h5,h6 {font-weight:bold; text-decoration:none;}
h1,h2,h3 {padding-bottom:1px; margin-top:1.2em;margin-bottom:0.3em;}
h4,h5,h6 {margin-top:1em;}
h1 {font-size:1.35em;}
h2 {font-size:1.25em;}
h3 {font-size:1.1em;}
h4 {font-size:1em;}
h5 {font-size:.9em;}

hr {height:1px;}

a {text-decoration:none;}

dt {font-weight:bold;}

ol {list-style-type:decimal;}
ol ol {list-style-type:lower-alpha;}
ol ol ol {list-style-type:lower-roman;}
ol ol ol ol {list-style-type:decimal;}
ol ol ol ol ol {list-style-type:lower-alpha;}
ol ol ol ol ol ol {list-style-type:lower-roman;}
ol ol ol ol ol ol ol {list-style-type:decimal;}

.txtOptionInput {width:11em;}

#contentWrapper .chkOptionInput {border:0;}

.externalLink {text-decoration:underline;}

.indent {margin-left:3em;}
.outdent {margin-left:3em; text-indent:-3em;}
code.escaped {white-space:nowrap;}

.tiddlyLinkExisting {font-weight:bold;}
.tiddlyLinkNonExisting {font-style:italic;}

/* the 'a' is required for IE, otherwise it renders the whole tiddler in bold */
a.tiddlyLinkNonExisting.shadow {font-weight:bold;}

#mainMenu .tiddlyLinkExisting,
	#mainMenu .tiddlyLinkNonExisting,
	#sidebarTabs .tiddlyLinkNonExisting {font-weight:normal; font-style:normal;}
#sidebarTabs .tiddlyLinkExisting {font-weight:bold; font-style:normal;}

.header {position:relative;}
.header a:hover {background:transparent;}
.headerShadow {position:relative; padding:4.5em 0em 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:4.5em 0em 1em 1em; left:0px; top:0px;}

.siteTitle {font-size:3em;}
.siteSubtitle {font-size:1.2em;}

#mainMenu {position:absolute; left:0; width:10em; text-align:right; line-height:1.6em; padding:1.5em 0.5em 0.5em 0.5em; font-size:1.1em;}

#sidebar {position:absolute; right:3px; width:16em; font-size:.9em;}
#sidebarOptions {padding-top:0.3em;}
#sidebarOptions a {margin:0em 0.2em; padding:0.2em 0.3em; display:block;}
#sidebarOptions input {margin:0.4em 0.5em;}
#sidebarOptions .sliderPanel {margin-left:1em; padding:0.5em; font-size:.85em;}
#sidebarOptions .sliderPanel a {font-weight:bold; display:inline; padding:0;}
#sidebarOptions .sliderPanel input {margin:0 0 .3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

.wizard {padding:0.1em 1em 0em 2em;}
.wizard h1 {font-size:2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
.wizard h2 {font-size:1.2em; font-weight:bold; background:none; padding:0em 0em 0em 0em; margin:0.4em 0em 0.2em 0em;}
.wizardStep {padding:1em 1em 1em 1em;}
.wizard .button {margin:0.5em 0em 0em 0em; font-size:1.2em;}
.wizardFooter {padding:0.8em 0.4em 0.8em 0em;}
.wizardFooter .status {padding:0em 0.4em 0em 0.4em; margin-left:1em;}
.wizard .button {padding:0.1em 0.2em 0.1em 0.2em;}

#messageArea {position:fixed; top:2em; right:0em; margin:0.5em; padding:0.5em; z-index:2000; _position:absolute;}
.messageToolbar {display:block; text-align:right; padding:0.2em 0.2em 0.2em 0.2em;}
#messageArea a {text-decoration:underline;}

.tiddlerPopupButton {padding:0.2em 0.2em 0.2em 0.2em;}
.popupTiddler {position: absolute; z-index:300; padding:1em 1em 1em 1em; margin:0;}

.popup {position:absolute; z-index:300; font-size:.9em; padding:0; list-style:none; margin:0;}
.popup .popupMessage {padding:0.4em;}
.popup hr {display:block; height:1px; width:auto; padding:0; margin:0.2em 0em;}
.popup li.disabled {padding:0.4em;}
.popup li a {display:block; padding:0.4em; font-weight:normal; cursor:pointer;}
.listBreak {font-size:1px; line-height:1px;}
.listBreak div {margin:2px 0;}

.tabset {padding:1em 0em 0em 0.5em;}
.tab {margin:0em 0em 0em 0.25em; padding:2px;}
.tabContents {padding:0.5em;}
.tabContents ul, .tabContents ol {margin:0; padding:0;}
.txtMainTab .tabContents li {list-style:none;}
.tabContents li.listLink { margin-left:.75em;}

#contentWrapper {display:block;}
#splashScreen {display:none;}

#displayArea {margin:1em 17em 0em 14em;}

.toolbar {text-align:right; font-size:.9em;}

.tiddler {padding:1em 1em 0em 1em;}

.missing .viewer,.missing .title {font-style:italic;}

.title {font-size:1.6em; font-weight:bold;}

.missing .subtitle {display:none;}
.subtitle {font-size:1.1em;}

.tiddler .button {padding:0.2em 0.4em;}

.tagging {margin:0.5em 0.5em 0.5em 0; float:left; display:none;}
.isTag .tagging {display:block;}
.tagged {margin:0.5em; float:right;}
.tagging, .tagged {font-size:0.9em; padding:0.25em;}
.tagging ul, .tagged ul {list-style:none; margin:0.25em; padding:0;}
.tagClear {clear:both;}

.footer {font-size:.9em;}
.footer li {display:inline;}

.annotation {padding:0.5em; margin:0.5em;}

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0em 0.25em; padding:0em 0.25em;}
.viewer blockquote {line-height:1.5em; padding-left:0.8em;margin-left:2.5em;}
.viewer ul, .viewer ol {margin-left:0.5em; padding-left:1.5em;}

.viewer table, table.twtable {border-collapse:collapse; margin:0.8em 1.0em;}
.viewer th, .viewer td, .viewer tr,.viewer caption,.twtable th, .twtable td, .twtable tr,.twtable caption {padding:3px;}
table.listView {font-size:0.85em; margin:0.8em 1.0em;}
table.listView th, table.listView td, table.listView tr {padding:0px 3px 0px 3px;}

.viewer pre {padding:0.5em; margin-left:0.5em; font-size:1.2em; line-height:1.4em; overflow:auto;}
.viewer code {font-size:1.2em; line-height:1.4em;}

.editor {font-size:1.1em;}
.editor input, .editor textarea {display:block; width:100%; font:inherit;}
.editorFooter {padding:0.25em 0em; font-size:.9em;}
.editorFooter .button {padding-top:0px; padding-bottom:0px;}

.fieldsetFix {border:0; padding:0; margin:1px 0px 1px 0px;}

.sparkline {line-height:1em;}
.sparktick {outline:0;}

.zoomer {font-size:1.1em; position:absolute; overflow:hidden;}
.zoomer div {padding:1em;}

* html #backstage {width:99%;}
* html #backstageArea {width:99%;}
#backstageArea {display:none; position:relative; overflow: hidden; z-index:150; padding:0.3em 0.5em 0.3em 0.5em;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em 0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0em; right:0em;}
#backstageButton a {padding:0.1em 0.4em 0.1em 0.4em; margin:0.1em 0.1em 0.1em 0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel {display:none; z-index:100; position:absolute; width:90%; margin:0em 3em 0em 3em; padding:1em 1em 1em 1em;}
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding:0.2em 0.4em 0.2em 0.4em;}
#backstageCloak {display:none; z-index:20; position:absolute; width:100%; height:100px;}

.whenBackstage {display:none;}
.backstageVisible .whenBackstage {display:block;}
/*}}}*/
/***
StyleSheet for use when a translation requires any css style changes.
This StyleSheet can be used directly by languages such as Chinese, Japanese and Korean which need larger font sizes.
***/
/*{{{*/
body {font-size:0.8em;}
#sidebarOptions {font-size:1.05em;}
#sidebarOptions a {font-style:normal;}
#sidebarOptions .sliderPanel {font-size:0.95em;}
.subtitle {font-size:0.8em;}
.viewer table.listView {font-size:0.95em;}
/*}}}*/
/*{{{*/
@media print {
#mainMenu, #sidebar, #messageArea, .toolbar, #backstageButton, #backstageArea {display: none ! important;}
#displayArea {margin: 1em 1em 0em 1em;}
/* Fixes a feature in Firefox 1.5.0.2 where print preview displays the noscript content */
noscript {display:none;}
}
/*}}}*/
<!--{{{-->
<div class='header' macro='gradient vert [[ColorPalette::PrimaryLight]] [[ColorPalette::PrimaryMid]]'>
<div class='headerShadow'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>&nbsp;
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
</div>
<div id='mainMenu' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
<div id='sidebarOptions' refresh='content' tiddler='SideBarOptions'></div>
<div id='sidebarTabs' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::ViewToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='subtitle'><span macro='view modifier link'></span>, <span macro='view modified date'></span> (<span macro='message views.wikified.createdPrompt'></span> <span macro='view created date'></span>)</div>
<div class='tagging' macro='tagging'></div>
<div class='tagged' macro='tags'></div>
<div class='viewer' macro='view text wikified'></div>
<div class='tagClear'></div>
<!--}}}-->
<!--{{{-->
<div class='toolbar' macro='toolbar [[ToolbarCommands::EditToolbar]]'></div>
<div class='title' macro='view title'></div>
<div class='editor' macro='edit title'></div>
<div macro='annotations'></div>
<div class='editor' macro='edit text'></div>
<div class='editor' macro='edit tags'></div><div class='editorFooter'><span macro='message views.editor.tagPrompt'></span><span macro='tagChooser'></span></div>
<!--}}}-->
To get started with this blank TiddlyWiki, you'll need to modify the following tiddlers:
* SiteTitle & SiteSubtitle: The title and subtitle of the site, as shown above (after saving, they will also appear in the browser title bar)
* MainMenu: The menu (usually on the left)
* DefaultTiddlers: Contains the names of the tiddlers that you want to appear when the TiddlyWiki is opened
You'll also need to enter your username for signing your edits: <<option txtUserName>>
These InterfaceOptions for customising TiddlyWiki are saved in your browser

Your username for signing your edits. Write it as a WikiWord (eg JoeBloggs)

<<option txtUserName>>
<<option chkSaveBackups>> SaveBackups
<<option chkAutoSave>> AutoSave
<<option chkRegExpSearch>> RegExpSearch
<<option chkCaseSensitiveSearch>> CaseSensitiveSearch
<<option chkAnimate>> EnableAnimations

----
Also see AdvancedOptions
<<importTiddlers>>
[[JSSpec and TiddlyWikis]]
* Required
** JSSpecScript: the original [[JSSpec script|http://code.google.com/p/jsspec/source/browse/trunk/JSSpec.js?r=108]] with minor modifications. Not all the modifications work as expected in IE ([rerun] links).
** JSSpecPlugin: the code for the {{{<jsspec></jsspec>}}} macro
** [[google-diff-match-patch]]: [[Diff, Match and Patch libraries for Plain Text|http://code.google.com/p/google-diff-match-patch/]], in its [[DiffPlugin adaptation|http://trac.tiddlywiki.org/browser/Trunk/contributors/FND/plugins/DiffPlugin]]
* Useful
** [[tests_mock]] simple functions to simulate complex behaviour during tests (see http://en.wikipedia.org/wiki/Mock_object), taken from the [[TiddlyWiki core testing scripts|http://trac.tiddlywiki.org/browser/Trunk/core/tests/js]]
** [[JSSpecStyles]] based on the original [[JSSpec style sheet|http://code.google.com/p/jsspec/source/browse/trunk/JSSpec.css?r=105]] 
The scripts have to be tagged {{{systemConfig}}}. The style sheet has to be referenced in [[StyleSheet]].

The plugin has started to eat its own dogfood: [[See where it wants to get when it grows up|JSSpecPlugin Tests]]
[[JSSpec|http://jania.pe.kr/aw/moin.cgi/JSSpec]]  is a ~JavaScript [[Behavior Driven Development|http://en.wikipedia.org/wiki/Behavior_driven_development]] (BDD) framework. Behavior Driven Development is a close cousin of [[Test Driven Development|http://en.wikipedia.org/wiki/Test_Driven_Development]]. If you are unfamiliar with any of these concepts, suffice it to say that they are techniques that make writing code more enjoyable and that lead to better designs and less bugs.

The core ~TiddlyWiki team uses [[JSSpec|http://jania.pe.kr/aw/moin.cgi/JSSpec]] to run automated unit test. You can a high level view of the workflow reading [[this post|http://groups.google.com/group/TiddlyWikiDev/msg/f8ced71ae81f9a0a]]. To summarize it, it is based on using command line tools that create an html file that runs the tests on loading.

The JSSpecPlugin allows to run ~JSSpec tests within a standard ~TiddlyWiki. The goals are to
* allow plugin authors to publish descriptive test in the same self-contained ~TiddlyWiki where they are publishing a plugin
* allow casual  ~TiddlyWiki hackers to get a taste of Behavior Driven Development without having to install the command line tools

[[See the code in action]]
//{{{

config.macros.jsspec = {};

(function(){
var plugin = config.macros.jsspec;

plugin.clearTestSpecs = function() 
{
	JSSpec.specs = [];
};
plugin.loadTestSpecs = function(place, srcCode)
{
	var execute = true;
	try{
		eval(srcCode);
	}
	catch(ex){
		createTiddlyError(place, "Error: " + ex.message);
		execute = false;
	}
        return execute;
};
plugin.verifyTestSpecs = function(place)
{
	JSSpec.onload();
};
plugin.runTestsInPlace = function(place)
{
    place.style.display = "block";
    var oldCnr = document.getElementById("jsspec_container");
    if (oldCnr){
        oldCnr.setAttribute("id","");
    }
    place.setAttribute("id","jsspec_container");
    place.className = "jsspec_container";

    plugin.clearTestSpecs();
    if(plugin.loadTestSpecs(place, place.spec)){
        plugin.verifyTestSpecs(place);
    }
    plugin.setClassNamesInContainer();
};
plugin.setClassNamesInContainer = function()
{
    // A bit superfluous: could be done with CSS selectors, but I think that we would not have the list at 50% until CSS 3...
    var subdivs = {"list":null, "log":null};  // the "title" div has its class changed by jsspec
    for (var id in subdivs){
        var subdiv = document.getElementById(id);
        if (subdiv) subdiv.className = "jsspec_" + id;
    }
};
plugin.runClickHandler = function(e)
{
    if(!e) var e = window.event;
    var n = this.nextSibling;
    plugin.runTestsInPlace(n);
    return false;
};
plugin.formatter = {
    name: "JSSpecPlugin",
    match: "\\<jsspec\\>",
    lookaheadRegExp: /(?:<jsspec>)((?:.|\n)*?)\n(?:<\/jsspec>)/mg,
    handler: function(w)
    {
        this.lookaheadRegExp.lastIndex = w.matchStart;
        var lookaheadMatch = this.lookaheadRegExp.exec(w.source)
        if(lookaheadMatch && lookaheadMatch.index == w.matchStart )
        {
            var code = lookaheadMatch[1];

            var testBtn = createTiddlyButton(w.output, "run tests","",plugin.runClickHandler,"button");
            var testPanel = createTiddlyElement(w.output,"div");
            testPanel.style.display = "none";
            testPanel.spec = code;
            w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;

            createTiddlyButton(w.output, "show/hide code" + " "+"\u00BB","",this.onClickSlider,"button sliderButton");
	    var codePanel = createTiddlyElement(w.output,"div");
            wikify("{{{\n" + code + "\n}}}", codePanel);
       }
    },
    onClickSlider : function(e)
    {
        if(!e) var e = window.event;
	var n = this.nextSibling;
        n.style.display = (n.style.display=="none") ? "block" : "none";
        return false;
    }
};
config.formatters.push(plugin.formatter);
})();


/*
// Lots of code stolen from http://tw.lewcid.org/sandbox/#InlineSlidersPlugin
config.formatters.push( {
    name: "JSSpecMacro",
    match: "\\<jsspec\\>",
    lookaheadRegExp: /(?:<jsspec>)((?:.|\n)*?)\n(?:<\/jsspec>)/mg,
    handler: function(w)
    {
        this.lookaheadRegExp.lastIndex = w.matchStart;
        var lookaheadMatch = this.lookaheadRegExp.exec(w.source)
        if(lookaheadMatch && lookaheadMatch.index == w.matchStart )
        {
            var code = lookaheadMatch[1];

            var testBtn = createTiddlyButton(w.output, "run tests","",this.onClickRunTests,"button");
            var testPanel = createTiddlyElement(w.output,"div");
            testPanel.style.display = "none";
            testBtn.spec = code;
            w.nextMatch = lookaheadMatch.index + lookaheadMatch[0].length;

            createTiddlyButton(w.output, "show/hide code" + " "+"\u00BB","",this.onClickSlider,"button sliderButton");
	    var codePanel = createTiddlyElement(w.output,"div");
            wikify("{{{\n" + code + "\n}}}", codePanel);
       }
    },
    onClickSlider : function(e)
    {
        if(!e) var e = window.event;
	var n = this.nextSibling;
        n.style.display = (n.style.display=="none") ? "block" : "none";
        return false;
    },
    onClickRunTests: function(e)
    {
        if(!e) var e = window.event;
	var n = this.nextSibling;
        n.style.display = "block";
        n.setAttribute("id","jsspec_container");
        JSSpec.specs = [];
        eval(this.spec);
        JSSpec.onload();
        return false;
    }
});
*/
//}}}
<jsspec>
var tstHelper={};

describe("<jsspec [autorun]>code</jsspec>", {

"should accept the 'autorun' optional parameter": function() {
	
	value_of(this).should_fail("Code and tests not implemented");
},
"should run the tests on rendering when 'autorun' specified": function(){
	
	value_of(this).should_fail("Code and tests not implemented");
},
"should report errors in the the embedded code":  function() {

	tests_mock.before("config.macros.jsspec.clearTestSpecs");
	tests_mock.before("config.macros.jsspec.verifyTestSpecs");
	var panel = tstHelper.createMockTestPanel();
	panel.spec = "a.a.a.a();";

	config.macros.jsspec.runTestsInPlace(panel);

	value_of(tests_mock.after("config.macros.jsspec.clearTestSpecs").called).should_be(1);
	value_of(tests_mock.after("config.macros.jsspec.verifyTestSpecs").called).should_be(0);
	value_of(panel.innerHTML).should_match(/Error/);
	value_of(panel.innerHTML).should_match(/defined/);
},
after_each : function() {

	tests_mock.reset();
	tstHelper.cleanupMockTestPanel();
}
});

tstHelper.originalTestPanel = undefined;
tstHelper.mockTestPanel = undefined;
tstHelper.createMockTestPanel = function()
{
	this.originalTestPanel = document.getElementById('jsspec_container');
	this.mockTestPanel = createTiddlyElement(document.body,"div");
	return this.mockTestPanel;
};
tstHelper.cleanupMockTestPanel = function()
{
	this.removeNode(this.mockTestPanel);
	this.mockTestPanel = undefined;
	if (this.originalTestPanel){
		this.originalTestPanel.setAttribute("id","jsspec_container");
		this.originalTestPanel = undefined;
	}
};
tstHelper.removeNode=function(node){
  if(node){
    node.parentNode.removeChild(node);
  }
};



describe("<<jsspec [autorun] tiddler1 tiddler2 ...>>", {

"should eval() all the specified tiddlers before having JSSpec called": function() {

	value_of(this).should_fail("Code and tests not implemented");
},
"should create a run button, a hidden panel and a link for every specified tiddler": function() {

	value_of(this).should_fail("Code and tests not implemented");
},
"should give an error when no tiddler is specified":  function() {

	value_of(this).should_fail("Code and tests not implemented");
},
"should report errors in the specified tiddlers":  function() {

	value_of(this).should_fail("Code and tests not implemented");
}
});

</jsspec>
/***
From http://code.google.com/p/jsspec/source/browse/trunk/JSSpec.js?r=108
with some modifications
***/
//{{{

//{{{
/**
 * JSSpec
 *
 * Copyright 2007 Alan Kang
 *  - mailto:jania902@gmail.com
 *  - http://jania.pe.kr
 *
 * http://jania.pe.kr/aw/moin.cgi/JSSpec
 *
 * Dependencies:
 *  - diff_match_patch.js ( http://code.google.com/p/google-diff-match-patch )
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
 */

/**
 * Namespace
 */

var JSSpec = {
	specs: [],
	
	EMPTY_FUNCTION: function() {},
	
	Browser: {
		Trident: navigator.appName == "Microsoft Internet Explorer",
		Webkit: navigator.userAgent.indexOf('AppleWebKit/') > -1,
		Gecko: navigator.userAgent.indexOf('Gecko') > -1 && navigator.userAgent.indexOf('KHTML') == -1,
		Presto: navigator.appName == "Opera"
	}
};



/**
 * Executor
 */
JSSpec.Executor = function(target, onSuccess, onException) {
	this.target = target;
	this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION;
	this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION;
	
	if(JSSpec.Browser.Trident) {
		// Exception handler for Trident. It helps to collect exact line number where exception occured.
		window.onerror = function(message, fileName, lineNumber) {
			var self = window._curExecutor;
			var ex = {message:message, fileName:fileName, lineNumber:lineNumber};

			if(JSSpec._secondPass)  {
				ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
				delete JSSpec._secondPass;
				delete JSSpec._assertionFailure;
				
				ex.type = "failure";
				self.onException(self, ex);
			} else if(JSSpec._assertionFailure) {
				JSSpec._secondPass = true;
				self.run();
			} else {
				self.onException(self, ex);
			}
			
			return true;
		};
	}
};
JSSpec.Executor.prototype.mergeExceptions = function(assertionFailure, normalException) {
	var merged = {
		message:assertionFailure.message,
		fileName:normalException.fileName,
		lineNumber:normalException.lineNumber
	};
	
	return merged;
};

JSSpec.Executor.prototype.run = function() {
	var self = this;
	var target = this.target;
	var onSuccess = this.onSuccess;
	var onException = this.onException;
	
	window.setTimeout(
		function() {
			var result;
			if(JSSpec.Browser.Trident) {
				window._curExecutor = self;
				
				result = self.target();
				self.onSuccess(self, result);
			} else {
				try {
					result = self.target();
					self.onSuccess(self, result);
				} catch(ex) {
					if(JSSpec.Browser.Webkit) ex = {message:ex.message, fileName:ex.sourceURL, lineNumber:ex.line};
					
					if(JSSpec._secondPass)  {
						ex = self.mergeExceptions(JSSpec._assertionFailure, ex);
						delete JSSpec._secondPass;
						delete JSSpec._assertionFailure;
						
						ex.type = "failure";
						self.onException(self, ex);
					} else if(JSSpec._assertionFailure) {
						JSSpec._secondPass = true;
						self.run();
					} else {
						self.onException(self, ex);
					}
				}
			}
		},
		0
	);
};



/**
 * CompositeExecutor composites one or more executors and execute them sequencially.
 */
JSSpec.CompositeExecutor = function(onSuccess, onException, continueOnException) {
	this.queue = [];
	this.onSuccess = typeof onSuccess == 'function' ? onSuccess : JSSpec.EMPTY_FUNCTION;
	this.onException = typeof onException == 'function' ? onException : JSSpec.EMPTY_FUNCTION;
	this.continueOnException = !!continueOnException;
};

JSSpec.CompositeExecutor.prototype.addFunction = function(func) {
	this.addExecutor(new JSSpec.Executor(func));
};

JSSpec.CompositeExecutor.prototype.addExecutor = function(executor) {
	var last = this.queue.length == 0 ? null : this.queue[this.queue.length - 1];
	if(last) {
		last.next = executor;
	}
	
	executor.parent = this;
	executor.onSuccessBackup = executor.onSuccess;
	executor.onSuccess = function(result) {
		this.onSuccessBackup(result);
		if(this.next) {
			this.next.run();
		} else {
			this.parent.onSuccess();
		}
	};
	executor.onExceptionBackup = executor.onException;
	executor.onException = function(executor, ex) {
		this.onExceptionBackup(executor, ex);

		if(this.parent.continueOnException) {
			if(this.next) {
				this.next.run();
			} else {
				this.parent.onSuccess();
			}
		} else {
			this.parent.onException(executor, ex);
		}
	};

	this.queue.push(executor);
};

JSSpec.CompositeExecutor.prototype.run = function() {
	if(this.queue.length > 0) {
		this.queue[0].run();
	}
};

/**
 * Spec is a set of Examples in a specific context
 */
JSSpec.Spec = function(context, entries) {
	this.id = JSSpec.Spec.id++;
	this.context = context;
	this.url = location.href;
	
	this.filterEntriesByEmbeddedExpressions(entries);
	this.extractOutSpecialEntries(entries);
	this.examples = this.makeExamplesFromEntries(entries);
	this.examplesMap = this.makeMapFromExamples(this.examples);
};

JSSpec.Spec.id = 0;
JSSpec.Spec.prototype.getExamples = function() {
	return this.examples;
};

JSSpec.Spec.prototype.hasException = function() {
	return this.getTotalFailures() > 0 || this.getTotalErrors() > 0;
};

JSSpec.Spec.prototype.getTotalFailures = function() {
	var examples = this.examples;
	var failures = 0;
	for(var i = 0; i < examples.length; i++) {
		if(examples[i].isFailure()) failures++;
	}
	return failures;
};

JSSpec.Spec.prototype.getTotalErrors = function() {
	var examples = this.examples;
	var errors = 0;
	for(var i = 0; i < examples.length; i++) {
		if(examples[i].isError()) errors++;
	}
	return errors;
};

JSSpec.Spec.prototype.filterEntriesByEmbeddedExpressions = function(entries) {
	var isTrue;
	for(name in entries) {
		var m = name.match(/\[\[(.+)\]\]/);
		if(m && m[1]) {
			eval("isTrue = (" + m[1] + ")");
			if(!isTrue) delete entries[name];
		}
	}
};

JSSpec.Spec.prototype.extractOutSpecialEntries = function(entries) {
	this.beforeEach = JSSpec.EMPTY_FUNCTION;
	this.beforeAll = JSSpec.EMPTY_FUNCTION;
	this.afterEach = JSSpec.EMPTY_FUNCTION;
	this.afterAll = JSSpec.EMPTY_FUNCTION;
	
	for(name in entries) {
		if(name == 'before' || name == 'before each' || name == 'before_each') {
			this.beforeEach = entries[name];
		} else if(name == 'before all' || name == 'before_all') {
			this.beforeAll = entries[name];
		} else if(name == 'after' || name == 'after each' || name == 'after_each') {
			this.afterEach = entries[name];
		} else if(name == 'after all' || name == 'after_all') {
			this.afterAll = entries[name];
		}
	}
	
	delete entries['before'];
	delete entries['before each'];
	delete entries['before_each'];
	delete entries['before all'];
	delete entries['before_all'];
	delete entries['after'];
	delete entries['after each'];
	delete entries['after_each'];
	delete entries['after all'];
	delete entries['after_all'];
};

JSSpec.Spec.prototype.makeExamplesFromEntries = function(entries) {
	var examples = [];
	for(name in entries) {
		examples.push(new JSSpec.Example(name, entries[name], this.beforeEach, this.afterEach));
	}
	return examples;
};

JSSpec.Spec.prototype.makeMapFromExamples = function(examples) {
	var map = {};
	for(var i = 0; i < examples.length; i++) {
		var example = examples[i];
		map[example.id] = examples[i];
	}
	return map;
};

JSSpec.Spec.prototype.getExampleById = function(id) {
	return this.examplesMap[id];
};

JSSpec.Spec.prototype.getExecutor = function() {
	var self = this;
	var onException = function(executor, ex) {
		self.exception = ex;
	};
	
	var composite = new JSSpec.CompositeExecutor();
	composite.addFunction(function() {JSSpec.log.onSpecStart(self);});
	composite.addExecutor(new JSSpec.Executor(this.beforeAll, null, function(exec, ex) {
		self.exception = ex;
		JSSpec.log.onSpecEnd(self);
	}));
	
	var exampleAndAfter = new JSSpec.CompositeExecutor(null,null,true);
	for(var i = 0; i < this.examples.length; i++) {
		exampleAndAfter.addExecutor(this.examples[i].getExecutor());
	}
	exampleAndAfter.addExecutor(new JSSpec.Executor(this.afterAll, null, onException));
	exampleAndAfter.addFunction(function() {JSSpec.log.onSpecEnd(self);});
	composite.addExecutor(exampleAndAfter);
	
	return composite;
};

/**
 * Example
 */
JSSpec.Example = function(name, target, before, after) {
	this.id = JSSpec.Example.id++;
	this.name = name;
	this.target = target;
	this.before = before;
	this.after = after;
};

JSSpec.Example.id = 0;
JSSpec.Example.prototype.isFailure = function() {
	return this.exception && this.exception.type == "failure";
};

JSSpec.Example.prototype.isError = function() {
	return this.exception && !this.exception.type;
};

JSSpec.Example.prototype.getExecutor = function() {
	var self = this;
	var onException = function(executor, ex) {
		self.exception = ex;
	};
	
	var composite = new JSSpec.CompositeExecutor();
	composite.addFunction(function() {JSSpec.log.onExampleStart(self);});
	composite.addExecutor(new JSSpec.Executor(this.before, null, function(exec, ex) {
		self.exception = ex;
		JSSpec.log.onExampleEnd(self);
	}));
	
	var targetAndAfter = new JSSpec.CompositeExecutor(null,null,true);
	
	targetAndAfter.addExecutor(new JSSpec.Executor(this.target, null, onException));
	targetAndAfter.addExecutor(new JSSpec.Executor(this.after, null, onException));
	targetAndAfter.addFunction(function() {JSSpec.log.onExampleEnd(self);});
	
	composite.addExecutor(targetAndAfter);
	
	return composite;
};

/**
 * Runner
 */
JSSpec.Runner = function(specs, logger) {
	JSSpec.log = logger;
	
	this.totalExamples = 0;
	this.specs = [];
	this.specsMap = {};
	this.addAllSpecs(specs);
};

JSSpec.Runner.prototype.addAllSpecs = function(specs) {
	for(var i = 0; i < specs.length; i++) {
		this.addSpec(specs[i]);
	}
};

JSSpec.Runner.prototype.addSpec = function(spec) {
	this.specs.push(spec);
	this.specsMap[spec.id] = spec;
	this.totalExamples += spec.getExamples().length;
};

JSSpec.Runner.prototype.getSpecById = function(id) {
	return this.specsMap[id];
};

JSSpec.Runner.prototype.getSpecByContext = function(context) {
	for(var i = 0; i < this.specs.length; i++) {
		if(this.specs[i].context == context) return this.specs[i];
	}
	return null;
};

JSSpec.Runner.prototype.getSpecs = function() {
	return this.specs;
};

JSSpec.Runner.prototype.hasException = function() {
	return this.getTotalFailures() > 0 || this.getTotalErrors() > 0;
};

JSSpec.Runner.prototype.getTotalFailures = function() {
	var specs = this.specs;
	var failures = 0;
	for(var i = 0; i < specs.length; i++) {
		failures += specs[i].getTotalFailures();
	}
	return failures;
};

JSSpec.Runner.prototype.getTotalErrors = function() {
	var specs = this.specs;
	var errors = 0;
	for(var i = 0; i < specs.length; i++) {
		errors += specs[i].getTotalErrors();
	}
	return errors;
};


JSSpec.Runner.prototype.run = function() {
	JSSpec.log.onRunnerStart();
	var executor = new JSSpec.CompositeExecutor(function() {JSSpec.log.onRunnerEnd()},null,true);
	for(var i = 0; i < this.specs.length; i++) {
		executor.addExecutor(this.specs[i].getExecutor());
	}
	executor.run();
};


JSSpec.Runner.prototype.rerun = function(context) {
	JSSpec.runner = new JSSpec.Runner([this.getSpecByContext(context)], JSSpec.log);
	JSSpec.runner.run();
};

/**
 * Logger
 */
JSSpec.Logger = function() {
	this.finishedExamples = 0;
	this.startedAt = null;
};

JSSpec.Logger.prototype.onRunnerStart = function() {
	this._title = document.title;

	this.startedAt = new Date();
	var container = document.getElementById('jsspec_container');
	if(container) {
		container.innerHTML = "";
	} else {
		container = document.createElement("DIV");
		container.id = "jsspec_container";
		document.body.appendChild(container);
	}
	
	var title = document.createElement("DIV");
	title.id = "title";
	title.innerHTML = [
		'<h1>JSSpec</h1>',
		'<ul>',
		JSSpec.options.rerun ? '<li>[<a href="?" title="rerun all specs">X</a>] ' + JSSpec.util.escapeTags(decodeURIComponent(JSSpec.options.rerun)) + '</li>' : '',
		'	<li><span id="total_examples">' + JSSpec.runner.totalExamples + '</span> examples</li>',
		'	<li><span id="total_failures">0</span> failures</li>',
		'	<li><span id="total_errors">0</span> errors</li>',
		'	<li><span id="progress">0</span>% done</li>',
		'	<li><span id="total_elapsed">0</span> secs</li>',
		'</ul>',
		'<p><a href="http://jania.pe.kr/aw/moin.cgi/JSSpec">JSSpec homepage</a></p>',
	].join("");
	container.appendChild(title);

	var list = document.createElement("DIV");
	list.id = "list";
	list.innerHTML = [
		'<h2>List</h2>',
		'<ul class="specs">',
		function() {
			var specs = JSSpec.runner.getSpecs();
			var sb = [];
			for(var i = 0; i < specs.length; i++) {
				var spec = specs[i];
                                // -Xv changed rerun link
				sb.push('<li id="spec_' + specs[i].id + '_list"><h3><a href="#spec_' + specs[i].id + '">' + JSSpec.util.escapeTags(specs[i].context) + '</a> [<a href="javascript:JSSpec.runner.rerun(decodeURIComponent(\'' + encodeURIComponent(specs[i].context) + '\'));">rerun</a>]</h3></li>');
			}
			return sb.join("");
		}(),
		'</ul>'
	].join("");
	container.appendChild(list);
	
	var log = document.createElement("DIV");
	log.id = "log";
	log.innerHTML = [
		'<h2>Log</h2>',
		'<ul class="specs">',
		function() {
			var specs = JSSpec.runner.getSpecs();
			var sb = [];
			for(var i = 0; i < specs.length; i++) {
				var spec = specs[i];
				sb.push('	<li id="spec_' + specs[i].id + '">');
                                // -Xv changed rerun link
				sb.push('		<h3>' + JSSpec.util.escapeTags(specs[i].context) + ' [<a href="javascript:JSSpec.runner.rerun(decodeURIComponent(\'' + encodeURIComponent(specs[i].context) + '\'));">rerun</a>]</h3>');
				sb.push('		<ul id="spec_' + specs[i].id + '_examples" class="examples">');
				for(var j = 0; j < spec.examples.length; j++) {
					var example = spec.examples[j];
					sb.push('			<li id="example_' + example.id + '">')
					sb.push('				<h4>' + JSSpec.util.escapeTags(example.name) + '</h4>')
					sb.push('			</li>')
				}
				sb.push('		</ul>');
				sb.push('	</li>');
			}
			return sb.join("");
		}(),
		'</ul>'
	].join("");
	
	container.appendChild(log);
	
	// add event handler for toggling
	var specs = JSSpec.runner.getSpecs();
	var sb = [];
	for(var i = 0; i < specs.length; i++) {
		var spec = document.getElementById("spec_" + specs[i].id);
		var title = spec.getElementsByTagName("H3")[0];
		title.onclick = function(e) {
			var target = document.getElementById(this.parentNode.id + "_examples");
			target.style.display = target.style.display == "none" ? "block" : "none";
			return true;
		}
	}
};

JSSpec.Logger.prototype.onRunnerEnd = function() {
	if(JSSpec.runner.hasException()) {
		var times = 4;
		var title1 = "*" + this._title;
		var title2 = "*F" + JSSpec.runner.getTotalFailures() + " E" + JSSpec.runner.getTotalErrors() + "* " + this._title;
	} else {
		var times = 2;
		var title1 = this._title;
		var title2 = "Success";
	}
	this.blinkTitle(times,title1,title2);
};

JSSpec.Logger.prototype.blinkTitle = function(times, title1, title2) {
	var times = times * 2;
	var mode = true;
	
	var f = function() {
		if(times > 0) {
			document.title = mode ? title1 : title2;
			mode = !mode;
			times--;
			window.setTimeout(f, 500);
		} else {
			document.title = title1;
		}
	};
	
	f();
};

JSSpec.Logger.prototype.onSpecStart = function(spec) {
	var spec_list = document.getElementById("spec_" + spec.id + "_list");
	var spec_log = document.getElementById("spec_" + spec.id);
	
	spec_list.className = "ongoing";
	spec_log.className = "ongoing";
};

JSSpec.Logger.prototype.onSpecEnd = function(spec) {
	var spec_list = document.getElementById("spec_" + spec.id + "_list");
	var spec_log = document.getElementById("spec_" + spec.id);
	var examples = document.getElementById("spec_" + spec.id + "_examples");
	var className = spec.hasException() ? "exception" : "success";

	spec_list.className = className;
	spec_log.className = className;

	if(JSSpec.options.autocollapse && !spec.hasException()) examples.style.display = "none";
	
	if(spec.exception) {
		heading.appendChild(document.createTextNode(" - " + spec.exception.message));
	}
};

JSSpec.Logger.prototype.onExampleStart = function(example) {
	var li = document.getElementById("example_" + example.id);
	li.className = "ongoing";
};

JSSpec.Logger.prototype.onExampleEnd = function(example) {
	var li = document.getElementById("example_" + example.id);
	li.className = example.exception ? "exception" : "success";
	
	if(example.exception) {
		var div = document.createElement("DIV");
		div.innerHTML = example.exception.message + "<p><br />" + " at " + example.exception.fileName + ", line " + example.exception.lineNumber + "</p>";
		li.appendChild(div);
	}
	
	var title = document.getElementById("title");
	var runner = JSSpec.runner;
	
	title.className = runner.hasException() ? "exception" : "success";
	
	this.finishedExamples++;
	document.getElementById("total_failures").innerHTML = runner.getTotalFailures();
	document.getElementById("total_errors").innerHTML = runner.getTotalErrors();
	var progress = parseInt(this.finishedExamples / runner.totalExamples * 100);
	document.getElementById("progress").innerHTML = progress;
	document.getElementById("total_elapsed").innerHTML = (new Date().getTime() - this.startedAt.getTime()) / 1000;
	
	document.title = progress + "%: " + this._title;
};

/**
 * IncludeMatcher
 */
JSSpec.IncludeMatcher = function(actual, expected, condition) {
	this.actual = actual;
	this.expected = expected;
	this.condition = condition;
	this.match = false;
	this.explaination = this.makeExplain();
};

JSSpec.IncludeMatcher.createInstance = function(actual, expected, condition) {
	return new JSSpec.IncludeMatcher(actual, expected, condition);
};

JSSpec.IncludeMatcher.prototype.matches = function() {
	return this.match;
};

JSSpec.IncludeMatcher.prototype.explain = function() {
	return this.explaination;
};

JSSpec.IncludeMatcher.prototype.makeExplain = function() {
	if(typeof this.actual.length == 'undefined') {
		return this.makeExplainForNotArray();
	} else {
		return this.makeExplainForArray();
	}
};

JSSpec.IncludeMatcher.prototype.makeExplainForNotArray = function() {
	if(this.condition) {
		this.match = !!this.actual[this.expected];
	} else {
		this.match = !this.actual[this.expected];
	}
	
	var sb = [];
	sb.push('<p>actual value:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, this.expected) + '</p>');
	sb.push('<p>should ' + (this.condition ? '' : 'not') + ' include:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected) + '</p>');
	return sb.join("");
}
;
JSSpec.IncludeMatcher.prototype.makeExplainForArray = function() {
	var matches;
	if(this.condition) {
		for(var i = 0; i < this.actual.length; i++) {
			matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches();
			if(matches) {
				this.match = true;
				break;
			}
		}
	} else {
		for(var i = 0; i < this.actual.length; i++) {
			matches = JSSpec.EqualityMatcher.createInstance(this.expected, this.actual[i]).matches();
			if(matches) {
				this.match = false;
				break;
			}
		}
	}
	
	if(this.match) return "";
	
	var sb = [];
	sb.push('<p>actual value:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, this.condition ? null : i) + '</p>');
	sb.push('<p>should ' + (this.condition ? '' : 'not') + ' include:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected) + '</p>');
	return sb.join("");
};

/**
 * PropertyLengthMatcher
 */
JSSpec.PropertyLengthMatcher = function(num, property, o, condition) {
	this.num = num;
	this.o = o;
	this.property = property;
	if((property == 'characters' || property == 'items') && typeof o.length != 'undefined') {
		this.property = 'length';
	}
	
	this.condition = condition;
	this.conditionMet = function(x) {
		if(condition == 'exactly') return x.length == num;
		if(condition == 'at least') return x.length >= num;
		if(condition == 'at most') return x.length <= num;

		throw "Unknown condition '" + condition + "'";
	};
	this.match = false;
	this.explaination = this.makeExplain();
};

JSSpec.PropertyLengthMatcher.prototype.makeExplain = function() {
	if(this.o._type == 'String' && this.property == 'length') {
		this.match = this.conditionMet(this.o);
		return this.match ? '' : this.makeExplainForString();
	} else if(typeof this.o.length != 'undefined' && this.property == "length") {
		this.match = this.conditionMet(this.o);
		return this.match ? '' : this.makeExplainForArray();
	} else if(typeof this.o[this.property] != 'undefined' && this.o[this.property] != null) {
		this.match = this.conditionMet(this.o[this.property]);
		return this.match ? '' : this.makeExplainForObject();
	} else if(typeof this.o[this.property] == 'undefined' || this.o[this.property] == null) {
		this.match = false;
		return this.makeExplainForNoProperty();
	}

	this.match = true;
};

JSSpec.PropertyLengthMatcher.prototype.makeExplainForString = function() {
	var sb = [];
	
	var exp = this.num == 0 ?
		'be an <strong>empty string</strong>' :
		'have <strong>' + this.condition + ' ' + this.num + ' characters</strong>';
	
	sb.push('<p>actual value has <strong>' + this.o.length + ' characters</strong>:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o) + '</p>');
	sb.push('<p>but it should ' + exp + '.</p>');
	
	return sb.join("");
};

JSSpec.PropertyLengthMatcher.prototype.makeExplainForArray = function() {
	var sb = [];
	
	var exp = this.num == 0 ?
		'be an <strong>empty array</strong>' :
		'have <strong>' + this.condition + ' ' + this.num + ' items</strong>';

	sb.push('<p>actual value has <strong>' + this.o.length + ' items</strong>:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o) + '</p>');
	sb.push('<p>but it should ' + exp + '.</p>');
	
	return sb.join("");
};

JSSpec.PropertyLengthMatcher.prototype.makeExplainForObject = function() {
	var sb = [];

	var exp = this.num == 0 ?
		'be <strong>empty</strong>' :
		'have <strong>' + this.condition + ' ' + this.num + ' ' + this.property + '.</strong>';

	sb.push('<p>actual value has <strong>' + this.o[this.property].length + ' ' + this.property + '</strong>:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o, false, this.property) + '</p>');
	sb.push('<p>but it should ' + exp + '.</p>');
	
	return sb.join("");
};

JSSpec.PropertyLengthMatcher.prototype.makeExplainForNoProperty = function() {
	var sb = [];
	
	sb.push('<p>actual value:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.o) + '</p>');
	sb.push('<p>should have <strong>' + this.condition + ' ' + this.num + ' ' + this.property + '</strong> but there\'s no such property.</p>');
	
	return sb.join("");
};

JSSpec.PropertyLengthMatcher.prototype.matches = function() {
	return this.match;
};

JSSpec.PropertyLengthMatcher.prototype.explain = function() {
	return this.explaination;
};

JSSpec.PropertyLengthMatcher.createInstance = function(num, property, o, condition) {
	return new JSSpec.PropertyLengthMatcher(num, property, o, condition);
};

/**
 * EqualityMatcher
 */
JSSpec.EqualityMatcher = {};

JSSpec.EqualityMatcher.createInstance = function(expected, actual) {
	if(expected == null || actual == null) {
		return new JSSpec.NullEqualityMatcher(expected, actual);
	} else if(expected._type && expected._type == actual._type) {
		if(expected._type == "String") {
			return new JSSpec.StringEqualityMatcher(expected, actual);
		} else if(expected._type == "Date") {
			return new JSSpec.DateEqualityMatcher(expected, actual);
		} else if(expected._type == "Number") {
			return new JSSpec.NumberEqualityMatcher(expected, actual);
		} else if(expected._type == "Array") {
			return new JSSpec.ArrayEqualityMatcher(expected, actual);
		} else if(expected._type == "Boolean") {
			return new JSSpec.BooleanEqualityMatcher(expected, actual);
		}
	}
	
	return new JSSpec.ObjectEqualityMatcher(expected, actual);
};

JSSpec.EqualityMatcher.basicExplain = function(expected, actual, expectedDesc, actualDesc) {
	var sb = [];
	
	sb.push(actualDesc || '<p>actual value:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(actual) + '</p>');
	sb.push(expectedDesc || '<p>should be:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(expected) + '</p>');
	
	return sb.join("");
};

JSSpec.EqualityMatcher.diffExplain = function(expected, actual) {
	var sb = [];

	sb.push('<p>diff:</p>');
	sb.push('<p style="margin-left:2em;">');
	
	var dmp = new diff_match_patch();
	var diff = dmp.diff_main(expected, actual);
	dmp.diff_cleanupEfficiency(diff);
	
	sb.push(JSSpec.util.inspect(dmp.diff_prettyHtml(diff), true));
	
	sb.push('</p>');
	
	return sb.join("");
};

/**
 * BooleanEqualityMatcher
 */
JSSpec.BooleanEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
};

JSSpec.BooleanEqualityMatcher.prototype.explain = function() {
	var sb = [];
	
	sb.push('<p>actual value:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
	sb.push('<p>should be:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected) + '</p>');
	
	return sb.join("");
};

JSSpec.BooleanEqualityMatcher.prototype.matches = function() {
	return this.expected == this.actual;
};

/**
 * NullEqualityMatcher
 */
JSSpec.NullEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
};

JSSpec.NullEqualityMatcher.prototype.matches = function() {
	return this.expected == this.actual && typeof this.expected == typeof this.actual;
};

JSSpec.NullEqualityMatcher.prototype.explain = function() {
	return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual);
};

JSSpec.DateEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
};

JSSpec.DateEqualityMatcher.prototype.matches = function() {
	return this.expected.getTime() == this.actual.getTime();
};

JSSpec.DateEqualityMatcher.prototype.explain = function() {
	var sb = [];
	
	sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
	sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected.toString(), this.actual.toString()));

	return sb.join("");
};

/**
 * ObjectEqualityMatcher
 */
JSSpec.ObjectEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
	this.match = this.expected == this.actual;
	this.explaination = this.makeExplain();
};

JSSpec.ObjectEqualityMatcher.prototype.matches = function() {return this.match};

JSSpec.ObjectEqualityMatcher.prototype.explain = function() {return this.explaination};

JSSpec.ObjectEqualityMatcher.prototype.makeExplain = function() {
	if(this.expected == this.actual) {
		this.match = true;
		return "";
	}
	
	if(JSSpec.util.isDomNode(this.expected)) {
		return this.makeExplainForDomNode();
	}
	
	var key, expectedHasItem, actualHasItem;

	for(key in this.expected) {
		expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined';
		actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined';
		if(expectedHasItem && !actualHasItem) return this.makeExplainForMissingItem(key);
	}
	for(key in this.actual) {
		expectedHasItem = this.expected[key] != null && typeof this.expected[key] != 'undefined';
		actualHasItem = this.actual[key] != null && typeof this.actual[key] != 'undefined';
		if(actualHasItem && !expectedHasItem) return this.makeExplainForUnknownItem(key);
	}
	
	for(key in this.expected) {
		var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[key], this.actual[key]);
		if(!matcher.matches()) return this.makeExplainForItemMismatch(key);
	}
		
	this.match = true;
};

JSSpec.ObjectEqualityMatcher.prototype.makeExplainForDomNode = function(key) {
	var sb = [];
	
	sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
	
	return sb.join("");
};

JSSpec.ObjectEqualityMatcher.prototype.makeExplainForMissingItem = function(key) {
	var sb = [];

	sb.push('<p>actual value has no item named <strong>' + JSSpec.util.inspect(key) + '</strong></p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, key) + '</p>');
	sb.push('<p>but it should have the item whose value is <strong>' + JSSpec.util.inspect(this.expected[key]) + '</strong></p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, key) + '</p>');
	
	return sb.join("");
};

JSSpec.ObjectEqualityMatcher.prototype.makeExplainForUnknownItem = function(key) {
	var sb = [];

	sb.push('<p>actual value has item named <strong>' + JSSpec.util.inspect(key) + '</strong></p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, key) + '</p>');
	sb.push('<p>but there should be no such item</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, key) + '</p>');
	
	return sb.join("");
};

JSSpec.ObjectEqualityMatcher.prototype.makeExplainForItemMismatch = function(key) {
	var sb = [];

	sb.push('<p>actual value has an item named <strong>' + JSSpec.util.inspect(key) + '</strong> whose value is <strong>' + JSSpec.util.inspect(this.actual[key]) + '</strong></p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, key) + '</p>');
	sb.push('<p>but it\'s value should be <strong>' + JSSpec.util.inspect(this.expected[key]) + '</strong></p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, key) + '</p>');
	
	return sb.join("");
};




/**
 * ArrayEqualityMatcher
 */
JSSpec.ArrayEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
	this.match = this.expected == this.actual;
	this.explaination = this.makeExplain();
};

JSSpec.ArrayEqualityMatcher.prototype.matches = function() {return this.match};

JSSpec.ArrayEqualityMatcher.prototype.explain = function() {return this.explaination};

JSSpec.ArrayEqualityMatcher.prototype.makeExplain = function() {
	if(this.expected.length != this.actual.length) return this.makeExplainForLengthMismatch();
	
	for(var i = 0; i < this.expected.length; i++) {
		var matcher = JSSpec.EqualityMatcher.createInstance(this.expected[i], this.actual[i]);
		if(!matcher.matches()) return this.makeExplainForItemMismatch(i);
	}
		
	this.match = true;
};

JSSpec.ArrayEqualityMatcher.prototype.makeExplainForLengthMismatch = function() {
	return JSSpec.EqualityMatcher.basicExplain(
		this.expected,
		this.actual,
		'<p>but it should be <strong>' + this.expected.length + '</strong></p>',
		'<p>actual value has <strong>' + this.actual.length + '</strong> items</p>'
	);
};

JSSpec.ArrayEqualityMatcher.prototype.makeExplainForItemMismatch = function(index) {
	var postfix = ["th", "st", "nd", "rd", "th"][Math.min((index + 1) % 10,4)];
	
	var sb = [];

	sb.push('<p>' + (index + 1) + postfix + ' item (index ' + index + ') of actual value is <strong>' + JSSpec.util.inspect(this.actual[index]) + '</strong>:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual, false, index) + '</p>');
	sb.push('<p>but it should be <strong>' + JSSpec.util.inspect(this.expected[index]) + '</strong>:</p>');
	sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.expected, false, index) + '</p>');
	
	return sb.join("");
};

/**
 * NumberEqualityMatcher
 */
JSSpec.NumberEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
};

JSSpec.NumberEqualityMatcher.prototype.matches = function() {
	if(this.expected == this.actual) return true;
};

JSSpec.NumberEqualityMatcher.prototype.explain = function() {
	return JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual);
};

/**
 * StringEqualityMatcher
 */
JSSpec.StringEqualityMatcher = function(expected, actual) {
	this.expected = expected;
	this.actual = actual;
};

JSSpec.StringEqualityMatcher.prototype.matches = function() {
	if(this.expected == this.actual) return true;
};

JSSpec.StringEqualityMatcher.prototype.explain = function() {
	var sb = [];

	sb.push(JSSpec.EqualityMatcher.basicExplain(this.expected, this.actual));
	sb.push(JSSpec.EqualityMatcher.diffExplain(this.expected, this.actual));	
	return sb.join("");
};

/**
 * PatternMatcher
 */
JSSpec.PatternMatcher = function(actual, pattern, condition) {
	this.actual = actual;
	this.pattern = pattern;
	this.condition = condition;
	this.match = false;
	this.explaination = this.makeExplain();
};

JSSpec.PatternMatcher.createInstance = function(actual, pattern, condition) {
	return new JSSpec.PatternMatcher(actual, pattern, condition);
};

JSSpec.PatternMatcher.prototype.makeExplain = function() {
	var sb;
	if(this.actual == null || this.actual._type != 'String') {
		sb = [];
		sb.push('<p>actual value:</p>');
		sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
		sb.push('<p>should ' + (this.condition ? '' : 'not') + ' match with pattern:</p>');
		sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.pattern) + '</p>');
		sb.push('<p>but pattern matching cannot be performed.</p>');
		return sb.join("");
	} else {
		this.match = this.condition == !!this.actual.match(this.pattern);
		if(this.match) return "";
		
		sb = [];
		sb.push('<p>actual value:</p>');
		sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.actual) + '</p>');
		sb.push('<p>should ' + (this.condition ? '' : 'not') + ' match with pattern:</p>');
		sb.push('<p style="margin-left:2em;">' + JSSpec.util.inspect(this.pattern) + '</p>');
		return sb.join("");
	}
};

JSSpec.PatternMatcher.prototype.matches = function() {
	return this.match;
};

JSSpec.PatternMatcher.prototype.explain = function() {
	return this.explaination;
};

/**
 * Domain Specific Languages
 */
JSSpec.DSL = {};

JSSpec.DSL.forString = {
	normalizeHtml: function() {
		var html = this;
		
		// Uniformize quotation, turn tag names and attribute names into lower case
		html = html.replace(/<(\/?)(\w+)([^>]*?)>/img, function(str, closingMark, tagName, attrs) {
			var sortedAttrs = JSSpec.util.sortHtmlAttrs(JSSpec.util.correctHtmlAttrQuotation(attrs).toLowerCase())
			return "<" + closingMark + tagName.toLowerCase() + sortedAttrs + ">"
		});
		
		// validation self-closing tags
		html = html.replace(/<(br|hr|img)([^>]*?)>/mg, function(str, tag, attrs) {
			return "<" + tag + attrs + " />";
		});
		
		// append semi-colon at the end of style value
		html = html.replace(/style="(.*?)"/mg, function(str, styleStr) {
			styleStr = JSSpec.util.sortStyleEntries(styleStr.strip()); // for Safari
			if(styleStr.charAt(styleStr.length - 1) != ';') styleStr += ";"
			
			return 'style="' + styleStr + '"'
		});
		
		// sort style entries
		
		// remove empty style attributes
		html = html.replace(/ style=";"/mg, "");
		
		// remove new-lines
		html = html.replace(/\r/mg, '');
		html = html.replace(/\n/mg, '');
			
		return html;
	}
};



JSSpec.DSL.describe = function(context, entries) {
	JSSpec.specs.push(new JSSpec.Spec(context, entries));
};

JSSpec.DSL.value_of = function(target) {
	if(JSSpec._secondPass) return {};
	
	var subject = new JSSpec.DSL.Subject(target);
	return subject;
};

JSSpec.DSL.Subject = function(target) {
	this.target = target;
};

JSSpec.DSL.Subject.prototype._type = 'Subject';

JSSpec.DSL.Subject.prototype.should_fail = function(message) {
	JSSpec._assertionFailure = {message:message};
	throw JSSpec._assertionFailure;
};

JSSpec.DSL.Subject.prototype.should_be = function(expected) {
	var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target);
	if(!matcher.matches()) {
		JSSpec._assertionFailure = {message:matcher.explain()};
		throw JSSpec._assertionFailure;
	}
};

JSSpec.DSL.Subject.prototype.should_not_be = function(expected) {
	// TODO JSSpec.EqualityMatcher should support 'condition'
	var matcher = JSSpec.EqualityMatcher.createInstance(expected, this.target);
	if(matcher.matches()) {
		JSSpec._assertionFailure = {message:"'" + this.target + "' should not be '" + expected + "'"};
		throw JSSpec._assertionFailure;
	}
};

JSSpec.DSL.Subject.prototype.should_be_empty = function() {
	this.should_have(0, this.getType() == 'String' ? 'characters' : 'items');
};

JSSpec.DSL.Subject.prototype.should_not_be_empty = function() {
	this.should_have_at_least(1, this.getType() == 'String' ? 'characters' : 'items');
};

JSSpec.DSL.Subject.prototype.should_be_true = function() {
	this.should_be(true);
};

JSSpec.DSL.Subject.prototype.should_be_false = function() {
	this.should_be(false);
};

JSSpec.DSL.Subject.prototype.should_be_null = function() {
	this.should_be(null);
};

JSSpec.DSL.Subject.prototype.should_be_undefined = function() {
	this.should_be(undefined);
};

JSSpec.DSL.Subject.prototype.should_not_be_null = function() {
	this.should_not_be(null);
};

JSSpec.DSL.Subject.prototype.should_not_be_undefined = function() {
	this.should_not_be(undefined);
};

JSSpec.DSL.Subject.prototype._should_have = function(num, property, condition) {
	var matcher = JSSpec.PropertyLengthMatcher.createInstance(num, property, this.target, condition);
	if(!matcher.matches()) {
		JSSpec._assertionFailure = {message:matcher.explain()};
		throw JSSpec._assertionFailure;
	}
};

JSSpec.DSL.Subject.prototype.should_have = function(num, property) {
	this._should_have(num, property, "exactly");
};

JSSpec.DSL.Subject.prototype.should_have_exactly = function(num, property) {
	this._should_have(num, property, "exactly");
};

JSSpec.DSL.Subject.prototype.should_have_at_least = function(num, property) {
	this._should_have(num, property, "at least");
};

JSSpec.DSL.Subject.prototype.should_have_at_most = function(num, property) {
	this._should_have(num, property, "at most");
};

JSSpec.DSL.Subject.prototype.should_include = function(expected) {
	var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, true);
	if(!matcher.matches()) {
		JSSpec._assertionFailure = {message:matcher.explain()};
		throw JSSpec._assertionFailure;
	}
};

JSSpec.DSL.Subject.prototype.should_not_include = function(expected) {
	var matcher = JSSpec.IncludeMatcher.createInstance(this.target, expected, false);
	if(!matcher.matches()) {
		JSSpec._assertionFailure = {message:matcher.explain()};
		throw JSSpec._assertionFailure;
	}
};

JSSpec.DSL.Subject.prototype.should_match = function(pattern) {
	var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, true);
	if(!matcher.matches()) {
		JSSpec._assertionFailure = {message:matcher.explain()};
		throw JSSpec._assertionFailure;
	}
}
JSSpec.DSL.Subject.prototype.should_not_match = function(pattern) {
	var matcher = JSSpec.PatternMatcher.createInstance(this.target, pattern, false);
	if(!matcher.matches()) {
		JSSpec._assertionFailure = {message:matcher.explain()};
		throw JSSpec._assertionFailure;
	}
};

JSSpec.DSL.Subject.prototype.getType = function() {
	if(typeof this.target == 'undefined') {
		return 'undefined';
	} else if(this.target == null) {
		return 'null';
	} else if(this.target._type) {
		return this.target._type;
	} else if(JSSpec.util.isDomNode(this.target)) {
		return 'DomNode';
	} else {
		return 'object';
	}
};

/**
 * Utilities
 */
JSSpec.util = {
	escapeTags: function(string) {
		return string.replace(/</img, '&lt;').replace(/>/img, '&gt;');
	},
	parseOptions: function(defaults) {
		var options = defaults;
		
		var url = location.href;
		var queryIndex = url.indexOf('?');
		if(queryIndex == -1) return options;
		
		var query = url.substring(queryIndex + 1);
		var pairs = query.split('&');
		for(var i = 0; i < pairs.length; i++) {
			var tokens = pairs[i].split('=');
			options[tokens[0]] = tokens[1];
		}
		
		return options;
	},
	correctHtmlAttrQuotation: function(html) {
		html = html.replace(/(\w+)=['"]([^'"]+)['"]/mg,function (str, name, value) {return name + '=' + '"' + value + '"';});
		html = html.replace(/(\w+)=([^ '"]+)/mg,function (str, name, value) {return name + '=' + '"' + value + '"';});
		html = html.replace(/'/mg, '"');
		
		return html;
	},
	sortHtmlAttrs: function(html) {
		var attrs = [];
		html.replace(/((\w+)="[^"]+")/mg, function(str, matched) {
			attrs.push(matched);
		});
		return attrs.length == 0 ? "" : " " + attrs.sort().join(" ");
	},
	sortStyleEntries: function(styleText) {
		var entries = styleText.split(/; /);
		return entries.sort().join("; ");
	},
	escapeHtml: function(str) {
		if(!this._div) {
			this._div = document.createElement("DIV");
			this._text = document.createTextNode('');
			this._div.appendChild(this._text);
		}
		this._text.data = str;
		return this._div.innerHTML;
	},
	isDomNode: function(o) {
		// TODO: make it more stricter
		return (typeof o.nodeName == 'string') && (typeof o.nodeType == 'number');
	},
	inspectDomPath: function(o) {
		var sb = [];
		while(o && o.nodeName != '#document' && o.parent) {
			var siblings = o.parentNode.childNodes;
			for(var i = 0; i < siblings.length; i++) {
				if(siblings[i] == o) {
					sb.push(o.nodeName + (i == 0 ? '' : '[' + i + ']'));
					break;
				}
			}
			o = o.parentNode;
		}
		return sb.join(" &gt; ");
	},
	inspectDomNode: function(o) {
		if(o.nodeType == 1) {
			var nodeName = o.nodeName.toLowerCase();
			var sb = [];
			sb.push('<span class="dom_value">');
			sb.push("&lt;");
			sb.push(nodeName);
			
			var attrs = o.attributes;
			for(var i = 0; i < attrs.length; i++) {
				if(
					attrs[i].nodeValue &&
					attrs[i].nodeName != 'contentEditable' &&
					attrs[i].nodeName != 'style' &&
					typeof attrs[i].nodeValue != 'function'
				) sb.push(' <span class="dom_attr_name">' + attrs[i].nodeName.toLowerCase() + '</span>=<span class="dom_attr_value">"' + attrs[i].nodeValue + '"</span>');
			}
			if(o.style && o.style.cssText) {
				sb.push(' <span class="dom_attr_name">style</span>=<span class="dom_attr_value">"' + o.style.cssText + '"</span>');
			}
			sb.push('&gt;');
			sb.push(JSSpec.util.escapeHtml(o.innerHTML));
			sb.push('&lt;/' + nodeName + '&gt;');
			sb.push(' <span class="dom_path">(' + JSSpec.util.inspectDomPath(o) + ')</span>' );
			sb.push('</span>');
			return sb.join("");
		} else if(o.nodeType == 3) {
			return '<span class="dom_value">#text ' + o.nodeValue + '</span>';
		} else {
			return '<span class="dom_value">UnknownDomNode</span>';
		}
	},
	inspect: function(o, dontEscape, emphasisKey) {
		var sb, inspected;

		if(typeof o == 'undefined') return '<span class="undefined_value">undefined</span>';
		if(o == null) return '<span class="null_value">null</span>';
		if(o._type == 'String') return '<span class="string_value">"' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '"</span>';

		if(o._type == 'Date') {
			return '<span class="date_value">"' + o.toString() + '"</span>';
		}
		
		if(o._type == 'Number') return '<span class="number_value">' + (dontEscape ? o : JSSpec.util.escapeHtml(o)) + '</span>';
		
		if(o._type == 'Boolean') return '<span class="boolean_value">' + o + '</span>';

		if(o._type == 'RegExp') return '<span class="regexp_value">' + JSSpec.util.escapeHtml(o.toString()) + '</span>';

		if(JSSpec.util.isDomNode(o)) return JSSpec.util.inspectDomNode(o);

		if(o._type == 'Array' || typeof o.length != 'undefined') {
			sb = [];
			for(var i = 0; i < o.length; i++) {
				inspected = JSSpec.util.inspect(o[i]);
				sb.push(i == emphasisKey ? ('<strong>' + inspected + '</strong>') : inspected);
			}
			return '<span class="array_value">[' + sb.join(', ') + ']</span>';
		}
		
		// object
		sb = [];
		for(var key in o) {
			if(key == 'should') continue;
			
			inspected = JSSpec.util.inspect(key) + ":" + JSSpec.util.inspect(o[key]);
			sb.push(key == emphasisKey ? ('<strong>' + inspected + '</strong>') : inspected);
		}
		return '<span class="object_value">{' + sb.join(', ') + '}</span>';
	}
};

describe = JSSpec.DSL.describe;
behavior_of = JSSpec.DSL.describe;
value_of = JSSpec.DSL.value_of;
expect = JSSpec.DSL.value_of; // @deprecated

String.prototype._type = "String";
Number.prototype._type = "Number";
Date.prototype._type = "Date";
Array.prototype._type = "Array";
Boolean.prototype._type = "Boolean";
RegExp.prototype._type = "RegExp";

var targets = [Array.prototype, Date.prototype, Number.prototype, String.prototype, Boolean.prototype, RegExp.prototype];

String.prototype.normalizeHtml = JSSpec.DSL.forString.normalizeHtml;
String.prototype.asHtml = String.prototype.normalizeHtml; //@deprecated



/**
 * Main
 */
JSSpec.defaultOptions = {
	autorun: 1,
	specIdBeginsWith: 0,
	exampleIdBeginsWith: 0,
	autocollapse: 1
};
JSSpec.options = JSSpec.util.parseOptions(JSSpec.defaultOptions);

JSSpec.Spec.id = JSSpec.options.specIdBeginsWith;
JSSpec.Example.id = JSSpec.options.exampleIdBeginsWith;


// -Xv: removed window.onload
JSSpec.onload = function() {
	if(JSSpec.specs.length > 0) {
		if(!JSSpec.options.inSuite) {
			JSSpec.runner = new JSSpec.Runner(JSSpec.specs, new JSSpec.Logger());
			if(JSSpec.options.rerun) {
				JSSpec.runner.rerun(decodeURIComponent(JSSpec.options.rerun));
			} else {
				JSSpec.runner.run();
			}
		} else {
			// in suite, send all specs to parent
			var parentWindow = window.frames.parent.window;
			for(var i = 0; i < JSSpec.specs.length; i++) {
				parentWindow.JSSpec.specs.push(JSSpec.specs[i]);
			}
		}
	} else {
		var links = document.getElementById('list').getElementsByTagName('A');
		var frameContainer = document.createElement('DIV');
		frameContainer.style.display = 'none';
		document.body.appendChild(frameContainer);
		
		for(var i = 0; i < links.length; i++) {
			var frame = document.createElement('IFRAME');
			frame.src = links[i].href + '?inSuite=0&specIdBeginsWith=' + (i * 10000) + '&exampleIdBeginsWith=' + (i * 10000);
			frameContainer.appendChild(frame);
		}
	}
}
//}}}
/***
From http://code.google.com/p/jsspec/source/browse/trunk/JSSpec.css?r=105
with some modifications
***/

/*{{{*/
.jsspec_container {
  overflow: auto;
  padding: 0;
  margin: 0;
  width: 100%;
  height: 100%;
}

#jsspec_title,
.jsspec_container > .exception,
.jsspec_container > .success
{
  padding: 0em 0.5em;
  margin: 0;
  overflow: auto;
}

.jsspec_list {
  padding: 0;
  margin: 0;
  overflow: auto;
  width: 50%;   
}

.jsspec_log {
  padding: 0;
  margin: 0;
  overflow: auto;
}



/* --------------------
 * @Decorations and colors
 */
.jsspec_container * {
	padding: 0;
	margin: 0;
	font-family: "Lucida Grande", Helvetica, sans-serif;
}

.jsspec_container li {
	list-style: none;
}

/* title section */
#jsspec_title,
.jsspec_container > .exception,
.jsspec_container > .success
 {
	padding: 0em 0.5em;
}

#jsspec_title h1,
.jsspec_container > .exception h1,
.jsspec_container > .success h1
{
	font-size: 1.5em;
	float: left;
	border: none;
	color: #FFFFFF;
}

#jsspec_title ul li,
.jsspec_container > .exception ul li,
.jsspec_container > .success ul li
{
	float: left;
	padding: 0.5em 0em 0.5em 0.75em;
}

#jsspec_title p,
.jsspec_container > .exception p,
.jsspec_container > .success p
{
	float:right;
	margin-right:1em;
	font-size: 0.75em;
}

/* spec container */
.jsspec_container ul.specs {
	margin: 0.5em;
}
.jsspec_container ul.specs li {
	margin-bottom: 0.1em;
}

/* spec title */
.jsspec_container ul.specs li h3 {
	font-weight: bold;
	/*font-size: 0.75em;*/
	padding: 0.2em 1em;
	cursor: pointer;
	_cursor: hand;
}

/* example container */
.jsspec_container ul.examples li {
	border-style: solid;
	border-width: 0px 0px 1px 5px;
	margin: 0.2em 0em 0.2em 1em;
}

/* example title */
.jsspec_container ul.examples li h4 {
	font-weight: normal;
	/*font-size: 0.75em;*/
	margin-left: 1em;
}

/* example explaination */
.jsspec_container ul.examples li div {
	padding: 1em 2em;
	/*font-size: 0.75em;*/
}

/* styles for ongoing, success, failure, error */
.jsspec_container div.success, 
.jsspec_container div.success a {
	color: #FFFFFF;
	background-color: #65C400;
}

.jsspec_container ul.specs li.success h3, 
.jsspec_container ul.specs li.success h3 a {
	color: #FFFFFF;
	background-color: #65C400;
}

.jsspec_container ul.examples li.success, 
.jsspec_container ul.examples li.success a {
	color: #3D7700;
	background-color: #DBFFB4;
	border-color: #65C400;
}

.jsspec_container div.exception, 
.jsspec_container div.exception a {
	color: #FFFFFF;
	background-color: #C20000;
}

.jsspec_container ul.specs li.exception h3, 
.jsspec_container ul.specs li.exception h3 a {
	color: #FFFFFF;
	background-color: #C20000;
}

.jsspec_container ul.examples li.exception, 
.jsspec_container ul.examples li.exception a {
	color: #C20000;
	background-color: #FFFBD3;
	border-color: #C20000;
}

.jsspec_container div.ongoing, 
.jsspec_container div.ongoing a {
	color: #000000;
	background-color: #FFFF80;
}

.jsspec_container ul.specs li.ongoing h3, 
.jsspec_container ul.specs li.ongoing h3 a {
	color: #000000;
	background-color: #FFFF80;
}

.jsspec_container ul.examples li.ongoing, 
.jsspec_container ul.examples li.ongoing a {
	color: #000000;
	background-color: #FFFF80;
	border-color: #DDDD00;
}



/* --------------------
 * values
 */
.jsspec_container .number_value, 
.jsspec_container .string_value, 
.jsspec_container .regexp_value, 
.jsspec_container .boolean_value, 
.jsspec_container .dom_value {
	font-family: monospace;
	color: blue;
}
.jsspec_container .object_value, 
.jsspec_container .array_value {
	line-height: 2em;
	padding: 0.1em 0.2em;
	margin: 0.1em 0;
}
.jsspec_container .date_value {
	font-family: monospace;
	color: olive;
}
.jsspec_container .undefined_value, 
.jsspec_container .null_value {
	font-style: italic;
	color: blue;
}
.jsspec_container .dom_attr_name {
}
.jsspec_container .dom_attr_value {
	color: red;
}
.jsspec_container .dom_path {
	/*font-size: 0.75em;*/
	color: gray;
}
.jsspec_container strong {
	font-weight: normal;
	background-color: #FFC6C6;
}
/*}}}*/
/***
|''Name:''|LoadRemoteFileThroughProxy (previous LoadRemoteFileHijack)|
|''Description:''|When the TiddlyWiki file is located on the web (view over http) the content of [[SiteProxy]] tiddler is added in front of the file url. If [[SiteProxy]] does not exist "/proxy/" is added. |
|''Version:''|1.1.0|
|''Date:''|mar 17, 2007|
|''Source:''|http://tiddlywiki.bidix.info/#LoadRemoteFileHijack|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0|
***/
//{{{
version.extensions.LoadRemoteFileThroughProxy = {
 major: 1, minor: 1, revision: 0, 
 date: new Date("mar 17, 2007"), 
 source: "http://tiddlywiki.bidix.info/#LoadRemoteFileThroughProxy"};

if (!window.bidix) window.bidix = {}; // bidix namespace
if (!bidix.core) bidix.core = {};

bidix.core.loadRemoteFile = loadRemoteFile;
loadRemoteFile = function(url,callback,params)
{
 if ((document.location.toString().substr(0,4) == "http") && (url.substr(0,4) == "http")){ 
 url = store.getTiddlerText("SiteProxy", "/proxy/") + url;
 }
 return bidix.core.loadRemoteFile(url,callback,params);
}
//}}}
[[JSSpec|JSSpec and TiddlyWikis]]
[[See the code in action]]
[[Get the code]]
/***
|''Name:''|PasswordOptionPlugin|
|''Description:''|Extends TiddlyWiki options with non encrypted password option.|
|''Version:''|1.0.2|
|''Date:''|Apr 19, 2007|
|''Source:''|http://tiddlywiki.bidix.info/#PasswordOptionPlugin|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0 (Beta 5)|
***/
//{{{
version.extensions.PasswordOptionPlugin = {
	major: 1, minor: 0, revision: 2, 
	date: new Date("Apr 19, 2007"),
	source: 'http://tiddlywiki.bidix.info/#PasswordOptionPlugin',
	author: 'BidiX (BidiX (at) bidix (dot) info',
	license: '[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D]]',
	coreVersion: '2.2.0 (Beta 5)'
};

config.macros.option.passwordCheckboxLabel = "Save this password on this computer";
config.macros.option.passwordInputType = "password"; // password | text
setStylesheet(".pasOptionInput {width: 11em;}\n","passwordInputTypeStyle");

merge(config.macros.option.types, {
	'pas': {
		elementType: "input",
		valueField: "value",
		eventName: "onkeyup",
		className: "pasOptionInput",
		typeValue: config.macros.option.passwordInputType,
		create: function(place,type,opt,className,desc) {
			// password field
			config.macros.option.genericCreate(place,'pas',opt,className,desc);
			// checkbox linked with this password "save this password on this computer"
			config.macros.option.genericCreate(place,'chk','chk'+opt,className,desc);			
			// text savePasswordCheckboxLabel
			place.appendChild(document.createTextNode(config.macros.option.passwordCheckboxLabel));
		},
		onChange: config.macros.option.genericOnChange
	}
});

merge(config.optionHandlers['chk'], {
	get: function(name) {
		// is there an option linked with this chk ?
		var opt = name.substr(3);
		if (config.options[opt]) 
			saveOptionCookie(opt);
		return config.options[name] ? "true" : "false";
	}
});

merge(config.optionHandlers, {
	'pas': {
 		get: function(name) {
			if (config.options["chk"+name]) {
				return encodeCookie(config.options[name].toString());
			} else {
				return "";
			}
		},
		set: function(name,value) {config.options[name] = decodeCookie(value);}
	}
});

// need to reload options to load passwordOptions
loadOptionsCookie();

/*
if (!config.options['pasPassword'])
	config.options['pasPassword'] = '';

merge(config.optionsDesc,{
		pasPassword: "Test password"
	});
*/
//}}}
<jsspec>
describe('Plus operator (just for example)', {
	'should concatenate two strings': function() {
		value_of("Hello " + "World").should_be("Hello World");
	},
	'should add two numbers': function() {
		value_of(1 + 2).should_be(3);
	}
})

describe('"Should match"s', {
	'Should match': function() {
		value_of("Hello").should_match(/ell/);
	},
	'Should match 1': function() {
		value_of("Hello").should_match(/x/);
	},
	'Should match 2': function() {
		value_of([1,2,3]).should_match(/x/);
	},
	'Should not match 1': function() {
		value_of("Hello").should_not_match(/ell/);
	},
	'Should not match 2': function() {
		value_of([1,2,3]).should_not_match(/x/);
	}
})
describe('"Should include"s', {
	'Should include': function() {
		value_of([1,2,3]).should_include(4);
	},
	'Should not include': function() {
		value_of([1,2,3]).should_not_include(2);
	},
	'Should include / Non-array object': function() {
		value_of(new Date()).should_include(4);
	},
	'Should not include / Non-array object': function() {
		value_of(new Date()).should_not_include('getMonth');
	},
	'Should include 2': function() {
		value_of({a:1, b:2}).should_not_include('a');
	}
})

describe('"Should have"s', {
	'String length': function() {
		value_of("Hello").should_have(4, "characters");
	},
	'Array length': function() {
		value_of([1,2,3]).should_have(4, "items");
	},
	'Object\'s item length': function() {
		value_of({name:'Alan Kang', email:'jania902@gmail.com', accounts:['A', 'B']}).should_have(3, "accounts");
	},
	'No match': function() {
		value_of("This is a string").should_have(5, "players");
	},
	'Exactly': function() {
		value_of([1,2,3]).should_have_exactly(2, "items");
	},
	'At least': function() {
		value_of([1,2,3]).should_have_at_least(4, "items");
	},
	'At most': function() {
		value_of([1,2,3]).should_have_at_most(2, "items");
	},
	'Member': function() {
		value_of({x: 0}).should_have_member('x'); // true
		value_of({x: 0}).should_have_member('y'); // false
	}
})
describe('"Should be empty"s', {
	'String': function() {
		value_of("Hello").should_be_empty();
	},
	'Array': function() {
		value_of([1,2,3]).should_be_empty();
	},
	'Object\'s item': function() {
		value_of({name:'Alan Kang', email:'jania902@gmail.com', accounts:['A', 'B']}).should_have(0, "accounts");
	}
})

describe('Failure messages', {
	'Should be (String)': function() {
		value_of("Hello World").should_be("Good-bye world");
	},
	'Should have (Object\s item)': function() {
		value_of({name:'Alan Kang', email:'jania902@gmail.com', accounts:['A', 'B']}).should_have(3, "accounts");
	},
	'Should have at least': function() {
		value_of([1,2,3]).should_have_at_least(4, "items");
	},
	'Should include': function() {
		value_of([1,2,3]).should_include(4);
	},
	'Should match': function() {
		value_of("Hello").should_match(/bye/);
	}
})

describe('"Should be"s', {
	'String mismatch': function() {
		value_of("Hello world").should_be("Good-bye world");
	},
	'Array item mismatch': function() {
		value_of(['ab','cd','ef']).should_be(['ab','bd','ef']);
	},
	'Array length mismatch': function() {
		value_of(['a',2,'4',5]).should_be([1,2,[4,5,6],6,7]);
	},
	'Undefined value': function() {
		value_of("Test").should_be(undefined);
	},
	'Null value': function() {
		value_of(null).should_be("Test");
	},
	'Boolean value 1': function() {
		value_of(true).should_be(false);
	},
	'Boolean value 2': function() {
		value_of(false).should_be_true();
	},
	'Boolean value 3': function() {
		value_of(true).should_be_false();
	},
	'Number mismatch': function() {
		value_of(1+2).should_be(4);
	},
	'Date mismatch': function() {
		value_of(new Date(1979, 3, 27)).should_be(new Date(1976, 7, 23));
	},
	'Object mismatch 1': function() {
		var actual = {a:1, b:2};
		var expected = {a:1, b:2, d:3};
		
		value_of(actual).should_be(expected);
	},
	'Object mismatch 2': function() {
		var actual = {a:1, b:2, c:3, d:4};
		var expected = {a:1, b:2, c:3};
		
		value_of(actual).should_be(expected);
	},
	'Object mismatch 3': function() {
		var actual = {a:1, b:4, c:3};
		var expected = {a:1, b:2, c:3};
		
		value_of(actual).should_be(expected);
	},
	'null should be null': function() {
		value_of(null).should_be(null);
	},
	'null should not be undefined': function() {
		value_of(null).should_be(undefined);
	},
	'null should not be null': function() {
		value_of(null).should_not_be(null);
	},
	'empty array 1': function() {
		value_of([]).should_be_empty();
		value_of([1]).should_be_empty();
	},
	'empty array 2': function() {
		value_of([1]).should_not_be_empty();
		value_of([]).should_not_be_empty();
	}
})

describe('Equality operator', {
	'should work for different Date instances which have same value': function() {
		var date1 = new Date(1979, 03, 27);
		var date2 = new Date(1979, 03, 27);
		value_of(date1).should_be(date2);
	}
})
</jsspec>
Behaviour Driven Development for your ~TiddlyWiki
JSSpecPlugin
[[JSSpecStyles]]
/***
Description: Contains the stuff you need to use Tiddlyspot
Note, you also need UploadPlugin, PasswordOptionPlugin and LoadRemoteFileThroughProxy
from http://tiddlywiki.bidix.info for a complete working Tiddlyspot site.
***/
//{{{

// edit this if you are migrating sites or retrofitting an existing TW
config.tiddlyspotSiteId = 'jsspecplugin';

// make it so you can by default see edit controls via http
config.options.chkHttpReadOnly = false;
window.readOnly = false; // make sure of it (for tw 2.2)
window.showBackstage = true; // show backstage too

// disable autosave in d3
if (window.location.protocol != "file:")
	config.options.chkGTDLazyAutoSave = false;

// tweak shadow tiddlers to add upload button, password entry box etc
with (config.shadowTiddlers) {
	SiteUrl = 'http://'+config.tiddlyspotSiteId+'.tiddlyspot.com';
	SideBarOptions = SideBarOptions.replace(/(<<saveChanges>>)/,"$1<<tiddler TspotSidebar>>");
	OptionsPanel = OptionsPanel.replace(/^/,"<<tiddler TspotOptions>>");
	DefaultTiddlers = DefaultTiddlers.replace(/^/,"[[WelcomeToTiddlyspot]] ");
	MainMenu = MainMenu.replace(/^/,"[[WelcomeToTiddlyspot]] ");
}

// create some shadow tiddler content
merge(config.shadowTiddlers,{

'WelcomeToTiddlyspot':[
 "This document is a ~TiddlyWiki from tiddlyspot.com.  A ~TiddlyWiki is an electronic notebook that is great for managing todo lists, personal information, and all sorts of things.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //What now?// &nbsp;&nbsp;@@ Before you can save any changes, you need to enter your password in the form below.  Then configure privacy and other site settings at your [[control panel|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/controlpanel]] (your control panel username is //" + config.tiddlyspotSiteId + "//).",
 "<<tiddler TspotControls>>",
 "See also GettingStarted.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Working online// &nbsp;&nbsp;@@ You can edit this ~TiddlyWiki right now, and save your changes using the \"save to web\" button in the column on the right.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Working offline// &nbsp;&nbsp;@@ A fully functioning copy of this ~TiddlyWiki can be saved onto your hard drive or USB stick.  You can make changes and save them locally without being connected to the Internet.  When you're ready to sync up again, just click \"upload\" and your ~TiddlyWiki will be saved back to tiddlyspot.com.",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Help!// &nbsp;&nbsp;@@ Find out more about ~TiddlyWiki at [[TiddlyWiki.com|http://tiddlywiki.com]].  Also visit [[TiddlyWiki.org|http://tiddlywiki.org]] for documentation on learning and using ~TiddlyWiki. New users are especially welcome on the [[TiddlyWiki mailing list|http://groups.google.com/group/TiddlyWiki]], which is an excellent place to ask questions and get help.  If you have a tiddlyspot related problem email [[tiddlyspot support|mailto:support@tiddlyspot.com]].",
 "",
 "@@font-weight:bold;font-size:1.3em;color:#444; //Enjoy :)// &nbsp;&nbsp;@@ We hope you like using your tiddlyspot.com site.  Please email [[feedback@tiddlyspot.com|mailto:feedback@tiddlyspot.com]] with any comments or suggestions."
].join("\n"),

'TspotControls':[
 "| tiddlyspot password:|<<option pasUploadPassword>>|",
 "| site management:|<<upload http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/store.cgi index.html . .  " + config.tiddlyspotSiteId + ">>//(requires tiddlyspot password)//<br>[[control panel|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/controlpanel]], [[download (go offline)|http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/download]]|",
 "| links:|[[tiddlyspot.com|http://tiddlyspot.com/]], [[FAQs|http://faq.tiddlyspot.com/]], [[blog|http://tiddlyspot.blogspot.com/]], email [[support|mailto:support@tiddlyspot.com]] & [[feedback|mailto:feedback@tiddlyspot.com]], [[donate|http://tiddlyspot.com/?page=donate]]|"
].join("\n"),

'TspotSidebar':[
 "<<upload http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/store.cgi index.html . .  " + config.tiddlyspotSiteId + ">><html><a href='http://" + config.tiddlyspotSiteId + ".tiddlyspot.com/download' class='button'>download</a></html>"
].join("\n"),

'TspotOptions':[
 "tiddlyspot password:",
 "<<option pasUploadPassword>>",
 ""
].join("\n")

});
//}}}
| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |
| 13/09/2008 21:48:31 | Xavier | [[jsspecplugin.html|file:///D:/docs/Xavier/TiddlyWikis/jsspecplugin.html]] | [[store.cgi|http://jsspecplugin.tiddlyspot.com/store.cgi]] | . | [[index.html | http://jsspecplugin.tiddlyspot.com/index.html]] | . |
/***
|''Name:''|UploadPlugin|
|''Description:''|Save to web a TiddlyWiki|
|''Version:''|4.1.3|
|''Date:''|Feb 24, 2008|
|''Source:''|http://tiddlywiki.bidix.info/#UploadPlugin|
|''Documentation:''|http://tiddlywiki.bidix.info/#UploadPluginDoc|
|''Author:''|BidiX (BidiX (at) bidix (dot) info)|
|''License:''|[[BSD open source license|http://tiddlywiki.bidix.info/#%5B%5BBSD%20open%20source%20license%5D%5D ]]|
|''~CoreVersion:''|2.2.0|
|''Requires:''|PasswordOptionPlugin|
***/
//{{{
version.extensions.UploadPlugin = {
	major: 4, minor: 1, revision: 3,
	date: new Date("Feb 24, 2008"),
	source: 'http://tiddlywiki.bidix.info/#UploadPlugin',
	author: 'BidiX (BidiX (at) bidix (dot) info',
	coreVersion: '2.2.0'
};

//
// Environment
//

if (!window.bidix) window.bidix = {}; // bidix namespace
bidix.debugMode = false;	// true to activate both in Plugin and UploadService
	
//
// Upload Macro
//

config.macros.upload = {
// default values
	defaultBackupDir: '',	//no backup
	defaultStoreScript: "store.php",
	defaultToFilename: "index.html",
	defaultUploadDir: ".",
	authenticateUser: true	// UploadService Authenticate User
};
	
config.macros.upload.label = {
	promptOption: "Save and Upload this TiddlyWiki with UploadOptions",
	promptParamMacro: "Save and Upload this TiddlyWiki in %0",
	saveLabel: "save to web", 
	saveToDisk: "save to disk",
	uploadLabel: "upload"	
};

config.macros.upload.messages = {
	noStoreUrl: "No store URL in parmeters or options",
	usernameOrPasswordMissing: "Username or password missing"
};

config.macros.upload.handler = function(place,macroName,params) {
	if (readOnly)
		return;
	var label;
	if (document.location.toString().substr(0,4) == "http") 
		label = this.label.saveLabel;
	else
		label = this.label.uploadLabel;
	var prompt;
	if (params[0]) {
		prompt = this.label.promptParamMacro.toString().format([this.destFile(params[0], 
			(params[1] ? params[1]:bidix.basename(window.location.toString())), params[3])]);
	} else {
		prompt = this.label.promptOption;
	}
	createTiddlyButton(place, label, prompt, function() {config.macros.upload.action(params);}, null, null, this.accessKey);
};

config.macros.upload.action = function(params)
{
		// for missing macro parameter set value from options
		if (!params) params = {};
		var storeUrl = params[0] ? params[0] : config.options.txtUploadStoreUrl;
		var toFilename = params[1] ? params[1] : config.options.txtUploadFilename;
		var backupDir = params[2] ? params[2] : config.options.txtUploadBackupDir;
		var uploadDir = params[3] ? params[3] : config.options.txtUploadDir;
		var username = params[4] ? params[4] : config.options.txtUploadUserName;
		var password = config.options.pasUploadPassword; // for security reason no password as macro parameter	
		// for still missing parameter set default value
		if ((!storeUrl) && (document.location.toString().substr(0,4) == "http")) 
			storeUrl = bidix.dirname(document.location.toString())+'/'+config.macros.upload.defaultStoreScript;
		if (storeUrl.substr(0,4) != "http")
			storeUrl = bidix.dirname(document.location.toString()) +'/'+ storeUrl;
		if (!toFilename)
			toFilename = bidix.basename(window.location.toString());
		if (!toFilename)
			toFilename = config.macros.upload.defaultToFilename;
		if (!uploadDir)
			uploadDir = config.macros.upload.defaultUploadDir;
		if (!backupDir)
			backupDir = config.macros.upload.defaultBackupDir;
		// report error if still missing
		if (!storeUrl) {
			alert(config.macros.upload.messages.noStoreUrl);
			clearMessage();
			return false;
		}
		if (config.macros.upload.authenticateUser && (!username || !password)) {
			alert(config.macros.upload.messages.usernameOrPasswordMissing);
			clearMessage();
			return false;
		}
		bidix.upload.uploadChanges(false,null,storeUrl, toFilename, uploadDir, backupDir, username, password); 
		return false; 
};

config.macros.upload.destFile = function(storeUrl, toFilename, uploadDir) 
{
	if (!storeUrl)
		return null;
		var dest = bidix.dirname(storeUrl);
		if (uploadDir && uploadDir != '.')
			dest = dest + '/' + uploadDir;
		dest = dest + '/' + toFilename;
	return dest;
};

//
// uploadOptions Macro
//

config.macros.uploadOptions = {
	handler: function(place,macroName,params) {
		var wizard = new Wizard();
		wizard.createWizard(place,this.wizardTitle);
		wizard.addStep(this.step1Title,this.step1Html);
		var markList = wizard.getElement("markList");
		var listWrapper = document.createElement("div");
		markList.parentNode.insertBefore(listWrapper,markList);
		wizard.setValue("listWrapper",listWrapper);
		this.refreshOptions(listWrapper,false);
		var uploadCaption;
		if (document.location.toString().substr(0,4) == "http") 
			uploadCaption = config.macros.upload.label.saveLabel;
		else
			uploadCaption = config.macros.upload.label.uploadLabel;
		
		wizard.setButtons([
				{caption: uploadCaption, tooltip: config.macros.upload.label.promptOption, 
					onClick: config.macros.upload.action},
				{caption: this.cancelButton, tooltip: this.cancelButtonPrompt, onClick: this.onCancel}
				
			]);
	},
	options: [
		"txtUploadUserName",
		"pasUploadPassword",
		"txtUploadStoreUrl",
		"txtUploadDir",
		"txtUploadFilename",
		"txtUploadBackupDir",
		"chkUploadLog",
		"txtUploadLogMaxLine"		
	],
	refreshOptions: function(listWrapper) {
		var opts = [];
		for(i=0; i<this.options.length; i++) {
			var opt = {};
			opts.push();
			opt.option = "";
			n = this.options[i];
			opt.name = n;
			opt.lowlight = !config.optionsDesc[n];
			opt.description = opt.lowlight ? this.unknownDescription : config.optionsDesc[n];
			opts.push(opt);
		}
		var listview = ListView.create(listWrapper,opts,this.listViewTemplate);
		for(n=0; n<opts.length; n++) {
			var type = opts[n].name.substr(0,3);
			var h = config.macros.option.types[type];
			if (h && h.create) {
				h.create(opts[n].colElements['option'],type,opts[n].name,opts[n].name,"no");
			}
		}
		
	},
	onCancel: function(e)
	{
		backstage.switchTab(null);
		return false;
	},
	
	wizardTitle: "Upload with options",
	step1Title: "These options are saved in cookies in your browser",
	step1Html: "<input type='hidden' name='markList'></input><br>",
	cancelButton: "Cancel",
	cancelButtonPrompt: "Cancel prompt",
	listViewTemplate: {
		columns: [
			{name: 'Description', field: 'description', title: "Description", type: 'WikiText'},
			{name: 'Option', field: 'option', title: "Option", type: 'String'},
			{name: 'Name', field: 'name', title: "Name", type: 'String'}
			],
		rowClasses: [
			{className: 'lowlight', field: 'lowlight'} 
			]}
};

//
// upload functions
//

if (!bidix.upload) bidix.upload = {};

if (!bidix.upload.messages) bidix.upload.messages = {
	//from saving
	invalidFileError: "The original file '%0' does not appear to be a valid TiddlyWiki",
	backupSaved: "Backup saved",
	backupFailed: "Failed to upload backup file",
	rssSaved: "RSS feed uploaded",
	rssFailed: "Failed to upload RSS feed file",
	emptySaved: "Empty template uploaded",
	emptyFailed: "Failed to upload empty template file",
	mainSaved: "Main TiddlyWiki file uploaded",
	mainFailed: "Failed to upload main TiddlyWiki file. Your changes have not been saved",
	//specific upload
	loadOriginalHttpPostError: "Can't get original file",
	aboutToSaveOnHttpPost: 'About to upload on %0 ...',
	storePhpNotFound: "The store script '%0' was not found."
};

bidix.upload.uploadChanges = function(onlyIfDirty,tiddlers,storeUrl,toFilename,uploadDir,backupDir,username,password)
{
	var callback = function(status,uploadParams,original,url,xhr) {
		if (!status) {
			displayMessage(bidix.upload.messages.loadOriginalHttpPostError);
			return;
		}
		if (bidix.debugMode) 
			alert(original.substr(0,500)+"\n...");
		// Locate the storeArea div's 
		var posDiv = locateStoreArea(original);
		if((posDiv[0] == -1) || (posDiv[1] == -1)) {
			alert(config.messages.invalidFileError.format([localPath]));
			return;
		}
		bidix.upload.uploadRss(uploadParams,original,posDiv);
	};
	
	if(onlyIfDirty && !store.isDirty())
		return;
	clearMessage();
	// save on localdisk ?
	if (document.location.toString().substr(0,4) == "file") {
		var path = document.location.toString();
		var localPath = getLocalPath(path);
		saveChanges();
	}
	// get original
	var uploadParams = new Array(storeUrl,toFilename,uploadDir,backupDir,username,password);
	var originalPath = document.location.toString();
	// If url is a directory : add index.html
	if (originalPath.charAt(originalPath.length-1) == "/")
		originalPath = originalPath + "index.html";
	var dest = config.macros.upload.destFile(storeUrl,toFilename,uploadDir);
	var log = new bidix.UploadLog();
	log.startUpload(storeUrl, dest, uploadDir,  backupDir);
	displayMessage(bidix.upload.messages.aboutToSaveOnHttpPost.format([dest]));
	if (bidix.debugMode) 
		alert("about to execute Http - GET on "+originalPath);
	var r = doHttp("GET",originalPath,null,null,username,password,callback,uploadParams,null);
	if (typeof r == "string")
		displayMessage(r);
	return r;
};

bidix.upload.uploadRss = function(uploadParams,original,posDiv) 
{
	var callback = function(status,params,responseText,url,xhr) {
		if(status) {
			var destfile = responseText.substring(responseText.indexOf("destfile:")+9,responseText.indexOf("\n", responseText.indexOf("destfile:")));
			displayMessage(bidix.upload.messages.rssSaved,bidix.dirname(url)+'/'+destfile);
			bidix.upload.uploadMain(params[0],params[1],params[2]);
		} else {
			displayMessage(bidix.upload.messages.rssFailed);			
		}
	};
	// do uploadRss
	if(config.options.chkGenerateAnRssFeed) {
		var rssPath = uploadParams[1].substr(0,uploadParams[1].lastIndexOf(".")) + ".xml";
		var rssUploadParams = new Array(uploadParams[0],rssPath,uploadParams[2],'',uploadParams[4],uploadParams[5]);
		var rssString = generateRss();
		// no UnicodeToUTF8 conversion needed when location is "file" !!!
		if (document.location.toString().substr(0,4) != "file")
			rssString = convertUnicodeToUTF8(rssString);	
		bidix.upload.httpUpload(rssUploadParams,rssString,callback,Array(uploadParams,original,posDiv));
	} else {
		bidix.upload.uploadMain(uploadParams,original,posDiv);
	}
};

bidix.upload.uploadMain = function(uploadParams,original,posDiv) 
{
	var callback = function(status,params,responseText,url,xhr) {
		var log = new bidix.UploadLog();
		if(status) {
			// if backupDir specified
			if ((params[3]) && (responseText.indexOf("backupfile:") > -1))  {
				var backupfile = responseText.substring(responseText.indexOf("backupfile:")+11,responseText.indexOf("\n", responseText.indexOf("backupfile:")));
				displayMessage(bidix.upload.messages.backupSaved,bidix.dirname(url)+'/'+backupfile);
			}
			var destfile = responseText.substring(responseText.indexOf("destfile:")+9,responseText.indexOf("\n", responseText.indexOf("destfile:")));
			displayMessage(bidix.upload.messages.mainSaved,bidix.dirname(url)+'/'+destfile);
			store.setDirty(false);
			log.endUpload("ok");
		} else {
			alert(bidix.upload.messages.mainFailed);
			displayMessage(bidix.upload.messages.mainFailed);
			log.endUpload("failed");			
		}
	};
	// do uploadMain
	var revised = bidix.upload.updateOriginal(original,posDiv);
	bidix.upload.httpUpload(uploadParams,revised,callback,uploadParams);
};

bidix.upload.httpUpload = function(uploadParams,data,callback,params)
{
	var localCallback = function(status,params,responseText,url,xhr) {
		url = (url.indexOf("nocache=") < 0 ? url : url.substring(0,url.indexOf("nocache=")-1));
		if (xhr.status == 404)
			alert(bidix.upload.messages.storePhpNotFound.format([url]));
		if ((bidix.debugMode) || (responseText.indexOf("Debug mode") >= 0 )) {
			alert(responseText);
			if (responseText.indexOf("Debug mode") >= 0 )
				responseText = responseText.substring(responseText.indexOf("\n\n")+2);
		} else if (responseText.charAt(0) != '0') 
			alert(responseText);
		if (responseText.charAt(0) != '0')
			status = null;
		callback(status,params,responseText,url,xhr);
	};
	// do httpUpload
	var boundary = "---------------------------"+"AaB03x";	
	var uploadFormName = "UploadPlugin";
	// compose headers data
	var sheader = "";
	sheader += "--" + boundary + "\r\nContent-disposition: form-data; name=\"";
	sheader += uploadFormName +"\"\r\n\r\n";
	sheader += "backupDir="+uploadParams[3] +
				";user=" + uploadParams[4] +
				";password=" + uploadParams[5] +
				";uploaddir=" + uploadParams[2];
	if (bidix.debugMode)
		sheader += ";debug=1";
	sheader += ";;\r\n"; 
	sheader += "\r\n" + "--" + boundary + "\r\n";
	sheader += "Content-disposition: form-data; name=\"userfile\"; filename=\""+uploadParams[1]+"\"\r\n";
	sheader += "Content-Type: text/html;charset=UTF-8" + "\r\n";
	sheader += "Content-Length: " + data.length + "\r\n\r\n";
	// compose trailer data
	var strailer = new String();
	strailer = "\r\n--" + boundary + "--\r\n";
	data = sheader + data + strailer;
	if (bidix.debugMode) alert("about to execute Http - POST on "+uploadParams[0]+"\n with \n"+data.substr(0,500)+ " ... ");
	var r = doHttp("POST",uploadParams[0],data,"multipart/form-data; ;charset=UTF-8; boundary="+boundary,uploadParams[4],uploadParams[5],localCallback,params,null);
	if (typeof r == "string")
		displayMessage(r);
	return r;
};

// same as Saving's updateOriginal but without convertUnicodeToUTF8 calls
bidix.upload.updateOriginal = function(original, posDiv)
{
	if (!posDiv)
		posDiv = locateStoreArea(original);
	if((posDiv[0] == -1) || (posDiv[1] == -1)) {
		alert(config.messages.invalidFileError.format([localPath]));
		return;
	}
	var revised = original.substr(0,posDiv[0] + startSaveArea.length) + "\n" +
				store.allTiddlersAsHtml() + "\n" +
				original.substr(posDiv[1]);
	var newSiteTitle = getPageTitle().htmlEncode();
	revised = revised.replaceChunk("<title"+">","</title"+">"," " + newSiteTitle + " ");
	revised = updateMarkupBlock(revised,"PRE-HEAD","MarkupPreHead");
	revised = updateMarkupBlock(revised,"POST-HEAD","MarkupPostHead");
	revised = updateMarkupBlock(revised,"PRE-BODY","MarkupPreBody");
	revised = updateMarkupBlock(revised,"POST-SCRIPT","MarkupPostBody");
	return revised;
};

//
// UploadLog
// 
// config.options.chkUploadLog :
//		false : no logging
//		true : logging
// config.options.txtUploadLogMaxLine :
//		-1 : no limit
//      0 :  no Log lines but UploadLog is still in place
//		n :  the last n lines are only kept
//		NaN : no limit (-1)

bidix.UploadLog = function() {
	if (!config.options.chkUploadLog) 
		return; // this.tiddler = null
	this.tiddler = store.getTiddler("UploadLog");
	if (!this.tiddler) {
		this.tiddler = new Tiddler();
		this.tiddler.title = "UploadLog";
		this.tiddler.text = "| !date | !user | !location | !storeUrl | !uploadDir | !toFilename | !backupdir | !origin |";
		this.tiddler.created = new Date();
		this.tiddler.modifier = config.options.txtUserName;
		this.tiddler.modified = new Date();
		store.addTiddler(this.tiddler);
	}
	return this;
};

bidix.UploadLog.prototype.addText = function(text) {
	if (!this.tiddler)
		return;
	// retrieve maxLine when we need it
	var maxLine = parseInt(config.options.txtUploadLogMaxLine,10);
	if (isNaN(maxLine))
		maxLine = -1;
	// add text
	if (maxLine != 0) 
		this.tiddler.text = this.tiddler.text + text;
	// Trunck to maxLine
	if (maxLine >= 0) {
		var textArray = this.tiddler.text.split('\n');
		if (textArray.length > maxLine + 1)
			textArray.splice(1,textArray.length-1-maxLine);
			this.tiddler.text = textArray.join('\n');		
	}
	// update tiddler fields
	this.tiddler.modifier = config.options.txtUserName;
	this.tiddler.modified = new Date();
	store.addTiddler(this.tiddler);
	// refresh and notifiy for immediate update
	story.refreshTiddler(this.tiddler.title);
	store.notify(this.tiddler.title, true);
};

bidix.UploadLog.prototype.startUpload = function(storeUrl, toFilename, uploadDir,  backupDir) {
	if (!this.tiddler)
		return;
	var now = new Date();
	var text = "\n| ";
	var filename = bidix.basename(document.location.toString());
	if (!filename) filename = '/';
	text += now.formatString("0DD/0MM/YYYY 0hh:0mm:0ss") +" | ";
	text += config.options.txtUserName + " | ";
	text += "[["+filename+"|"+location + "]] |";
	text += " [[" + bidix.basename(storeUrl) + "|" + storeUrl + "]] | ";
	text += uploadDir + " | ";
	text += "[[" + bidix.basename(toFilename) + " | " +toFilename + "]] | ";
	text += backupDir + " |";
	this.addText(text);
};

bidix.UploadLog.prototype.endUpload = function(status) {
	if (!this.tiddler)
		return;
	this.addText(" "+status+" |");
};

//
// Utilities
// 

bidix.checkPlugin = function(plugin, major, minor, revision) {
	var ext = version.extensions[plugin];
	if (!
		(ext  && 
			((ext.major > major) || 
			((ext.major == major) && (ext.minor > minor))  ||
			((ext.major == major) && (ext.minor == minor) && (ext.revision >= revision))))) {
			// write error in PluginManager
			if (pluginInfo)
				pluginInfo.log.push("Requires " + plugin + " " + major + "." + minor + "." + revision);
			eval(plugin); // generate an error : "Error: ReferenceError: xxxx is not defined"
	}
};

bidix.dirname = function(filePath) {
	if (!filePath) 
		return;
	var lastpos;
	if ((lastpos = filePath.lastIndexOf("/")) != -1) {
		return filePath.substring(0, lastpos);
	} else {
		return filePath.substring(0, filePath.lastIndexOf("\\"));
	}
};

bidix.basename = function(filePath) {
	if (!filePath) 
		return;
	var lastpos;
	if ((lastpos = filePath.lastIndexOf("#")) != -1) 
		filePath = filePath.substring(0, lastpos);
	if ((lastpos = filePath.lastIndexOf("/")) != -1) {
		return filePath.substring(lastpos + 1);
	} else
		return filePath.substring(filePath.lastIndexOf("\\")+1);
};

bidix.initOption = function(name,value) {
	if (!config.options[name])
		config.options[name] = value;
};

//
// Initializations
//

// require PasswordOptionPlugin 1.0.1 or better
bidix.checkPlugin("PasswordOptionPlugin", 1, 0, 1);

// styleSheet
setStylesheet('.txtUploadStoreUrl, .txtUploadBackupDir, .txtUploadDir {width: 22em;}',"uploadPluginStyles");

//optionsDesc
merge(config.optionsDesc,{
	txtUploadStoreUrl: "Url of the UploadService script (default: store.php)",
	txtUploadFilename: "Filename of the uploaded file (default: in index.html)",
	txtUploadDir: "Relative Directory where to store the file (default: . (downloadService directory))",
	txtUploadBackupDir: "Relative Directory where to backup the file. If empty no backup. (default: ''(empty))",
	txtUploadUserName: "Upload Username",
	pasUploadPassword: "Upload Password",
	chkUploadLog: "do Logging in UploadLog (default: true)",
	txtUploadLogMaxLine: "Maximum of lines in UploadLog (default: 10)"
});

// Options Initializations
bidix.initOption('txtUploadStoreUrl','');
bidix.initOption('txtUploadFilename','');
bidix.initOption('txtUploadDir','');
bidix.initOption('txtUploadBackupDir','');
bidix.initOption('txtUploadUserName','');
bidix.initOption('pasUploadPassword','');
bidix.initOption('chkUploadLog',true);
bidix.initOption('txtUploadLogMaxLine','10');


// Backstage
merge(config.tasks,{
	uploadOptions: {text: "upload", tooltip: "Change UploadOptions and Upload", content: '<<uploadOptions>>'}
});
config.backstageTasks.push("uploadOptions");


//}}}

/***
|''Name:''|LogMessagePlugin|
|''Description:''|Browser independent message logging|
|''Author:''|PaulDowney (psd (at) osmosoft (dot) com)|
|''CodeRepository:''|http://svn.tiddlywiki.org/Trunk/contributors/PaulDowney/plugins/LogMessagePlugin/ |
|''Version:''|0.2|
|''License:''|[[BSD open source license]]|
|''Comments:''|Please make comments at http://groups.google.co.uk/group/TiddlyWikiDev |
|''~CoreVersion:''|2.4.1|
!!Documentation
This plugin provides a log() function with the same semantics as the [[Firebug|http://getfirebug.com/]] [[console.log|http://getfirebug.com/console.html]] function. Calling console.log() is likely to fail in browsers without Firebug installed. 

Depending upon the Options, the message will be sent to the Firebug console, as well as to an external popup window, and TiddlyWiki display message. As of TiddlyWiki release 2.4.1, the core provides a [[default log() function|http://trac.tiddlywiki.org/ticket/674]], so code containing log() calls will continue to function without generating errors, even without this plugin installed.

TiddlyWiki initializes plugin tiddlers in collation sequence order, so in order to make the log() function available for use in other plugins during initialization, the tiddler has been named _LogMessagePlugin.

An example TiddlyWiki containing the latest released version with a simple example is available from from http://whatfettle.com/2008/07/LogMessagePlugin/

!!Options
|<<option chkLogMessageEnabled>>|<<message config.optionsDesc.chkLogMessageEnabled>>|
|<<option chkLogMessageConsole>>|<<message config.optionsDesc.chkLogMessageConsole>>|
|<<option chkLogMessageWindow>>|<<message config.optionsDesc.chkLogMessageWindow>>|
|<<option chkLogMessageDisplayMessage>>|<<message config.optionsDesc.chkLogMessageDisplayMessage>>|

!!Code:
***/

//{{{
if(!version.extensions.LogMessagePlugin){
version.extensions.LogMessagePlugin = {installed:true};

	config.optionsDesc.chkLogMessageEnabled = "logging of messages enabled";
	config.options.chkLogMessageEnabled = false;

	config.optionsDesc.chkLogMessageConsole = "log messages to the console, when available";
	config.options.chkLogMessageConsole = false;

	config.optionsDesc.chkLogMessageWindow = "log messages to an external window";
	config.options.chkLogMessageWindow = false;

	config.optionsDesc.chkLogMessageDisplayMessage = "log messages using displayMessage";
	config.options.chkLogMessageDisplayMessage = false;
	
	config.macros.LogMessage = { 

		log: function(){
			if(!config.options.chkLogMessageEnabled){
				return;
			}
			if(config.options.chkLogMessageConsole){
				if (window.console){
					console.log(arguments);
				}
			}
			if(!(config.options.chkLogMessageWindow||config.options.chkLogMessageDisplayMessage)){
				return;
			}
			var message = (function(a){var x=[]; for(var i=0; i<a.length; i++)
				x.push(a[i]); return x.join(', ');})(arguments);
			if(config.options.chkLogMessageWindow){
				var me = config.macros.LogMessage;
				me.logWindow(message);
			}
			if(config.options.chkLogMessageDisplayMessage){
				displayMessage(message);
			}
		},

		logWindow: function(message){
			var me = config.macros.LogMessage;
			try{
				if(!me.window_ || me.window_.closed){
					var win = window.open("", null, "width=400,height=200,scrollbars=yes,"+
						"resizable=yes,status=no,location=no,menubar=no,toolbar=no");
					if(!win) 
						return;
					var doc = win.document;
					doc.write("<html><head><title>Log</title></head><body></body></html>");
					doc.close();
					me.window_ = win;
				}
				var line = me.window_.document.createElement("div");
				line.appendChild(me.window_.document.createTextNode(message));
				me.window_.document.body.appendChild(line);
			}catch(ex){
				me.window_ = undefined;
				if (!me.logToWindowErrorReported){
					alert("Error logging to window: probably another document has been loaded");
					me.logToWindowErrorReported = true;
				}
			}
		},

		handler: function(place,macroName,params,wikifier,paramString,tiddler){
			var me = config.macros.LogMessage;
			me.log(params);
		}
	};

	log = config.macros.LogMessage.log;

} //# end of 'install only once'
//}}}
/***
|<<option chkLogMessageEnabled>>|<<message config.optionsDesc.chkLogMessageEnabled>>|
|<<option chkLogMessageConsole>>|<<message config.optionsDesc.chkLogMessageConsole>>|
|<<option chkLogMessageWindow>>|<<message config.optionsDesc.chkLogMessageWindow>>|
|<<option chkLogMessageDisplayMessage>>|<<message config.optionsDesc.chkLogMessageDisplayMessage>>|
***/
//{{{
config.options.chkLogMessageEnabled = true;
config.options.chkLogMessageConsole = false;
config.options.chkLogMessageWindow = true;
config.options.chkLogMessageDisplayMessage = true;
//}}}
/***
Required by [[JSSpecScript]]
From
http://trac.tiddlywiki.org/browser/Trunk/contributors/FND/plugins/DiffPlugin/google-diff-match-patch.js?rev=6299
***/
//{{{
/*
 * adapted version of Neil Fraser's Neil Fraser's google-diff-match-patch library
 * (http://code.google.com/p/google-diff-match-patch/)
 *
 * License:
 *  GNU Lesser General Public License
 *  http://www.gnu.org/licenses/lgpl.html
 */


/**
 * Class containing the diff, match and patch methods.
 * @constructor
 */
function diff_match_patch() {

  // Defaults.
  // Redefine these in your program to override the defaults.

  // Number of seconds to map a diff before giving up.  (0 for infinity)
  this.Diff_Timeout = 1.0;
  // Cost of an empty edit operation in terms of edit characters.
  this.Diff_EditCost = 4;
  // The size beyond which the double-ended diff activates.
  // Double-ending is twice as fast, but less accurate.
  this.Diff_DualThreshold = 32;
  // Tweak the relative importance (0.0 = accuracy, 1.0 = proximity)
  this.Match_Balance = 0.5;
  // At what point is no match declared (0.0 = perfection, 1.0 = very loose)
  this.Match_Threshold = 0.5;
  // The min and max cutoffs used when computing text lengths.
  this.Match_MinLength = 100;
  this.Match_MaxLength = 1000;
  // Chunk size for context length.
  this.Patch_Margin = 4;

  /**
   * Compute the number of bits in an int.
   * The normal answer for JavaScript is 32.
   * @return {number} Max bits
   */
  function getMaxBits() {
    var maxbits = 0;
    var oldi = 1;
    var newi = 2;
    while (oldi != newi) {
      maxbits++;
      oldi = newi;
      newi = newi << 1;
    }
    return maxbits;
  }
  // How many bits in a number?
  this.Match_MaxBits = getMaxBits();
}


//  DIFF FUNCTIONS


/**
 * The data structure representing a diff is an array of tuples:
 * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']]
 * which means: delete 'Hello', add 'Goodbye' and keep ' world.'
 */
var DIFF_DELETE = -1;
var DIFF_INSERT = 1;
var DIFF_EQUAL = 0;


/**
 * Find the differences between two texts.  Simplifies the problem by stripping
 * any common prefix or suffix off the texts before diffing.
 * @param {string} text1 Old string to be diffed
 * @param {string} text2 New string to be diffed
 * @param {boolean} opt_checklines Optional speedup flag.  If present and false,
 *     then don't run a line-level diff first to identify the changed areas.
 *     Defaults to true, which does a faster, slightly less optimal diff
 * @return {Array.<Array.<*>>} Array of diff tuples
 */
diff_match_patch.prototype.diff_main = function(text1, text2, opt_checklines) {
  // Check for equality (speedup)
  if (text1 == text2) {
    return [[DIFF_EQUAL, text1]];
  }

  if (typeof opt_checklines == 'undefined') {
    opt_checklines = true;
  }
  var checklines = opt_checklines;

  // Trim off common prefix (speedup)
  var commonlength = this.diff_commonPrefix(text1, text2);
  var commonprefix = text1.substring(0, commonlength);
  text1 = text1.substring(commonlength);
  text2 = text2.substring(commonlength);

  // Trim off common suffix (speedup)
  commonlength = this.diff_commonSuffix(text1, text2);
  var commonsuffix = text1.substring(text1.length - commonlength);
  text1 = text1.substring(0, text1.length - commonlength);
  text2 = text2.substring(0, text2.length - commonlength);

  // Compute the diff on the middle block
  var diffs = this.diff_compute(text1, text2, checklines);

  // Restore the prefix and suffix
  if (commonprefix) {
    diffs.unshift([DIFF_EQUAL, commonprefix]);
  }
  if (commonsuffix) {
    diffs.push([DIFF_EQUAL, commonsuffix]);
  }
  this.diff_cleanupMerge(diffs);
  return diffs;
};


/**
 * Find the differences between two texts.
 * @param {string} text1 Old string to be diffed
 * @param {string} text2 New string to be diffed
 * @param {boolean} checklines Speedup flag.  If false, then don't run a
 *     line-level diff first to identify the changed areas.
 *     If true, then run a faster, slightly less optimal diff
 * @return {Array.<Array.<*>>} Array of diff tuples
 */
diff_match_patch.prototype.diff_compute = function(text1, text2, checklines) {
  var diffs;

  if (!text1) {
    // Just add some text (speedup)
    return [[DIFF_INSERT, text2]];
  }

  if (!text2) {
    // Just delete some text (speedup)
    return [[DIFF_DELETE, text1]];
  }

  var longtext = text1.length > text2.length ? text1 : text2;
  var shorttext = text1.length > text2.length ? text2 : text1;
  var i = longtext.indexOf(shorttext);
  if (i != -1) {
    // Shorter text is inside the longer text (speedup)
    diffs = [[DIFF_INSERT, longtext.substring(0, i)],
             [DIFF_EQUAL, shorttext],
             [DIFF_INSERT, longtext.substring(i + shorttext.length)]];
    // Swap insertions for deletions if diff is reversed.
    if (text1.length > text2.length) {
      diffs[0][0] = diffs[2][0] = DIFF_DELETE;
    }
    return diffs;
  }
  longtext = shorttext = null;  // Garbage collect

  // Check to see if the problem can be split in two.
  var hm = this.diff_halfMatch(text1, text2);
  if (hm) {
    // A half-match was found, sort out the return data.
    var text1_a = hm[0];
    var text1_b = hm[1];
    var text2_a = hm[2];
    var text2_b = hm[3];
    var mid_common = hm[4];
    // Send both pairs off for separate processing.
    var diffs_a = this.diff_main(text1_a, text2_a, checklines);
    var diffs_b = this.diff_main(text1_b, text2_b, checklines);
    // Merge the results.
    return diffs_a.concat([[DIFF_EQUAL, mid_common]], diffs_b);
  }

  // Perform a real diff.
  if (checklines && (text1.length < 100 || text2.length < 100)) {
    // Too trivial for the overhead.
    checklines = false;
  }
  var linearray;
  if (checklines) {
    // Scan the text on a line-by-line basis first.
    var a = this.diff_linesToChars(text1, text2);
    text1 = a[0];
    text2 = a[1];
    linearray = a[2];
  }
  diffs = this.diff_map(text1, text2);
  if (!diffs) {
    // No acceptable result.
    diffs = [[DIFF_DELETE, text1], [DIFF_INSERT, text2]];
  }
  if (checklines) {
    // Convert the diff back to original text.
    this.diff_charsToLines(diffs, linearray);
    // Eliminate freak matches (e.g. blank lines)
    this.diff_cleanupSemantic(diffs);

    // Rediff any replacement blocks, this time character-by-character.
    // Add a dummy entry at the end.
    diffs.push([DIFF_EQUAL, '']);
    var pointer = 0;
    var count_delete = 0;
    var count_insert = 0;
    var text_delete = '';
    var text_insert = '';
    while (pointer < diffs.length) {
      switch (diffs[pointer][0]) {
      case DIFF_INSERT:
        count_insert++;
        text_insert += diffs[pointer][1];
        break;
      case DIFF_DELETE:
        count_delete++;
        text_delete += diffs[pointer][1];
        break;
      case DIFF_EQUAL:
        // Upon reaching an equality, check for prior redundancies.
        if (count_delete >= 1 && count_insert >= 1) {
          // Delete the offending records and add the merged ones.
          var a = this.diff_main(text_delete, text_insert, false);
          diffs.splice(pointer - count_delete - count_insert,
                       count_delete + count_insert);
          pointer = pointer - count_delete - count_insert;
          for (var j = a.length - 1; j >= 0; j--) {
            diffs.splice(pointer, 0, a[j]);
          }
          pointer = pointer + a.length;
        }
        count_insert = 0;
        count_delete = 0;
        text_delete = '';
        text_insert = '';
        break;
      }
     pointer++;
    }
    diffs.pop();  // Remove the dummy entry at the end.
  }
  return diffs;
};


/**
 * Split two texts into an array of strings.  Reduce the texts to a string of
 * hashes where each Unicode character represents one line.
 * @param {string} text1 First string
 * @param {string} text2 Second string
 * @return {Array.<string|Array.<string>>} Three element Array, containing the
 *     encoded text1, the encoded text2 and the array of unique strings.  The
 *     zeroth element of the array of unique strings is intentionally blank.
 */
diff_match_patch.prototype.diff_linesToChars = function(text1, text2) {
  var lineArray = [];  // e.g. lineArray[4] == 'Hello\n'
  var lineHash = {};   // e.g. lineHash['Hello\n'] == 4

  // '\x00' is a valid character, but various debuggers don't like it.
  // So we'll insert a junk entry to avoid generating a null character.
  lineArray[0] = '';

  /**
   * Split a text into an array of strings.  Reduce the texts to a string of
   * hashes where each Unicode character represents one line.
   * Modifies linearray and linehash through being a closure.
   * @param {string} text String to encode
   * @return {string} Encoded string
   */
  function diff_linesToCharsMunge(text) {
    var chars = '';
    // Walk the text, pulling out a substring for each line.
    // text.split('\n') would would temporarily double our memory footprint.
    // Modifying text would create many large strings to garbage collect.
    var lineStart = 0;
    var lineEnd = -1;
    // Keeping our own length variable is faster than looking it up.
    var lineArrayLength = lineArray.length;
    while (lineEnd < text.length - 1) {
      lineEnd = text.indexOf('\n', lineStart);
      if (lineEnd == -1) {
        lineEnd = text.length - 1;
      }
      var line = text.substring(lineStart, lineEnd + 1);
      lineStart = lineEnd + 1;

      if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) :
          (lineHash[line] !== undefined)) {
        chars += String.fromCharCode(lineHash[line]);
      } else {
        chars += String.fromCharCode(lineArrayLength);
        lineHash[line] = lineArrayLength;
        lineArray[lineArrayLength++] = line;
      }
    }
    return chars;
  }

  var chars1 = diff_linesToCharsMunge(text1);
  var chars2 = diff_linesToCharsMunge(text2);
  return [chars1, chars2, lineArray];
};


/**
 * Rehydrate the text in a diff from a string of line hashes to real lines of
 * text.
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 * @param {Array.<string>} lineArray Array of unique strings
 */
diff_match_patch.prototype.diff_charsToLines = function(diffs, lineArray) {
  for (var x = 0; x < diffs.length; x++) {
    var chars = diffs[x][1];
    var text = [];
    for (var y = 0; y < chars.length; y++) {
      text[y] = lineArray[chars.charCodeAt(y)];
    }
    diffs[x][1] = text.join('');
  }
};


/**
 * Explore the intersection points between the two texts.
 * @param {string} text1 Old string to be diffed
 * @param {string} text2 New string to be diffed
 * @return {Array.<Array.<*>>?} Array of diff tuples or null if no diff
 *     available
 */
diff_match_patch.prototype.diff_map = function(text1, text2) {
  // Don't run for too long.
  var ms_end = (new Date()).getTime() + this.Diff_Timeout * 1000;
  var max_d = text1.length + text2.length - 1;
  var doubleEnd = this.Diff_DualThreshold * 2 < max_d;
  var v_map1 = [];
  var v_map2 = [];
  var v1 = {};
  var v2 = {};
  v1[1] = 0;
  v2[1] = 0;
  var x, y;
  var footstep;  // Used to track overlapping paths.
  var footsteps = {};
  var done = false;
  // Safari 1.x doesn't have hasOwnProperty
  var hasOwnProperty = !!(footsteps.hasOwnProperty);
  // If the total number of characters is odd, then the front path will collide
  // with the reverse path.
  var front = (text1.length + text2.length) % 2;
  for (var d = 0; d < max_d; d++) {
    // Bail out if timeout reached.
    if (this.Diff_Timeout > 0 && (new Date()).getTime() > ms_end) {
      return null;
    }

    // Walk the front path one step.
    v_map1[d] = {};
    for (var k = -d; k <= d; k += 2) {
      if (k == -d || k != d && v1[k - 1] < v1[k + 1]) {
        x = v1[k + 1];
      } else {
        x = v1[k - 1] + 1;
      }
      y = x - k;
      if (doubleEnd) {
        footstep = x + ',' + y;
        if (front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
                      (footsteps[footstep] !== undefined))) {
          done = true;
        }
        if (!front) {
          footsteps[footstep] = d;
        }
      }
      while (!done && x < text1.length && y < text2.length &&
             text1.charAt(x) == text2.charAt(y)) {
        x++;
        y++;
        if (doubleEnd) {
          footstep = x + ',' + y;
          if (front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
              (footsteps[footstep] !== undefined))) {
            done = true;
          }
          if (!front) {
            footsteps[footstep] = d;
          }
        }
      }
      v1[k] = x;
      v_map1[d][x + ',' + y] = true;
      if (x == text1.length && y == text2.length) {
        // Reached the end in single-path mode.
        return this.diff_path1(v_map1, text1, text2);
      } else if (done) {
        // Front path ran over reverse path.
        v_map2 = v_map2.slice(0, footsteps[footstep] + 1);
        var a = this.diff_path1(v_map1, text1.substring(0, x),
                                text2.substring(0, y));
        return a.concat(this.diff_path2(v_map2, text1.substring(x),
                                        text2.substring(y)));
      }
    }

    if (doubleEnd) {
      // Walk the reverse path one step.
      v_map2[d] = {};
      for (var k = -d; k <= d; k += 2) {
        if (k == -d || k != d && v2[k - 1] < v2[k + 1]) {
          x = v2[k + 1];
        } else {
          x = v2[k - 1] + 1;
        }
        y = x - k;
        footstep = (text1.length - x) + ',' + (text2.length - y);
        if (!front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
                       (footsteps[footstep] !== undefined))) {
          done = true;
        }
        if (front) {
          footsteps[footstep] = d;
        }
        while (!done && x < text1.length && y < text2.length &&
               text1.charAt(text1.length - x - 1) ==
               text2.charAt(text2.length - y - 1)) {
          x++;
          y++;
          footstep = (text1.length - x) + ',' + (text2.length - y);
          if (!front && (hasOwnProperty ? footsteps.hasOwnProperty(footstep) :
                         (footsteps[footstep] !== undefined))) {
            done = true;
          }
          if (front) {
            footsteps[footstep] = d;
          }
        }
        v2[k] = x;
        v_map2[d][x + ',' + y] = true;
        if (done) {
          // Reverse path ran over front path.
          v_map1 = v_map1.slice(0, footsteps[footstep] + 1);
          var a = this.diff_path1(v_map1, text1.substring(0, text1.length - x),
                                  text2.substring(0, text2.length - y));
          return a.concat(this.diff_path2(v_map2,
                          text1.substring(text1.length - x),
                          text2.substring(text2.length - y)));
        }
      }
    }
  }
  // Number of diffs equals number of characters, no commonality at all.
  return null;
};


/**
 * Work from the middle back to the start to determine the path.
 * @param {Array.<Object>} v_map Array of paths.
 * @param {string} text1 Old string fragment to be diffed
 * @param {string} text2 New string fragment to be diffed
 * @return {Array.<Array.<*>>} Array of diff tuples
 */
diff_match_patch.prototype.diff_path1 = function(v_map, text1, text2) {
  var path = [];
  var x = text1.length;
  var y = text2.length;
  var last_op = null;
  for (var d = v_map.length - 2; d >= 0; d--) {
    while (1) {
      if (v_map[d].hasOwnProperty ? v_map[d].hasOwnProperty((x - 1) + ',' + y) :
          (v_map[d][(x - 1) + ',' + y] !== undefined)) {
        x--;
        if (last_op === DIFF_DELETE) {
          path[0][1] = text1.charAt(x) + path[0][1];
        } else {
          path.unshift([DIFF_DELETE, text1.charAt(x)]);
        }
        last_op = DIFF_DELETE;
        break;
      } else if (v_map[d].hasOwnProperty ?
                 v_map[d].hasOwnProperty(x + ',' + (y - 1)) :
                 (v_map[d][x + ',' + (y - 1)] !== undefined)) {
        y--;
        if (last_op === DIFF_INSERT) {
          path[0][1] = text2.charAt(y) + path[0][1];
        } else {
          path.unshift([DIFF_INSERT, text2.charAt(y)]);
        }
        last_op = DIFF_INSERT;
        break;
      } else {
        x--;
        y--;
        //if (text1.charAt(x) != text2.charAt(y)) {
        //  throw new Error('No diagonal.  Can\'t happen. (diff_path1)');
        //}
        if (last_op === DIFF_EQUAL) {
          path[0][1] = text1.charAt(x) + path[0][1];
        } else {
          path.unshift([DIFF_EQUAL, text1.charAt(x)]);
        }
        last_op = DIFF_EQUAL;
      }
    }
  }
  return path;
};


/**
 * Work from the middle back to the end to determine the path.
 * @param {Array.<Object>} v_map Array of paths.
 * @param {string} text1 Old string fragment to be diffed
 * @param {string} text2 New string fragment to be diffed
 * @return {Array.<Array.<*>>} Array of diff tuples
 */
diff_match_patch.prototype.diff_path2 = function(v_map, text1, text2) {
  var path = [];
  var pathLength = 0;
  var x = text1.length;
  var y = text2.length;
  var last_op = null;
  for (var d = v_map.length - 2; d >= 0; d--) {
    while (1) {
      if (v_map[d].hasOwnProperty ? v_map[d].hasOwnProperty((x - 1) + ',' + y) :
          (v_map[d][(x - 1) + ',' + y] !== undefined)) {
        x--;
        if (last_op === DIFF_DELETE) {
          path[pathLength - 1][1] += text1.charAt(text1.length - x - 1);
        } else {
          path[pathLength++] =
              [DIFF_DELETE, text1.charAt(text1.length - x - 1)];
        }
        last_op = DIFF_DELETE;
        break;
      } else if (v_map[d].hasOwnProperty ?
                 v_map[d].hasOwnProperty(x + ',' + (y - 1)) :
                 (v_map[d][x + ',' + (y - 1)] !== undefined)) {
        y--;
        if (last_op === DIFF_INSERT) {
          path[pathLength - 1][1] += text2.charAt(text2.length - y - 1);
        } else {
          path[pathLength++] =
              [DIFF_INSERT, text2.charAt(text2.length - y - 1)];
        }
        last_op = DIFF_INSERT;
        break;
      } else {
        x--;
        y--;
        //if (text1.charAt(text1.length - x - 1) !=
        //    text2.charAt(text2.length - y - 1)) {
        //  throw new Error('No diagonal.  Can\'t happen. (diff_path2)');
        //}
        if (last_op === DIFF_EQUAL) {
          path[pathLength - 1][1] += text1.charAt(text1.length - x - 1);
        } else {
          path[pathLength++] =
              [DIFF_EQUAL, text1.charAt(text1.length - x - 1)];
        }
        last_op = DIFF_EQUAL;
      }
    }
  }
  return path;
};


/**
 * Determine the common prefix of two strings
 * @param {string} text1 First string
 * @param {string} text2 Second string
 * @return {number} The number of characters common to the start of each
 *     string.
 */
diff_match_patch.prototype.diff_commonPrefix = function(text1, text2) {
  // Quick check for common null cases.
  if (!text1 || !text2 || text1.charCodeAt(0) !== text2.charCodeAt(0)) {
    return 0;
  }
  // Binary search.
  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
  var pointermin = 0;
  var pointermax = Math.min(text1.length, text2.length);
  var pointermid = pointermax;
  var pointerstart = 0;
  while (pointermin < pointermid) {
    if (text1.substring(pointerstart, pointermid) ==
        text2.substring(pointerstart, pointermid)) {
      pointermin = pointermid;
      pointerstart = pointermin;
    } else {
      pointermax = pointermid;
    }
    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
  }
  return pointermid;
};


/**
 * Determine the common suffix of two strings
 * @param {string} text1 First string
 * @param {string} text2 Second string
 * @return {number} The number of characters common to the end of each string.
 */
diff_match_patch.prototype.diff_commonSuffix = function(text1, text2) {
  // Quick check for common null cases.
  if (!text1 || !text2 || text1.charCodeAt(text1.length - 1) !==
                          text2.charCodeAt(text2.length - 1)) {
    return 0;
  }
  // Binary search.
  // Performance analysis: http://neil.fraser.name/news/2007/10/09/
  var pointermin = 0;
  var pointermax = Math.min(text1.length, text2.length);
  var pointermid = pointermax;
  var pointerend = 0;
  while (pointermin < pointermid) {
    if (text1.substring(text1.length - pointermid, text1.length - pointerend) ==
        text2.substring(text2.length - pointermid, text2.length - pointerend)) {
      pointermin = pointermid;
      pointerend = pointermin;
    } else {
      pointermax = pointermid;
    }
    pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin);
  }
  return pointermid;
};


/**
 * Do the two texts share a substring which is at least half the length of the
 * longer text?
 * @param {string} text1 First string
 * @param {string} text2 Second string
 * @return {Array.<string>?} Five element Array, containing the prefix of
 *     text1, the suffix of text1, the prefix of text2, the suffix of
 *     text2 and the common middle.  Or null if there was no match.
 */
diff_match_patch.prototype.diff_halfMatch = function(text1, text2) {
  var longtext = text1.length > text2.length ? text1 : text2;
  var shorttext = text1.length > text2.length ? text2 : text1;
  if (longtext.length < 10 || shorttext.length < 1) {
    return null;  // Pointless.
  }
  var dmp = this;  // 'this' becomes 'window' in a closure.

  /**
   * Does a substring of shorttext exist within longtext such that the substring
   * is at least half the length of longtext?
   * Closure, but does not reference any external variables.
   * @param {string} longtext Longer string
   * @param {string} shorttext Shorter string
   * @param {number} i Start index of quarter length substring within longtext
   * @return {Array.<string>?} Five element Array, containing the prefix of
   *     longtext, the suffix of longtext, the prefix of shorttext, the suffix
   *     of shorttext and the common middle.  Or null if there was no match.
   */
  function diff_halfMatchI(longtext, shorttext, i) {
    // Start with a 1/4 length substring at position i as a seed.
    var seed = longtext.substring(i, i + Math.floor(longtext.length / 4));
    var j = -1;
    var best_common = '';
    var best_longtext_a, best_longtext_b, best_shorttext_a, best_shorttext_b;
    while ((j = shorttext.indexOf(seed, j + 1)) != -1) {
      var prefixLength = dmp.diff_commonPrefix(longtext.substring(i),
                                     shorttext.substring(j));
      var suffixLength = dmp.diff_commonSuffix(longtext.substring(0, i),
                                     shorttext.substring(0, j));
      if (best_common.length < suffixLength + prefixLength) {
        best_common = shorttext.substring(j - suffixLength, j) +
            shorttext.substring(j, j + prefixLength);
        best_longtext_a = longtext.substring(0, i - suffixLength);
        best_longtext_b = longtext.substring(i + prefixLength);
        best_shorttext_a = shorttext.substring(0, j - suffixLength);
        best_shorttext_b = shorttext.substring(j + prefixLength);
      }
    }
    if (best_common.length >= longtext.length / 2) {
      return [best_longtext_a, best_longtext_b,
              best_shorttext_a, best_shorttext_b, best_common];
    } else {
      return null;
    }
  }

  // First check if the second quarter is the seed for a half-match.
  var hm1 = diff_halfMatchI(longtext, shorttext,
                             Math.ceil(longtext.length / 4));
  // Check again based on the third quarter.
  var hm2 = diff_halfMatchI(longtext, shorttext,
                             Math.ceil(longtext.length / 2));
  var hm;
  if (!hm1 && !hm2) {
    return null;
  } else if (!hm2) {
    hm = hm1;
  } else if (!hm1) {
    hm = hm2;
  } else {
    // Both matched.  Select the longest.
    hm = hm1[4].length > hm2[4].length ? hm1 : hm2;
  }

  // A half-match was found, sort out the return data.
  var text1_a, text1_b, text2_a, text2_b;
  if (text1.length > text2.length) {
    text1_a = hm[0];
    text1_b = hm[1];
    text2_a = hm[2];
    text2_b = hm[3];
  } else {
    text2_a = hm[0];
    text2_b = hm[1];
    text1_a = hm[2];
    text1_b = hm[3];
  }
  var mid_common = hm[4];
  return [text1_a, text1_b, text2_a, text2_b, mid_common];
};


/**
 * Reduce the number of edits by eliminating semantically trivial equalities.
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 */
diff_match_patch.prototype.diff_cleanupSemantic = function(diffs) {
  var changes = false;
  var equalities = [];  // Stack of indices where equalities are found.
  var equalitiesLength = 0;  // Keeping our own length var is faster in JS.
  var lastequality = null;  // Always equal to equalities[equalitiesLength-1][1]
  var pointer = 0;  // Index of current position.
  // Number of characters that changed prior to the equality.
  var length_changes1 = 0;
  // Number of characters that changed after the equality.
  var length_changes2 = 0;
  while (pointer < diffs.length) {
    if (diffs[pointer][0] == DIFF_EQUAL) {  // equality found
      equalities[equalitiesLength++] = pointer;
      length_changes1 = length_changes2;
      length_changes2 = 0;
      lastequality = diffs[pointer][1];
    } else {  // an insertion or deletion
      length_changes2 += diffs[pointer][1].length;
      if (lastequality !== null && (lastequality.length <= length_changes1) &&
          (lastequality.length <= length_changes2)) {
        // Duplicate record
        diffs.splice(equalities[equalitiesLength - 1], 0,
                     [DIFF_DELETE, lastequality]);
        // Change second copy to insert.
        diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
        // Throw away the equality we just deleted.
        equalitiesLength--;
        // Throw away the previous equality (it needs to be reevaluated).
        equalitiesLength--;
        pointer = equalitiesLength ? equalities[equalitiesLength - 1] : -1;
        length_changes1 = 0;  // Reset the counters.
        length_changes2 = 0;
        lastequality = null;
        changes = true;
      }
    }
    pointer++;
  }
  if (changes) {
    this.diff_cleanupMerge(diffs);
  }
  this.diff_cleanupSemanticLossless(diffs);
};


/**
 * Look for single edits surrounded on both sides by equalities
 * which can be shifted sideways to align the edit to a word boundary.
 * e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 */
diff_match_patch.prototype.diff_cleanupSemanticLossless = function(diffs) {
  /**
   * Given three strings, compute a score representing whether the two internal
   * boundaries fall on word boundaries.
   * Closure, but does not reference any external variables.
   * @param {string} one First string
   * @param {string} two Second string
   * @param {string} three Third string
   * @return {number} The score.
   */
  function diff_cleanupSemanticScore(one, two, three) {
    var whitespace = /\s/;
    var score = 0;
    if (one.charAt(one.length - 1).match(whitespace) ||
        two.charAt(0).match(whitespace)) {
      score++;
    }
    if (two.charAt(two.length - 1).match(whitespace) ||
        three.charAt(0).match(whitespace)) {
      score++;
    }
    return score;
  }

  var pointer = 1;
  // Intentionally ignore the first and last element (don't need checking).
  while (pointer < diffs.length - 1) {
    if (diffs[pointer - 1][0] == DIFF_EQUAL &&
        diffs[pointer + 1][0] == DIFF_EQUAL) {
      // This is a single edit surrounded by equalities.
      var equality1 = diffs[pointer - 1][1];
      var edit = diffs[pointer][1];
      var equality2 = diffs[pointer + 1][1];

      // First, shift the edit as far left as possible.
      var commonOffset = this.diff_commonSuffix(equality1, edit);
      if (commonOffset) {
        var commonString = edit.substring(edit.length - commonOffset);
        equality1 = equality1.substring(0, equality1.length - commonOffset);
        edit = commonString + edit.substring(0, edit.length - commonOffset);
        equality2 = commonString + equality2;
      }

      // Second, step character by character right, looking for the best fit.
      var bestEquality1 = equality1;
      var bestEdit = edit;
      var bestEquality2 = equality2;
      var bestScore = diff_cleanupSemanticScore(equality1, edit, equality2);
      while (edit.charAt(0) === equality2.charAt(0)) {
        equality1 += edit.charAt(0);
        edit = edit.substring(1) + equality2.charAt(0);
        equality2 = equality2.substring(1);
        var score = diff_cleanupSemanticScore(equality1, edit, equality2);
        if (score >= bestScore) {
          bestScore = score;
          bestEquality1 = equality1;
          bestEdit = edit;
          bestEquality2 = equality2;
        }
      }

      if (diffs[pointer - 1][1] != bestEquality1) {
        // We have an improvement, save it back to the diff.
        diffs[pointer - 1][1] = bestEquality1;
        diffs[pointer][1] = bestEdit;
        diffs[pointer + 1][1] = bestEquality2;
      }
    }
    pointer++;
  }
};


/**
 * Reduce the number of edits by eliminating operationally trivial equalities.
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 */
diff_match_patch.prototype.diff_cleanupEfficiency = function(diffs) {
  var changes = false;
  var equalities = [];  // Stack of indices where equalities are found.
  var equalitiesLength = 0;  // Keeping our own length var is faster in JS.
  var lastequality = '';  // Always equal to equalities[equalitiesLength-1][1]
  var pointer = 0;  // Index of current position.
  // Is there an insertion operation before the last equality.
  var pre_ins = false;
  // Is there a deletion operation before the last equality.
  var pre_del = false;
  // Is there an insertion operation after the last equality.
  var post_ins = false;
  // Is there a deletion operation after the last equality.
  var post_del = false;
  while (pointer < diffs.length) {
    if (diffs[pointer][0] == DIFF_EQUAL) {  // equality found
      if (diffs[pointer][1].length < this.Diff_EditCost &&
          (post_ins || post_del)) {
        // Candidate found.
        equalities[equalitiesLength++] = pointer;
        pre_ins = post_ins;
        pre_del = post_del;
        lastequality = diffs[pointer][1];
      } else {
        // Not a candidate, and can never become one.
        equalitiesLength = 0;
        lastequality = '';
      }
      post_ins = post_del = false;
    } else {  // an insertion or deletion
      if (diffs[pointer][0] == DIFF_DELETE) {
        post_del = true;
      } else {
        post_ins = true;
      }
      /*
       * Five types to be split:
       * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
       * <ins>A</ins>X<ins>C</ins><del>D</del>
       * <ins>A</ins><del>B</del>X<ins>C</ins>
       * <ins>A</del>X<ins>C</ins><del>D</del>
       * <ins>A</ins><del>B</del>X<del>C</del>
       */
      if (lastequality && ((pre_ins && pre_del && post_ins && post_del) ||
                           ((lastequality.length < this.Diff_EditCost / 2) &&
                            (pre_ins + pre_del + post_ins + post_del) == 3))) {
        // Duplicate record
        diffs.splice(equalities[equalitiesLength - 1], 0,
                     [DIFF_DELETE, lastequality]);
        // Change second copy to insert.
        diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT;
        equalitiesLength--;  // Throw away the equality we just deleted;
        lastequality = '';
        if (pre_ins && pre_del) {
          // No changes made which could affect previous entry, keep going.
          post_ins = post_del = true;
          equalitiesLength = 0;
        } else {
          equalitiesLength--;  // Throw away the previous equality;
          pointer = equalitiesLength ? equalities[equalitiesLength - 1] : -1;
          post_ins = post_del = false;
        }
        changes = true;
      }
    }
    pointer++;
  }

  if (changes) {
    this.diff_cleanupMerge(diffs);
  }
};

/**
 * Reorder and merge like edit sections.  Merge equalities.
 * Any edit section can move as long as it doesn't cross an equality.
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 */
diff_match_patch.prototype.diff_cleanupMerge = function(diffs) {
  diffs.push([DIFF_EQUAL, '']);  // Add a dummy entry at the end.
  var pointer = 0;
  var count_delete = 0;
  var count_insert = 0;
  var text_delete = '';
  var text_insert = '';
  var commonlength;
  while (pointer < diffs.length) {
    switch (diffs[pointer][0]) {
    case DIFF_INSERT:
      count_insert++;
      text_insert += diffs[pointer][1];
      pointer++;
      break;
    case DIFF_DELETE:
      count_delete++;
      text_delete += diffs[pointer][1];
      pointer++;
      break;
    case DIFF_EQUAL:
      // Upon reaching an equality, check for prior redundancies.
      if (count_delete !== 0 || count_insert !== 0) {
        if (count_delete !== 0 && count_insert !== 0) {
          // Factor out any common prefixies.
          commonlength = this.diff_commonPrefix(text_insert, text_delete);
          if (commonlength !== 0) {
            if ((pointer - count_delete - count_insert) > 0 &&
                diffs[pointer - count_delete - count_insert - 1][0] ==
                DIFF_EQUAL) {
              diffs[pointer - count_delete - count_insert - 1][1] +=
                  text_insert.substring(0, commonlength);
            } else {
              diffs.splice(0, 0, [DIFF_EQUAL,
                  text_insert.substring(0, commonlength)]);
              pointer++;
            }
            text_insert = text_insert.substring(commonlength);
            text_delete = text_delete.substring(commonlength);
          }
          // Factor out any common suffixies.
          commonlength = this.diff_commonSuffix(text_insert, text_delete);
          if (commonlength !== 0) {
            diffs[pointer][1] = text_insert.substring(text_insert.length -
                commonlength) + diffs[pointer][1];
            text_insert = text_insert.substring(0, text_insert.length -
                commonlength);
            text_delete = text_delete.substring(0, text_delete.length -
                commonlength);
          }
        }
        // Delete the offending records and add the merged ones.
        if (count_delete === 0) {
          diffs.splice(pointer - count_delete - count_insert,
                       count_delete + count_insert, [DIFF_INSERT, text_insert]);
        } else if (count_insert === 0) {
          diffs.splice(pointer - count_delete - count_insert,
                       count_delete + count_insert, [DIFF_DELETE, text_delete]);
        } else {
          diffs.splice(pointer - count_delete - count_insert,
                       count_delete + count_insert, [DIFF_DELETE, text_delete],
                       [DIFF_INSERT, text_insert]);
        }
        pointer = pointer - count_delete - count_insert +
                  (count_delete ? 1 : 0) + (count_insert ? 1 : 0) + 1;
      } else if (pointer !== 0 && diffs[pointer - 1][0] == DIFF_EQUAL) {
        // Merge this equality with the previous one.
        diffs[pointer - 1][1] += diffs[pointer][1];
        diffs.splice(pointer, 1);
      } else {
        pointer++;
      }
      count_insert = 0;
      count_delete = 0;
      text_delete = '';
      text_insert = '';
      break;
    }
  }
  if (diffs[diffs.length - 1][1] === '') {
    diffs.pop();  // Remove the dummy entry at the end.
  }

  // Second pass: look for single edits surrounded on both sides by equalities
  // which can be shifted sideways to eliminate an equality.
  // e.g: A<ins>BA</ins>C -> <ins>AB</ins>AC
  var changes = false;
  pointer = 1;
  // Intentionally ignore the first and last element (don't need checking).
  while (pointer < diffs.length - 1) {
    if (diffs[pointer - 1][0] == DIFF_EQUAL &&
        diffs[pointer + 1][0] == DIFF_EQUAL) {
      // This is a single edit surrounded by equalities.
      if (diffs[pointer][1].substring(diffs[pointer][1].length -
          diffs[pointer - 1][1].length) == diffs[pointer - 1][1]) {
        // Shift the edit over the previous equality.
        diffs[pointer][1] = diffs[pointer - 1][1] +
            diffs[pointer][1].substring(0, diffs[pointer][1].length -
                                        diffs[pointer - 1][1].length);
        diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1];
        diffs.splice(pointer - 1, 1);
        changes = true;
      } else if (diffs[pointer][1].substring(0, diffs[pointer + 1][1].length)
          == diffs[pointer + 1][1]) {
        // Shift the edit over the next equality.
        diffs[pointer - 1][1] += diffs[pointer + 1][1];
        diffs[pointer][1] =
            diffs[pointer][1].substring(diffs[pointer + 1][1].length) +
            diffs[pointer + 1][1];
        diffs.splice(pointer + 1, 1);
        changes = true;
      }
    }
    pointer++;
  }
  // If shifts were made, the diff needs reordering and another shift sweep.
  if (changes) {
    this.diff_cleanupMerge(diffs);
  }
};


/**
 * Add an index to each tuple, represents where the tuple is located in text2.
 * e.g. [[DIFF_DELETE, 'h', 0], [DIFF_INSERT, 'c', 0], [DIFF_EQUAL, 'at', 1]]
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 */
diff_match_patch.prototype.diff_addIndex = function(diffs) {
  var i = 0;
  for (var x = 0; x < diffs.length; x++) {
    diffs[x][2] = i;
    if (diffs[x][0] !== DIFF_DELETE) {
      i += diffs[x][1].length;
    }
  }
};


/**
 * Convert a diff array into a pretty HTML report.
 * @param {Array.<Array.<*>>} diffs Array of diff tuples
 * @return {string} HTML representation
 */
diff_match_patch.prototype.diff_prettyHtml = function(diffs) {
  this.diff_addIndex(diffs);
  var html = [];
  for (var x = 0; x < diffs.length; x++) {
    var m = diffs[x][0]; // Mode (delete, equal, insert)
    var t = diffs[x][1]; // Text of change.
    var i = diffs[x][2]; // Index of change.
    t = t.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    if(config.macros.diff && config.macros.diff.displayEOL)
		t = t.replace(/\n/g, '&para;\n');
    switch (m) {
    case DIFF_INSERT:
      html[x] = '<ins title="i=' + i + '">' +
              t + '</ins>';
      break;
    case DIFF_DELETE:
      html[x] = '<del title="i=' + i + '">' +
              t + '</del>';
      break;
    case DIFF_EQUAL:
      html[x] = t;
      break;
    }
  }
  return html.join('');
};
//}}}
/***
 From http://trac.tiddlywiki.org/browser/Trunk/core/tests/js/mock.js?rev=5613
 with the addition of a reset() method
***/
//{{{
tests_mock = {
	frame:  {},

	/*
 	 *  replace named global functions with a mock version
	 *  - mock can access original function, and store context in tests_mock.frame[funcName]
	 */
	before: function(funcName,mocker)
	{
		var frame = {};
		frame.called = 0;
		frame.savedFunc = eval(funcName); 
		if (typeof frame.savedFunc != "function") 
			throw(funcName +" is not a function: " + (typeof frame.savedFunc));

		var mockFunction = function() { 
			tests_mock.frame[funcName].called++; 
			if (mocker)
			    return mocker.apply(this, arguments);
		};
		eval(funcName + "=mockFunction");

		this.frame[funcName] = frame;
	},

	/*
	 *  restore named global function
	 *  - return frame object, which includes a count of calls
	 */
	after: function(funcName) 
	{
		frame = this.frame[funcName];
		eval(funcName + '=frame.savedFunc');
		return frame;
	},

	/*
	 *  save values of named global variables
	 */
	save: function(varName)
	{
		var frame = {};
		frame.restore = true;
		frame.savedValue = eval(varName); 
		if (typeof frame.savedValue == "function") 
			throw(varName +" is a function: " + (typeof frame.savedValue));

		this.frame[varName] = frame;
	},

	/*
	 *  restore any named global variables
	 */
	restore: function() 
	{
		var varName;
		for(varName in this.frame) {
			frame = this.frame[varName];
			if (frame.restore){
			    eval(varName+'=frame.savedValue');
			    frame.restore = false;
			}
		}
	},
	/*
	 * reset everything
	 */
	reset: function()
	{
		this.restore();
		var funcName;
		for(funcName in this.frame) {
			if (this.frame[funcName].savedFunc){
			   this.after(funcName);
			}
		}
		this.frame = {};
	}
};


//}}}