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
<!--{{{-->
<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 excludeLists'></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>>
<<importTiddlers>>
<!--{{{-->
<link rel='alternate' type='application/rss+xml' title='RSS' href='index.xml' />
<!--}}}-->
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]]
<!--{{{-->
<div class='header' role='banner'>
  <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' role='navigation' refresh='content' tiddler='MainMenu'></div>
<div id='sidebar'>
  <div id='sidebarOptions' role='navigation' refresh='content' tiddler='SideBarOptions'></div>
  <div id='sidebarTabs' role='complementary' refresh='content' force='true' tiddler='SideBarTabs'></div>
</div>
<div id='displayArea' role='main'>
<div id='messageArea'></div>
<div id='tiddlerDisplay'></div>
</div>
<!--}}}-->
/*{{{*/
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]]; }
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: -moz-linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
	background: linear-gradient(to bottom, [[ColorPalette::PrimaryLight]], [[ColorPalette::PrimaryMid]]);
}
.header a:hover {background:transparent;}
.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]]; }
.wizard__title    { color:[[ColorPalette::PrimaryDark]]; border:none; }
.wizard__subtitle { color:[[ColorPalette::Foreground]]; border:none; }
.wizardStep { background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]]; }
.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 { background:[[ColorPalette::SecondaryLight]]; color:[[ColorPalette::Foreground]]; box-shadow: 1px 2px 5px [[ColorPalette::TertiaryMid]]; }
.messageToolbar__button { color:[[ColorPalette::PrimaryMid]]; background:[[ColorPalette::SecondaryPale]]; border:none; }
.messageToolbar__button_withIcon { background:inherit; }
.messageToolbar__button_withIcon:active { background:inherit; border:none; }
.messageToolbar__icon { fill:[[ColorPalette::TertiaryDark]]; }
.messageToolbar__icon:hover { fill:[[ColorPalette::Foreground]]; }

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

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

.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::TertiaryLight]]; }
.tagging .listTitle, .tagged .listTitle {color:[[ColorPalette::PrimaryDark]];}
.tagging .button, .tagged .button {border:none;}

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

.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]]; }

.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 th, .viewer thead td, .twtable th, .twtable thead td { background: [[ColorPalette::SecondaryMid]]; color: [[ColorPalette::Background]]; }
.viewer td, .viewer tr, .twtable td, .twtable tr { border: 1px solid [[ColorPalette::TertiaryLight]]; }
.twtable caption { color: [[ColorPalette::TertiaryMid]]; }

.viewer pre {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]];}
.readOnly {background:[[ColorPalette::TertiaryPale]];}

#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);}
/*}}}*/
/*{{{*/
body { font-size:.75em; font-family:arial,helvetica,sans-serif; margin:0; padding:0; }

* html .tiddler {height:1%;}

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;}

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;}

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


a {text-decoration:none;}

.externalLink {text-decoration:underline;}

.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;}
.headerShadow {position:relative; padding:3em 0 1em 1em; left:-1px; top:-1px;}
.headerForeground {position:absolute; padding:3em 0 1em 1em; left:0; top:0;}

.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:0 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 0.3em 0;}
#sidebarTabs .tabContents {width:15em; overflow:hidden;}

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

#messageArea { position:fixed; top:2em; right:0; margin:0.5em; padding:0.7em 1em; z-index:2000; }
.messageToolbar { text-align:right; padding:0.2em 0; }
.messageToolbar__button { text-decoration:underline; }
.messageToolbar__icon { height: 1em; width: 1em; } /* width for IE */
.messageArea__text a { text-decoration:underline; }

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

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

.tabset {padding:1em 0 0 0.5em;}
.tab {margin:0 0 0 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 0 14em;}

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

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

.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;}
.tagged li, .tagging li { margin: 0.3em 0; }
.tagClear {clear:both;}

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

.annotation { padding: 0.5em 0.8em; margin: 0.5em 1px; }

.viewer {line-height:1.4em; padding-top:0.5em;}
.viewer .button {margin:0 0.25em; padding:0 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 0; }
.viewer th, .viewer td, .viewer tr, .viewer caption, .twtable th, .twtable td, .twtable tr, .twtable caption { padding: 0.2em 0.4em; }
.twtable caption { font-size: 0.9em; }
table.listView { margin: 0.8em 1.0em; }
table.listView th, table.listView td, table.listView tr { text-align: left; }
.listView > thead { position: sticky; top: 0; }

* html .viewer pre {width:99%; padding:0 0 1em 0;}
.viewer pre {padding:0.5em; overflow:auto;}
pre, code { font-family: monospace, monospace; font-size: 1em; }
.viewer pre, .viewer code { line-height: 1.4em; }

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

.fieldsetFix {border:0; padding:0; margin:1px 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;}
#backstageToolbar {position:relative;}
#backstageArea a {font-weight:bold; margin-left:0.5em; padding:0.3em 0.5em;}
#backstageButton {display:none; position:absolute; z-index:175; top:0; right:0;}
#backstageButton a {padding:0.1em 0.4em; margin:0.1em;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel { display:none; z-index:100; position:absolute; width:90%; margin-left:3em; }
.backstagePanelFooter {padding-top:0.2em; float:right;}
.backstagePanelFooter a {padding: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; }
}
/*}}}*/
<!--{{{-->
<div class='toolbar' role='navigation' 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>
<!--}}}-->
[[Tiddlyhost|https://tiddlyhost.com]] is a hosting service for ~TiddlyWiki.
no
[[KeyOptionExperiment]]
[[GettingStarted]]
|Source     |https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsCollection.txt|
|Description|This is a central collection for ExtensionsExplorerPlugin. It is meant to gather collections of existing extensions, and also to help new authors make their work more explorable.|
|Version    |0.2.2|
Current status is "under construction", meaning that there's a lot of collections and extensions to add. Other things should be considered as well, like quality guidelines, handling forks, etc. Instructions for authors will be published in a separate readme and linked here.
//{{{
[
  {
    "url": "https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt",
    "description": "Extensions created or heavily modified by Yakov Litvin",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_abego/blob/master/maintained/AbegoCollection.txt",
    "description": "Extensions created by Udo Borkowski",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_Extensions/blob/master/TranslationsCollection.txt",
    "description": "TiddlyWiki Translations",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyThemes/blob/master/ThemesCollection.txt",
    "description": "TiddlyWiki themes (see also https://yakovl.github.io/TiddlyThemes/)",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_Extensions/blob/master/FND/SimpleSearchPlugin.js",
    "description": "Displays search results as a simple list of matching tiddlers"
  },
  {
    "url": "https://github.com/PengjuYan/TiddlyWiki_SwitchPalettePlugin/blob/master/SwitchPalettePlugin.js",
    "description": "Switches among your color palettes"
  },
  {
    "url": "https://github.com/wangyenshu/TiddlyWikiClassicPluginsArchives/blob/main/TiddlyWikiClassicPluginsArchives.txt",
    "description": "Dedicated extensions archives, covering most of the existing plugins (collection per TW, like TiddlyTools or PeachTW)",
    "type": "collection"
  }
]
//}}}
/***
|Description|checks and reports updates of installed extensions on startup, introduces a macro/backstage button to explore, install and update extensions|
|Version    |0.7.0|
|Author     |Yakov Litvin|
|Source     |https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Installation & configuration
Installation of the plugin is as usual: import the tiddler or copy and tag it with {{{systemConfig}}}; reload TW.

!!!What EEP does, how to use it
Once you install this plugin, on startup, it will try to check if installed extensions have any updates available and report if it finds any. An update of a particular extension is looked up by the url in the Source slice (see this tiddler for example). EEP will recognize an "update" if it finds the content by that url, and that content has a Version slice and the version is higher than the installed one (like: 0.4.2 is higher than 0.3.9; 0.0.1 is also higher than none).

It also adds "explore extensions" in the backstage (and the {{{<<extensionsExplorer>>}}} macro with the same interface) that shows some extensions available for installation and the list of installed plugins with buttons to check for updates.

Note: With some TW savers/servers, loading an extension may fail if its author hasn't enabled CORS on the server pointed by Source.

!!!For extension authors: how to prepare extensions and repositories
To make EEP find updates for your extensions, you have to
# put it somewhere in the internet:
** the server should have CORS enabled (~GitHub is fine);
** the extension should be in either form: "plain text" (.js or .txt file extension) or a tiddler in a TW (.html extension);
# ensure that the extension has a Source slice with a url that points to itself (i.e. where to look for the latest version):
** for plain text, one can use a direct url, like: https://raw.githubusercontent.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/master/ShowUnsavedPlugin.js;
** for ~GitHub, one can also use the url of the UI page (i.e. navigate to it via ~GitHub UI and copy the address): https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js;
** for a tiddler inside a TW, use a permalink, like: https://TiddlyTools.com/Classic/#NestedSlidersPlugin (note that the Source slice in this plugin is in fact outdated: http://www.TiddlyTools.com/#NestedSlidersPlugin – you should avoid that as this will break the updating flow);
** for a tiddler inside a TW on ~GitHub, use ~GitHub Pages (this is in fact how ~TiddlyTools is served, they just use a custom domain; an example of an "ordinary" url: https://yakovl.github.io/TiddlyWiki_ExtraFilters/#ExtraFiltersPlugin);
** for your dev flow, it may be useful to put the plugin to ~GitHub as a .js file and load it into the demo TW via [[TiddlerInFilePlugin|https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin]]. An example of such setup can be found [[here|https://github.com/YakovL/TiddlyWiki_FromPlaceToPlacePlugin]].

***/
//{{{
// Returns the slice value if it is present or defaultText otherwise
//
Tiddler.prototype.getSlice = Tiddler.prototype.getSlice || function(sliceName, defaultText) {
	let re = TiddlyWiki.prototype.slicesRE, m
	re.lastIndex = 0
	while(m = re.exec(this.text)) {
		if(m[2]) {
			if(m[2] == sliceName) return m[3]
		} else {
			if(m[5] == sliceName) return m[6]
		}
	}
	return defaultText
}

const centralSourcesListName = "AvailableExtensions"

config.macros.extensionsExplorer = {
	lingo: {
		backstageButtonLabel: "explore extensions",
		backstageButtonTooltip: "See if there are any updates or install new ones",
		installButtonLabel: "install",
		installButtonPrompt: "get and install this extension",
		otherActionsPrompt: "show other actions",
		getFailedToLoadMsg: name => "failed to load " + name,
		getSucceededToLoadMsg: name => `loaded ${name}, about to import and install...`,
		noSourceUrlAvailable: "no source url",
		getEvalSuccessMsg: name => `Successfully installed ${name} (reload is not necessary)`,
		getEvalFailMsg: (name, error) => `${name} failed with error: ${error}`,
		getImportSuccessMsg: (title, versionString, isUpdated) => isUpdated ?
			`Updated ${title}${versionString ? " to " + versionString : ""}` :
			`Imported ${title}${versionString ? " v" + versionString : ""}`,

		updateButtonCheckLabel: "check",
		updateButtonCheckPrompt: "check for updates",
		updateButtonUpdateLabel: "update",
		updateButtonUpdatePrompt: "install available update",
		getUpdateAvailableMsg: name => `update of ${name} is available!`,
		getUpdateAvailableAndVersionsMsg: (existingTiddler, newTiddler) => {
			const getVersionString = config.macros.extensionsExplorer.getVersionString
			return `update of ${existingTiddler.title} is available ` +
				"(current version: " + getVersionString(existingTiddler) +
				", available version: " + getVersionString(newTiddler) + ")"
		},
		updateNotAvailable: "update is not available",
		getUpdateConfirmMsg: (title, loadedVersion, presentVersion) => {
			const loadedVersionString = loadedVersion ? formatVersion(loadedVersion) : ""
			const presentVersionString = presentVersion ? formatVersion(presentVersion) : ""
			return `Would you like to update ${title}` +
				` (new version: ${loadedVersionString || "unknown"}, ` +
			 	`current version: ${presentVersionString || "unknown"})?`
		},

		centralSourcesListAnnotation: "The JSON here describes extensions so that ExtensionsExplorerPlugin can install them"
	},

	// helpers specific to tiddler format
	guessExtensionType: function(tiddler) {
		if(tiddler.tags.contains('systemConfig') ||
		   tiddler.getSlice('Type', '').toLowerCase() == 'plugin' ||
		   /Plugin$/.exec(tiddler.title)
		)
			return 'plugin'
	},
	// We use the server.host field a bit different than the core does (see importing):
	// we keep #TiddlerName part which won't hurt except for the plugin https://github.com/TiddlyWiki/tiddlywiki/blob/master/plugins/Sync.js (which we kinda substitute anyway),
	// we also don't set server.type and server.page.revision fields yet (unlike import); see also server.workspace, wikiformat fields.
	sourceUrlField: 'server.host',
	getSourceUrl: function(tiddler) {
		return tiddler.fields[this.sourceUrlField] || tiddler.getSlice('Source')
		//# try also the field set by import (figure the name by experiment)
	},
	setSourceUrl: function(tiddler, url) {
		//# simple implementation, not sure if setValue should be used instead
		tiddler.fields[this.sourceUrlField] = url
	},
	getDescription: tiddler => tiddler.getSlice('Description', ''),
	getVersionString: tiddler => tiddler.getSlice('Version', ''),
	getVersion: function(tiddler) {
		const versionString = this.getVersionString(tiddler)
		//# should use a helper from core instead
		const parts = /(\d+)\.(\d+)(?:\.(\d+))?/.exec(versionString)
		return parts ? {
			major: parseInt(parts[1]),
			minor: parseInt(parts[2]),
			revision: parseInt(parts[3] || '0')
		} : {}
	},

	// helpers to get stuff from external repos
	//# start from hardcoding 1 (.oO data sctructures needed
	//  for getAvailableExtensions and various user scenarios),
	//  then several (TW/JSON, local/remote)
	availableRepositories: [],
	getAvailableRepositories: function() {
		return this.availableRepositories
	},
	// fallback used when AvailableExtensions is empty
	defaultAvailableExtensions: [
		{
			url: 'https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsCollection.txt',
			description: 'A central extensions collection for ExtensionsExplorerPlugin meant to both gather collections of existing extensions and help new authors make their work more explorable',
			type: 'collection'
		},
		{
			// js file @ github - worked /# simplify url to be inserted?
			name: 'ShowUnsavedPlugin',
			sourceType: 'txt',
			url: 'https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js',
			description: 'highlights saving button (bold red by default) and the document title (adds a leading "*") when there are unsaved changes',
			type: 'plugin',
			text: ''
		},
		{
			url: 'https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js',
			description: 'This plugin introduces "dark mode" (changes styles) and switching it by the {{{darkMode}}} macro and operating system settings'
		},
		{
			// in TW @ remote (CORS-enabled) – worked
			name: 'FieldsEditorPlugin',
			sourceType: 'tw',
			url: 'https://yakovl.github.io/VisualTW2/VisualTW2.html#FieldsEditorPlugin',
			description: 'adds controls (create/edit/rename/delete) to the "fields" toolbar dropdown',
			type: 'plugin'
		},
		{
			// txt file @ remote without CORS – worked with _
			url: 'http://yakovlitvin.pro/TW/pre-releases/Spreadsheets.html#HandsontablePlugin',
			description: 'a test plugin on a site without CORS'
		},
		{
			url: 'https://github.com/tobibeer/TiddlyWikiPlugins/blob/master/plugins/ListFiltrPlugin.js'
		}
	],
	guessNameByUrl: function(extension) {
		if(!extension.url) return undefined
		const urlParts = extension.url.split('#')

		// site.domain/path/tw.html#TiddlerName  or  site.domain/path/#TiddlerName
		if(urlParts.length > 1 && /(\.html|\/)$/.exec(urlParts[0])) return urlParts[1]

		// <url part>/TiddlerName.txt or <url part>/TiddlerName.js
		const textPathMatch = /\/([^\/]+)\.(js|txt)$/.exec(urlParts[0])
		return textPathMatch ? textPathMatch[1] : undefined
	},
	collectionTag: 'systemExtensionsCollection',
	parseCollection: function(text) {
		/* expected format:

		< additional info, like |Source|...| and other metadata >
		//{{{
		< extensions as JSON >
		//}}}

		*/
		const match = /(\/\/{{{)\s+((?:.|\n)+)\s+(\/\/}}})\s*$/.exec(text)
		if(match) try {
			const list = JSON.parse(match[2])
			return list.map(extension => ({
				name: extension.name || this.guessNameByUrl(extension),
				...extension
			}))
		} catch (e) {
			console.log(`problems with parsing ${centralSourcesListName}:`, e)
			return null
		}
	},
	// reads .centralSourcesListName, .defaultAvailableExtensions, collections
	getAvailableExtensions: function() {
		const listText = store.getTiddlerText(centralSourcesListName)
		const availableExtensions = this.parseCollection(listText)
			|| this.defaultAvailableExtensions

		const otherCollections = store.filterTiddlers("[tag[" + this.collectionTag + "]]")
		for(const collectionTiddler of otherCollections) {
			const extensions = this.parseCollection(collectionTiddler.text)
			// for now, just merge
			if(extensions) for(const extension of extensions) {
				availableExtensions.push(extension)
			}
		}

		return availableExtensions
	},
	availableUpdatesCache: {},
	cacheAvailableUpdate: function(sourceUrl, tiddler) {
		this.availableUpdatesCache[sourceUrl] = { tiddler: tiddler }
	},
	// github urls like https://github.com/tobibeer/TiddlyWikiPlugins/blob/master/plugins/FiltrPlugin.js
	// are urls of user interface; to get raw code, we use the official githubusercontent.com service
	// also, we change the old urls https://raw.github.com/tobibeer/TiddlyWikiPlugins/master/plugins/FiltrPlugin.js
	getUrlOfRawIfGithub: function(url) {
		const ghUrlRE = /^https:\/\/github\.com\/(\w+?)\/(\w+?)\/blob\/(.+)$/
		const oldGhRawUrlRE = /^https:\/\/raw.github.com\/(\w+?)\/(\w+?)\/(.+)$/
//# test
		const match = ghUrlRE.exec(url) || oldGhRawUrlRE.exec(url)
		if(match) return 'https://raw.githubusercontent.com/' + match[1] + // username
			'/' + match[2] + // repository name
			'/' + match[3] // path
		return url
	},
	twsCache: {}, // map of strings
	/*
	@param sourceType: 'tw' | string | fasly (default = 'txt') -
	 of the tiddler source (a TW or a text file)
	@param url: string - either url of the text file or url#TiddlerName
	 for a TW (TiddlerName defines the title of the tiddler to load)
	@param title: string - is assigned to the loaded tiddler
	@param callback: tiddler | null => void
	 support second param of callback? (error/xhr)
	*/
	loadExternalTiddler: function(sourceType, url, title, callback, useCache) {
		sourceType = sourceType || this.guessSourceType(url)
		//# if sourceType is uknown, we can load file and guess afterwards
		if(sourceType == 'tw') {
			const tiddlerName = url.split('#')[1] || title
			const requestUrl = url.split('#')[0]
			const cache = this.twsCache
			const onTwLoad = function(success, params, responseText, url, xhr) {
				//# pass more info? outside: warn?
				if(!success) return callback(null)
				if(!useCache) cache[requestUrl] = responseText

				const externalTW = new TiddlyWiki()
				const result = externalTW.importTiddlyWiki(responseText)
				//# pass more info? outside: warn?
				if(!result) return callback(null)

				const tiddler = externalTW.fetchTiddler(tiddlerName)
				tiddler.title = title
				callback(tiddler)

				// above is a simple "from scratch" implementation
				//# should we reuse existing core code? (see import)
				//  currently, this only loads and passes tiddler,
				//  actual import is done in 
				const context = {
					adaptor: {},
					complete: function() {}
				}
//				FileAdaptor.loadTiddlyWikiSuccess(context, );
				//# import, see ...
				//# tiddler.title = title;
				//# callback(tiddler);
			}
			if(useCache && cache[requestUrl])
				onTwLoad(true, null, cache[requestUrl])
			else
				httpReq('GET', requestUrl, onTwLoad)
		} else {
			url = this.getUrlOfRawIfGithub(url)
			httpReq('GET', url, function(success, params, responseText, url, xhr) {
				//# pass more info? outside: warn?
				if(!success) return callback(null)

				const tiddler = new Tiddler(title)
				// remove \r originating from Windows
				tiddler.text = responseText.replace(/\r\n/g, '\n')
				tiddler.generatedByTextOnly = true
				callback(tiddler)
			})
		}
	},

	getInstalledExtensions: function() {
		//# instead of returning tiddlers, create extension objects,
		//  those should have ~isInstalled, ~isEnabled, ~hasUpdates flags
		//  (and change refresh accordingly)
		return store.filterTiddlers(`[tag[systemConfig]] ` +
			`[tag[${this.collectionTag}]] [[${centralSourcesListName}]]`)
		//# implement others: themes, transclusions
	},
	// for each installed extension, check for update and reports (now: displays message)
	init: function() {
		//# set delegated handlers of install, update buttons
		const extensionTiddlers = this.getInstalledExtensions()
		if(!config.options.chkSkipExtensionsUpdatesCheckOnStartup && !readOnly)
			for(const eTiddler of extensionTiddlers) {
				const url = this.getSourceUrl(eTiddler)
				if(!url) continue
				this.checkForUpdate(url, eTiddler, result => {
		console.log('checkForUpdate for ' + url +
			',', eTiddler, 'result is:', result)
					if(result.tiddler && !result.noUpdateMessage) {
						displayMessage(this.lingo.getUpdateAvailableAndVersionsMsg(eTiddler, result.tiddler))
					}
					//# either report each one at once,
					//   (see onUpdateCheckResponse)
					//  create summary and report,
					//   (use availableUpdates)
					//  create summary and just show "+4" or alike (better something diminishing),
					//  or even update (some of) ext-s silently
					//# start with creating summary
				})
			}

		const taskName = "explorePlugins"
		config.backstageTasks.push(taskName)
		config.tasks[taskName] = {
			text: this.lingo.backstageButtonLabel,
			tooltip: this.lingo.backstageButtonTooltip,
			content: '<<tiddler ExtensionsInBackstage>>',
		}
	},
	handler: function(place, macroName, params, wikifier, paramString) {
		// parse param "[type:installed|available]"
		const pParams = paramString.parseParams("type", null, true, false, true)
		const type = getParam(pParams, "type", "")

		const tableHeaderMarkup = "|name|description|version||h"
		// name is supposted to be a link to the repo; 3d row – for "install" button
		wikify(tableHeaderMarkup, place)
		const table = place.lastChild

		jQuery(table).attr({ refresh: 'macro', macroName: macroName })
			.addClass('extensionsExplorer').append('<tbody>')
			.attr({ 'data-eep-type': type })

		this.refresh(table)
	},
	// grabs list of available extensions and shows with buttons to install;
	// for each installed plugin, shows a button to check update or "no url" message,
	refresh: function(table) {
		const $tbody = jQuery(table).find('tbody')
			.empty()
		const type = jQuery(table).attr('data-eep-type')

		// safe method (no wikification, innerHTML etc)
		const appendRow = function(cells) {
			const row = document.createElement('tr')
			const nameCell = createTiddlyElement(row, 'td')
			if(cells.url)
				createExternalLink(nameCell, cells.url, cells.name)
			else
				createTiddlyLink(nameCell, cells.name, true)

			createTiddlyElement(row, 'td', null, null, cells.description)

			createTiddlyElement(row, 'td', null, null, cells.version)

			const actionsCell = createTiddlyElement(row, 'td', null, 'actionsCell')
			const actionsWrapper = createTiddlyElement(actionsCell, 'div', null, 'actionsWrapper')
			if(cells.actionElements.length > 0) {
				actionsWrapper.appendChild(cells.actionElements[0])
				actionsWrapper.firstChild.classList.add('mainButton')
			}
			if(cells.actionElements.length > 1) {
				const { lingo } = config.macros.extensionsExplorer
				const otherActionEls = cells.actionElements.slice(1)
				createTiddlyButton(actionsWrapper, '▾',
					lingo.otherActionsPrompt,
					function(event) {
						const popup = Popup.create(actionsWrapper)
						for(const e of otherActionEls) {
							const li = createTiddlyElement(popup, 'li')
							li.appendChild(e)
						}
						popup.style.minWidth = actionsWrapper.offsetWidth + 'px'
						Popup.show()
						event.stopPropagation()
						return false
					},
					'button otherActionsButton')
			}

			$tbody.append(row)
		}

		//# when implemented: load list of available extensions (now hardcoded)

		const installedExtensionsTiddlers = this.getInstalledExtensions()
			.sort((e1, e2) => {
				const up1 = this.availableUpdatesCache[this.getSourceUrl(e1)]
				const up2 = this.availableUpdatesCache[this.getSourceUrl(e2)]
				return	up1 && up2 ? 0 :
					up1 && !up2 ? -1 :
					up2 && !up1 ? +1 :
					!this.getSourceUrl(e1) ? +1 :
					!this.getSourceUrl(e2) ? -1 : 0
			})

		// show extensions available to install
		if(!type || type == 'available') {
			const availableExtensions = this.getAvailableExtensions()

			for(const extension of availableExtensions) {
				// skip installed
				if(installedExtensionsTiddlers.some(tid =>
					tid.title === extension.name
					&& this.getSourceUrl(tid) === extension.url)
				) continue

				if(!extension.name && extension.sourceType == 'tw')
					extension.name = extension.url.split('#')[1]

				appendRow({
					name:        extension.name,
					url:         extension.url,
					description: extension.description,
					version:     extension.version,
					actionElements: [
						createTiddlyButton(null,
							this.lingo.installButtonLabel,
							this.lingo.installButtonPrompt,
							() => this.grabAndInstall(extension) )
					]
				})
			}
		}
		//# add link to open, update on the place of install – if installed

		// show installed ones.. # or only those having updates?
		if(!type) $tbody.append(jQuery(
			`<tr><td colspan="4" style="text-align: center;">Installed</td></tr>`))
		if(!type || type == 'installed') {
			for(const extensionTiddler of installedExtensionsTiddlers) {
				//# limit the width of the table|Description column
				const updateUrl = this.getSourceUrl(extensionTiddler)
					//# check also list of extensions to install
				const onUpdateCheckResponse = (result, isAlreadyReported) => {
					if(!result.tiddler) {
						displayMessage(this.lingo.updateNotAvailable)
						//# use result.error
						return
					}
					const versionOfLoaded = this.getVersion(result.tiddler)
					const versionOfPresent = this.getVersion(extensionTiddler)

					if(compareVersions(versionOfLoaded, versionOfPresent) >= 0) {
						displayMessage(this.lingo.updateNotAvailable)
						//# use result.error
						return
					}
					if(!isAlreadyReported) displayMessage(this.lingo.getUpdateAvailableMsg(extensionTiddler.title), updateUrl)

					//# later: better than confirm? option for silent?
					if(confirm(this.lingo.getUpdateConfirmMsg(
						extensionTiddler.title,
						versionOfLoaded, versionOfPresent))
					) {
						this.updateExtension(result.tiddler, updateUrl)
					}
				}

				const checkUpdateButton = createTiddlyButton(null,
					this.lingo.updateButtonCheckLabel,
					this.lingo.updateButtonCheckPrompt,
					() => this.checkForUpdate(updateUrl, extensionTiddler,
						onUpdateCheckResponse))

				const cachedUpdate = this.availableUpdatesCache[updateUrl]
				const installUpdateButton = createTiddlyButton(null,
					this.lingo.updateButtonUpdateLabel,
					this.lingo.updateButtonUpdatePrompt,
					() => onUpdateCheckResponse(cachedUpdate, true))

				appendRow({
					name: extensionTiddler.title,
					description: this.getDescription(extensionTiddler),
					version: this.getVersionString(extensionTiddler),
					actionElements: [
						!updateUrl ? createTiddlyElement(null, 'div', null, 'actionsLabel', this.lingo.noSourceUrlAvailable) :
						cachedUpdate ? installUpdateButton :
						checkUpdateButton
					]
				})
			}
		}
	},
	grabAndInstall: function(extension) {
		if(!extension) return
		if(extension.text) {
			const extensionTiddler = new Tiddler(extension.name)
			extensionTiddler.text = extension.text
			extensionTiddler.generatedByTextOnly = true
			//# share 3 ↑ lines as ~internalize helper (with loadExternalTiddler)
			this.install(extensionTiddler, extension.type, extension.url)
			return
		}
		this.loadExternalTiddler(
			extension.sourceType,
			extension.url,
			extension.name,
			tiddler => {
				if(!tiddler) {
					displayMessage(this.lingo.getFailedToLoadMsg(extension.name))
					return
				}
				displayMessage(this.lingo.getSucceededToLoadMsg(tiddler.title))
				this.install(tiddler, extension.type ||
					this.guessExtensionType(tiddler), extension.url)
			}
		)
	},
	// evaluate if a plugin, import
	//# simple unsafe version, no dependency handling, registering as installed,
	//  _install-only-once check_, result reporting, refreshing/notifying, ..
	install: function(extensionTiddler, extensionType, sourceUrl) {
		if(!extensionTiddler) return

		const { text, title } = extensionTiddler
		switch(extensionType) {
			case 'plugin':
				// enable at once
				try {
					eval(text)
					displayMessage(this.lingo.getEvalSuccessMsg(title))
				} catch(e) {
					displayMessage(this.lingo.getEvalFailMsg(title, e))
					//# don't import? only on confirm?
				}
				// import preparation
				extensionTiddler.tags.pushUnique('systemConfig')
			break;

			case 'collection':
				extensionTiddler.tags.pushUnique(this.collectionTag)
			break;

			//# add _ tag for themes?
		}

		// actually import etc
		this.updateExtension(extensionTiddler, sourceUrl)
		//# what if exists already? (by the same name; other name)
	},
	updateExtension: function(extensionTiddler, sourceUrl) {
		// import
		var existingTiddler = store.fetchTiddler(extensionTiddler.title)
		if(extensionTiddler.generatedByTextOnly && existingTiddler) {
			existingTiddler.text = extensionTiddler.text
			existingTiddler.modified = new Date()
			//# update also modifier? changecount?
		} else {
			store.addTiddler(extensionTiddler)
		}
		if(sourceUrl && this.getSourceUrl(extensionTiddler) !== sourceUrl) {
			this.setSourceUrl(extensionTiddler, sourceUrl)
		}

		delete this.availableUpdatesCache[sourceUrl]
		store.setDirty(true)
		//# store url for updating if slice is not present?
		// make explorer and other stuff refresh
		store.notify(extensionTiddler.title, true)
		//# .oO reloading, hot reinstalling
		displayMessage(this.lingo.getImportSuccessMsg(extensionTiddler.title,
			this.getVersionString(extensionTiddler), !!existingTiddler))
	},
	guessSourceType: function(url) {
		if(/\.(txt|js)$/.exec(url.split('#')[0])) return 'txt'
		//# guess by url instead, fall back to 'txt'
		return 'tw'
	},
//# careful: extension keyword is overloaded (extension object/tiddler)
	/*
	  tries to load update for tiddler, if succeeds calls callback with
	   argument depending on whether it has newer version than the existing one
	  @param url: _
	  @param extensionTiddler: _
	  @param callback: is called [not always yet..] with argument
		{ tiddler: Tiddler | null, error?: string, noUpdateMessage?: string }
		if update is found and it has version newer than extensionTiddler,
		it is called with { tiddler: Tiddler }
	*/
	checkForUpdate: function(url, extensionTiddler, callback) {
		if(!url) return
		const title = extensionTiddler.title
		this.loadExternalTiddler(null, url, title, loadedTiddler => {
			if(!loadedTiddler) return callback({
				tiddler: null,
				error: "" //# specify
			})
			if(compareVersions(this.getVersion(loadedTiddler),
					   this.getVersion(extensionTiddler)
					  ) >= 0)
			//# also get and compare modified dates?
			{
				//# what about undefined?
				console.log('loaded is not newer')
				callback({
					tiddler: loadedTiddler,
					noUpdateMessage: "current version is up-to-date"
				})
			} else {
				this.cacheAvailableUpdate(url, loadedTiddler)
				callback({ tiddler: loadedTiddler })
			}
		})
	}
}

config.shadowTiddlers.ExtensionsInBackstage = `<<tabs txtTabExtensionsExplorer
	"check and update" "" ExtensionsExplorer
	"explore and install" "" ExtensionsOutThere
	contribute "" ContributeToExtensionsEcosystem
>>`

config.shadowTiddlers.ExtensionsExplorer = `<<extensionsExplorer type:installed>>`

config.shadowTiddlers.ExtensionsOutThere = `<<extensionsExplorer type:available>>

Some repositories not yet indexed by EEP that may be worth checking:
|[[TiddlyTools|https://tiddlytools.com/Classic]]|The largest extensions repository created mostly by a single developer, Eric Shulman. Source slice is currently outdated in all the extensions, be sure to change it to the up-to-date urls|
|[[abegoExtensions|https://yakovl.github.io/TiddlyWiki_abego/original/]]|Includes groundshaping plugins like ~ForEachTiddlerPlugin and ~YourSearchPlugin|
|[[MPTW|https://mptw.tiddlyspot.com]]|Introduces various extensions like ~RenameTagsPlugin or ~HideWhenPlugin, and ~TagglyTagging/MPTW as overall "approaches"|
||..more repositories will be added, check the "contribute" tab for more|

Old indexes of existing extensions (EEP is meant to eventually substitute them):
|[[Customize|https://yakovlitvin.pro/TW/TS_backups/customize.tiddlyspace.com%20(24.02.2016).html]]|archive of the big index created by Tobias Beer and contributors|`

config.shadowTiddlers.ContributeToExtensionsEcosystem = `Indexing estensions and repositories for EEP is work in progress. You can suggest changes in [[Github|https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin]] (via issues or ~PRs) or in the [[Google Group|https://groups.google.com/g/tiddlywikiclassic]].

Things that we encourage you to do include:
✦ Reporting missing repos for the "explore and install" tab (repositories not yet indexed by EEP);
✦ Creating collections and indexing existing extensions (either yours or created by others);
✦ Asking questions about contributing and making it as simple as possible for others.`

config.shadowTiddlers[centralSourcesListName] = '//{{{\n' +
	JSON.stringify(config.macros.extensionsExplorer.defaultAvailableExtensions, null, 2) +
	'\n//}}}'
config.annotations[centralSourcesListName] =
	config.macros.extensionsExplorer.lingo.centralSourcesListAnnotation

// Add styles
const css = `
.actionsLabel, .actionsCell .button {
	padding: 0.2em;
	display: inline-block;
	border: none;
	white-space: normal;
}
td.actionsCell {
	padding: 0;
}

.actionsWrapper {
	white-space: nowrap;
}
.button.mainButton {
	padding-left: 0.7em;
}`

const shadowName = 'ExtensionsExplorerStyles'
if(!config.shadowTiddlers[shadowName]) {
	config.shadowTiddlers[shadowName] = css
	store.addNotification(shadowName, refreshStyles)
	store.addNotification("ColorPalette", function(_, doc) { refreshStyles(shadowName, doc) })
}
//}}}
To get started with this blank ~TiddlyWiki, you'll need to modify the following tiddlers:
* [[MainMenu]]: The menu (usually on the left)
Quick setup: EEP, TGSP, SUP, SystemSettings; SiteTitle, SiteSubtitle, DefaultTiddlers
/***
todo:
* make it always save to SystemSettings (add a macro argument?), prevent saving if it's default
* add placeholder and may be tooltip to the input explaining how it works
* adapt to combinations involving only ctrl, shift, and/or alt
* learn if additional modifiers are needed for Mac

test macro: <<option keyMyFunction>>
meta: core: merge config.optionHandlers and config.macros.option.types?
***/
//{{{
// this is needed at least to update SystemSettings
config.optionHandlers.key = config.optionHandlers.txt

// re-read as we extended config.optionHandlers
loadSystemSettings()

config.macros.option.types.key = {
	elementType: 'input',
	valueField: 'value',
	className: 'keyOptionInput',
	eventName: 'onkeydown',

	create: config.macros.option.genericCreate,

	onChange: function(e) {
		if(e.preventDefault) e.preventDefault()
		// prevent propagation/bubbling
		if(e.stopPropagation) e.stopPropagation()
		e.returnValue = true // IE
		e.cancelBubble = true

		// this is a en key (what layout? always QWERTY?)
		var key = e.code.substring(0, 3) === 'Key' ? e.code.substring(3) : e.code
		var modifierPrefixes = []
		if(e.ctrlKey) modifierPrefixes.push('Ctrl+')
		if(e.altKey) modifierPrefixes.push('Alt+')
		if(e.shiftKey) modifierPrefixes.push('Shift+')
		// TODO: any other meta keys for Mac/Unix?

		// TODO: don't update on pressing just ctrl/shift/alt?
		e.target.value = modifierPrefixes.join('') + key

		return config.macros.option.genericOnChange.apply(this, arguments)
	}
}
//}}}
/***
|Description|highlights saving button (bold red by default) and the document title (adds a leading "*") when there are unsaved changes (also toggles {{{hasUnsavedChanges}}} class of the root element for hackability)|
|Version    |1.5.1|
|Author     |Yakov Litvin|
|Source     |https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
<<option chkShowDirtyStory>> show unsaved if any tiddler is opened for editing
Styles applied to unsaved TW can be adjusted in StyleSheetUnsaved
***/
//{{{
config.macros.showDirtyPlugin = {
	// styles that highlight save button when there's something to save
	showDirtyCss: ".saveChangesButton { font-weight: bold; color: red !important; }",
	styleSheetName: "suggestSavingOnDirty",
	containerClassName: "hasUnsavedChanges",
	showDrity: function(dirty) {
		const css = store.getTiddlerText('StyleSheetUnsaved')
		if(dirty) {
			jQuery('html').addClass(this.containerClassName)
			setStylesheet(css, this.styleSheetName)
			document.title = "*" + getPageTitle()
		} else {
			jQuery('html').removeClass(this.containerClassName)
			removeStyleSheet(this.styleSheetName)
			document.title = getPageTitle()
		}
	},
	checkDirty: function() {
		return store.isDirty() ||
			(config.options.chkShowDirtyStory && story.areAnyDirty())
	},
	init: function() {
		config.shadowTiddlers.StyleSheetUnsaved = this.showDirtyCss

		// add the "saveChangesButton" class to the save changes button
		config.macros.saveChanges.SCM_orig_handler = config.macros.saveChanges.handler
		config.macros.saveChanges.handler = function(place, macroName, params) {
			this.SCM_orig_handler.apply(this, arguments)
			place.lastChild.classList.add("saveChangesButton")
		}

		// regularly check and indicate unsaved
		setInterval(function() {
			const isDirty = config.macros.showDirtyPlugin.checkDirty()
			config.macros.showDirtyPlugin.showDrity(isDirty)
		}, 500)
	}
}
//}}}
/***
|Description|Makes upgrading work ~correctly with (at least) Timimi or MTS 1.7.0 and above (tested on 2.6.5,2.9.2,2.9.3 → 2.9.3,2.9.4), adds optional upgrade autocheck on start; adds tiddlers and fields sorting so that the changes are easier to review|
|Source     |https://github.com/YakovL/TiddlyWiki_SimplifiedUpgradingPlugin/blob/master/SimplifiedUpgradingPlugin.js|
|Author     |Yakov Litvin|
|Version    |0.7.1|
|License    |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
Installation of this plugin is standard: create tiddler, paste this as text, tag with {{{systemConfig}}}, save, reload.

To start upgrading, use the usual way: open backstage, the "upgrade" tab and hit the "upgrade" button.

Configuration:
<<option txtWaitSavingSeconds>> "wait saving" interval (seconds) may need adjustments for big ~TWs (otherwise, you should check that after reloading the new version is opened: if not, try to reload again)
<<option chkReloadManually>> reload manually (don't reload automatically after saving upgraded TW)
<<option chkAutocheckUpgradeOnStart>> check for upgrades on start
***/
//{{{
config.options.txtWaitSavingSeconds = config.options.txtWaitSavingSeconds || "5" // no handler for number options

// a fix for older TWs, like 2.7.1
config.macros.upgrade.source = 'https://classic.tiddlywiki.com/upgrade/'

var upgradingEventBus = {
	handlers: {},
	// no "off" method, no array of handlers for now
	on: function(name, handler) {
		this.handlers[name] = handler
	},
	fire: function(name, params) {
		if(this.handlers[name]) this.handlers[name](params)
	}
}

config.macros.simplifiedUpgrade = {
	lingo: {
		isBackupCreatedQuestion: "Have you made a backup?",
		makeBackupCall: "Please make sure you have a backup before upgrading",
		unsupportedMtsVersionMessage: "Simplified upgrading in MainTiddlySaver below 1.7.0 is not made to work properly, aborting now",
		failedToLoadCore: "Something went wrong when loading core!",
		simplifiedUpgradingDissallowed: "The new core indicates that simplified upgrading is dangerous, please use import of your TW into a new empty TW instead",
		versionNotNewer: "The available core is not newer than the current one",
		getUpgradeFinishedReloadMessage: function() {
			return "Upgrading finished, " + (config.options.chkReloadManually ?
				"reload page to have the changes applied" :
				"will reload page to have the changes applied")
		},
		upgradeMacro: {
			statusUpgrading: "building upgraded TW and saving...",
			statusUpgradedTwSaved: "upgraded TW saved, should reload now",
			getUpgradeAvailableMessage: function(version) {
				return "An upgrade to TiddlyWiki v" + formatVersion(version) + " is available"
			}
		}
	},
	start: function(newCoreString) {
		// don't upgrade without a backup
		if(!confirm(this.lingo.isBackupCreatedQuestion)) {
			alert(this.lingo.makeBackupCall)
			return
		}

		// once MTS supports upgrading, here we will check MTS version instead [or feature-detect]
		var isMainTiddlyServerUsed = !!window.saveOnlineChanges ||
			(window.tiddlyBackend && tiddlyBackend.version && tiddlyBackend.version.title == 'MainTiddlyServer')
		if(isMainTiddlyServerUsed) {
			// for now, we assume that 1.7.0 supports upgrading (this is a matter of testing), so we don't check tiddlyBackend.version.asString
			var doesMtsSupportUpgrading = !!window.tiddlyBackend
			if(!doesMtsSupportUpgrading) {
				alert(this.lingo.unsupportedMtsVersionMessage)
				return
			}
		}

		var me = this
		if(newCoreString) {
			this.proceedWithLoadedCore(newCoreString)
		}
		else this.getNewCore(function(newCoreString) {
			upgradingEventBus.fire("available-core-loaded")
			me.proceedWithLoadedCore(newCoreString)
		}, this.onCoreLoadFail)
	},
	// onSuccess(newCoreString), onProblem(jqXHR, textStatus, errorThrown)
	getNewCore: function(onSuccess, onProblem) {
		var up = config.macros.upgrade
		var url = up.getSourceURL ? up.getSourceURL() : config.options.txtUpgradeCoreURI || up.source
		ajaxReq({
			type: "GET",
			url: url,
			processData: false,
			success: onSuccess,
			error: onProblem
		})
	},
	onCoreLoadFail: function(jqXHR, textStatus, errorThrown) {
		upgradingEventBus.fire("available-core-loading-failed")
		alert(config.macros.simplifiedUpgrade.lingo.failedToLoadCore)
	},
	getSavingWaitMillisecondsInterval: function() {
		return 1000 * parseFloat(config.options.txtWaitSavingSeconds)
	},
	overrides: {},
	// main idea: make sure loadOriginal or its async analogs will return the new core, then just save
	proceedWithLoadedCore: function(newCoreString) {
		var me = config.macros.simplifiedUpgrade
		if(newCoreString.indexOf("simplifiedUpgradingDisallowed") != -1) {
			alert(me.lingo.simplifiedUpgradingDissallowed)
			return
		}
		var availableVersion = config.macros.upgrade.extractVersion(newCoreString)
		if(compareVersions(version, availableVersion) !== 1) {
			displayMessage(me.lingo.versionNotNewer)
			return
		}

		// MainTiddlyServer: avoid granulated saving (won't change core)
		me.overrides.chkAvoidGranulatedSaving = config.options.chkAvoidGranulatedSaving
		config.options.chkAvoidGranulatedSaving = true

		me.overrides.loadOriginal = loadOriginal
		loadOriginal = function loadOriginal(localPath, callback) {
			if(!callback) return newCoreString
			callback(newCoreString)
		}
		// MTS 1.7.0
		if(window.tiddlyBackend) {
			me.overrides.tiddlyBackend_loadOriginal = tiddlyBackend.loadOriginal
			tiddlyBackend.loadOriginal = function(onSuccess) {
				onSuccess(newCoreString)
			}
		}

		saveChanges()
		// restore overrides
		loadOriginal = me.overrides.loadOriginal
		if(me.overrides.tiddlyBackend_loadOriginal) tiddlyBackend.loadOriginal = me.overrides.tiddlyBackend_loadOriginal
		config.options.chkAvoidGranulatedSaving = me.overrides.chkAvoidGranulatedSaving

		// wait so that saving finishes
		setTimeout(function() {
			upgradingEventBus.fire("upgraded-tw-saved")
			me.finalize()
		}, me.getSavingWaitMillisecondsInterval())
	},
	finalize: function() {
		var me = config.macros.simplifiedUpgrade
		alert(me.lingo.getUpgradeFinishedReloadMessage())
		if(!config.options.chkReloadManually) {
			window.location.reload()
		}
	}
}

merge(config.macros.upgrade, config.macros.simplifiedUpgrade.lingo.upgradeMacro)

config.macros.upgrade.onLoadCore = function(status, params, responseText, url, xhr) {

	var me = config.macros.upgrade
	var w = params
	var errMsg = !status ? me.errorLoadingCore : undefined
	var newVer = me.extractVersion(responseText)
	if(!newVer) errMsg = me.errorCoreFormat
	if(errMsg) {
		w.setButtons([], errMsg)
		alert(errMsg)
		return
	}

	// the overridden bit
	var onStartUpgrade = function(e) {
		w.setButtons([], me.statusUpgrading)
		upgradingEventBus.on("upgraded-tw-saved", function() {
			w.setButtons([], me.statusUpgradedTwSaved)
		})
		config.macros.simplifiedUpgrade.start(responseText)
	}

	var step2 = [me.step2Html_downgrade, me.step2Html_restore, me.step2Html_upgrade][compareVersions(version, newVer) + 1]
	w.addStep(me.step2Title, step2.format([formatVersion(newVer), formatVersion(version)]))
	w.setButtons([
		{ caption: me.startLabel, tooltip: me.startPrompt, onClick: onStartUpgrade },
		{ caption: me.cancelLabel, tooltip: me.cancelPrompt, onClick: me.onCancel }
	])
}

var isBelow2_9_3 = compareVersions(version, { major: 2, minor: 9, revision: 3 }) === 1
var isAbove2_9_3 = compareVersions(version, { major: 2, minor: 9, revision: 3 }) === -1

// support upgrading regardless the whitespace after '{' (extra spaces were in 2.9._)
if(isBelow2_9_3) {
	config.macros.upgrade.extractVersion = function(upgradeFile) {
		var re = /version = \{\s*title: "([^"]+)", major: (\d+), minor: (\d+), revision: (\d+)(, beta: (\d+)){0,1}, date: new Date\("([^"]+)"\)/mg
		var m = re.exec(upgradeFile)
		return !m ? null : {
			title: m[1], major: m[2], minor: m[3], revision: m[4], beta: m[6], date: new Date(m[7])
		}
	}
}

// not present before 2.9.2
config.macros.upgrade.getSourceURL = config.macros.upgrade.getSourceURL || function() {
	return config.options.txtUpgradeCoreURI || config.macros.upgrade.source;
}

// fix the bug introduced in 2.9.3 and fixed in 2.9.4 version;
// since 2.10.0, overriding is necessary to avoid the backup loading check irrelevant for this method
config.macros.upgrade.onClickUpgrade = function(e) {
	var me = config.macros.upgrade
	var w = new Wizard(this)
	if(window.allowSave && !window.allowSave()) {
		alert(me.errorCantUpgrade)
		return false
	}
	if(story.areAnyDirty() || store.isDirty()) {
		alert(me.errorNotSaved)
		return false
	}
	
	w.setButtons([], me.statusPreparingBackup)
	var localPath = getLocalPath(document.location.toString())
	var backupPath = getBackupPath(localPath, me.backupExtension)
	var original = loadOriginal(localPath)

	w.setButtons([], me.statusSavingBackup)
	var backupSuccess = copyFile(backupPath, localPath) || saveFile(backupPath, original)
	//# fails of backup saving with TF are not reported, resulting in empty TW after upgrade
	if(!backupSuccess) {
		w.setButtons([], me.errorSavingBackup)
		alert(me.errorSavingBackup)
		return false
	}
	w.setValue("backupPath", backupPath)

	w.setButtons([], me.statusLoadingCore)
	var sourceURL = me.getSourceURL()
	ajaxReq({
		type: "GET",
		url: sourceURL,
		processData: false,
		success: function(data, textStatus, jqXHR) {
			me.onLoadCore(true, w, jqXHR.responseText, sourceURL, jqXHR)
		},
		error: function(jqXHR, textStatus, errorThrown) {
			me.onLoadCore(false, w, null, sourceURL, jqXHR)
		}
	})
	return false
}

// auto-checking available upgrade
config.macros.upgrade.init = function() {
	if(readOnly) return

	config.macros.simplifiedUpgrade.getNewCore(function(coreAsText) {
		var me = config.macros.upgrade
		var availableVersion = me.extractVersion(coreAsText)
		if(compareVersions(version, availableVersion) !== 1) return
		if(config.options.chkAutocheckUpgradeOnStart) {
			displayMessage(me.getUpgradeAvailableMessage(availableVersion))
		}
	})
}

// sort fields and tiddlers to make html deterministic and avoid unmotivated diffs
// (not introduced into the core yet, nor tested if differs from MTS's granulated saving results)
//if(!isAbove2_9_3) {
	SaverBase.prototype.externalize = function(store) {
		var results = []
		var i, tiddlers = store.getTiddlers("title")
		if(!config.options.chkAvoidSortingAll) {
			tiddlers.sort(function(t1, t2) {
				return t1.title.localeCompare(t2.title)
			})
		}
		for(i = 0; i < tiddlers.length; i++) {
			if(!tiddlers[i].doNotSave())
				results.push(this.externalizeTiddler(store, tiddlers[i]))
		}
		return results.join("\n")
	}

	TW21Saver.prototype.externalizeTiddler = function(store, tiddler) {
		try {
			var usePre = config.options.chkUsePreForStorage;
			var created = tiddler.created;
			var modified = tiddler.modified;
			var tags = tiddler.getTags();
			var attributes =
				(tiddler.creator ? ' creator="' + tiddler.creator.htmlEncode() + '"' : "") +
				(tiddler.modifier ? ' modifier="' + tiddler.modifier.htmlEncode() + '"' : "") +
				((usePre && created == version.date) ? "" : ' created="' + created.convertToYYYYMMDDHHMM() + '"') +
				((usePre && modified == created) ? "" : ' modified="' + modified.convertToYYYYMMDDHHMM() + '"') +
				((!usePre || tags) ? ' tags="' + tags.htmlEncode() + '"' : "");
			//# todo: check if these changes (sort extended attributes so that the order is always the same) affect performance, commit
			var extendedAttributes = [];
			store.forEachField(tiddler, function(tiddler, fieldName, value) {
				if(typeof value != "string")
					value = "";
				// don't store fields from the temp namespace
				if(!fieldName.match(/^temp\./))
					extendedAttributes.push('%0="%1"'.format([fieldName, value.escapeLineBreaks().htmlEncode()]));
			}, true);
			if(!config.options.chkAvoidSortingAll) {
				extendedAttributes.sort();
			}
			//# avoid closing div tags for _
			return ('<div %0="%1"%2%3>%4</' + 'div>').format([
				usePre ? "title" : "tiddler",
				tiddler.title.htmlEncode(),
				attributes,
				' ' + extendedAttributes.join(' '),
				usePre ? "\n<pre>" + tiddler.text.htmlEncode() + "</pre>\n" : tiddler.text.escapeLineBreaks().htmlEncode()
			]);
		} catch (ex) {
			throw exceptionText(ex, config.messages.tiddlerSaveError.format([tiddler.title]));
		}
	};
//}
//}}}
a place for experiments to make TWC more keyboard-friendly
Hotkeys
txtUserName: Yakov Litvin
chkSaveBackups: false
chkAutoSave: true
chkInsertTabs: true
/***
|Version|0.1.0|
|Source|https://yllab.tiddlyhost.com#ThostGoodSavingPlugin|
should also: display "save from web" (use original {{{saveChanges}}}) instead of "save to tiddlyhost"
***/
//{{{
window.saveChanges = function(onlyIfDirty, tiddlers) {
	config.macros.thostUpload.action()
}
//}}}
/***
|Name         |ThostUploadPlugin |
|Description  |Support saving to Tiddlyhost.com |
|Version      |1.0.1 |
|Date         |March 06, 2021 |
|Source       |https://github.com/tiddlyhost/tiddlyhost-com/tree/main/rails/tw_content/plugins |
|Author       |BidiX, Simon Baird, Yakov Litvin |
|License      |BSD open source license |
|~CoreVersion |2.9.2 |
***/
//{{{

version.extensions.ThostUploadPlugin = { major: 1, minor: 0, revision: 1 };

//
// Environment
//

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

// To change these defaults, create a tiddler named "ThostOptions" with tag
// "systemConfig" and the following content:
// window.bidix = { "editModeAlways": false, "uploadButtonAlways": false };

// Set false if you want the chkHttpReadOnly cookie to decide whether to
// render in read-only mode or edit mode when you're not logged in or when
// the site is being viewed by others. Default true.
if (!("editModeAlways" in bidix)) { bidix.editModeAlways = true; }

// Set false to hide the "upload to tiddlyhost" button when you're not logged
// in or when the site is being viewed by others. Default true.
if (!("uploadButtonAlways" in bidix)) { bidix.uploadButtonAlways = true; }

// For debugging. Default false.
if (!("debugMode" in bidix)) { bidix.debugMode = false; }

//
// Upload Macro
//

config.macros.thostUpload = {
  handler: function(place,macroName,params) {
    createTiddlyButton(place, "save to tiddlyhost",
      "save this TiddlyWiki to a site on Tiddlyhost.com",
      this.action, null, null, this.accessKey);
  },

  action: function(params) {
    var siteName = config.options.txtThostSiteName.trim();
    if (!siteName) {
      alert("Tiddlyhost site name is missing!");
      clearMessage();
    }
    else {
      bidix.thostUpload.uploadChanges('https://' + siteName + '.tiddlyhost.com');
    }
    return false;
  }
};

//
// Upload functions
//

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

if (!bidix.thostUpload.messages) bidix.thostUpload.messages = {
  invalidFileError: "The original file '%0' does not appear to be a valid TiddlyWiki",
  mainSaved: "Main TiddlyWiki file uploaded",
  mainFailed: "Failed to upload main TiddlyWiki file. Your changes have not been saved",
  loadOriginalHttpPostError: "Can't get original file",
  aboutToSaveOnHttpPost: 'About to upload on %0 ...',
  storePhpNotFound: "The store script '%0' was not found."
};

bidix.thostUpload.uploadChanges = function(storeUrl) {
  var callback = function(status, uploadParams, original, url, xhr) {
    if (!status) {
      displayMessage(bidix.thostUpload.messages.loadOriginalHttpPostError);
      return;
    }
    if (bidix.debugMode) {
      alert(original.substr(0,500)+"\n...");
    }

    var posDiv = locateStoreArea(original);
    if ((posDiv[0] == -1) || (posDiv[1] == -1)) {
      alert(config.messages.invalidFileError.format([localPath]));
      return;
    }

    bidix.thostUpload.uploadMain(uploadParams, original, posDiv);
  };

  clearMessage();

  // get original
  var uploadParams = [storeUrl];
  var originalPath = document.location.toString();
  var dest = 'index.html';
  displayMessage(bidix.thostUpload.messages.aboutToSaveOnHttpPost.format([dest]));

  if (bidix.debugMode) {
    alert("about to execute Http - GET on "+originalPath);
  }

  var r = doHttp("GET", originalPath, null, null, null, null, callback, uploadParams, null);

  if (typeof r == "string") {
    displayMessage(r);
  }

  return r;
};

bidix.thostUpload.uploadMain = function(uploadParams, original, posDiv) {
  var callback = function(status, params, responseText, url, xhr) {
    if (status) {
      displayMessage(bidix.thostUpload.messages.mainSaved);
      store.setDirty(false);
    }
    else {
      alert(bidix.thostUpload.messages.mainFailed);
      displayMessage(bidix.thostUpload.messages.mainFailed);
    }
  };

  var revised = updateOriginal(original, posDiv);
  bidix.thostUpload.httpUpload(uploadParams, revised, callback, uploadParams);
};

bidix.thostUpload.httpUpload = function(uploadParams, data, callback, params) {
  var localCallback = function(status, params, responseText, url, xhr) {
    if (xhr.status == 404) {
      alert(bidix.thostUpload.messages.storePhpNotFound.format([url]));
    }

    var saveNotOk = responseText.charAt(0) != '0';

    if (bidix.debugMode || saveNotOk) {
      alert(responseText);
    }

    if (saveNotOk) {
      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=x" +
        ";user=x" +
        ";password=x" +
        ";uploaddir=x";
  if (bidix.debugMode) {
    sheader += ";debug=1";
  }
  sheader += ";;\r\n";
  sheader += "\r\n" + "--" + boundary + "\r\n";
  sheader += "Content-disposition: form-data; name=\"userfile\"; filename=\"index.html\"\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 = "";
  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, 'x','x', localCallback, params, null);

  if (typeof r == "string") {
    displayMessage(r);
  }

  return r;
};

// a fix for versions before 2.9.2 (updateOriginal used conversions irrelevant for Tiddlyhost)
convertUnicodeToFileFormat = function(s) { return s };

//
// Site config
//

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

merge(config.optionsDesc, {
  txtThostSiteName: "Site name for uploads to Tiddlyhost.com",
});

bidix.initOption('txtThostSiteName','hotkeys');

//
// Tiddlyhost stuff
//

bidix.ownerLoggedIn = (config.shadowTiddlers.TiddlyHostIsLoggedIn &&
  config.shadowTiddlers.TiddlyHostIsLoggedIn == "yes")

if (bidix.editModeAlways || bidix.ownerLoggedIn) {
  // If user is logged in to Tiddlyhost and viewing their own site then
  // we disregard the original value of the chkHttpReadOnly cookie
  config.options.chkHttpReadOnly = false
  // window.readOnly gets set before plugins are loaded, so we need to
  // set it here to make sure TW is editable, unlike window.showBackstage
  // which is set after
  window.readOnly = false
}

if (bidix.uploadButtonAlways || bidix.ownerLoggedIn) {
  // Add the 'save to tiddlyhost' button after the regular save button
  config.shadowTiddlers.SideBarOptions = config.shadowTiddlers.SideBarOptions
    .replace(/(<<saveChanges>>)/,"$1<<thostUpload>>");
}

//}}}
|Source |https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLExtensionsCollection.txt|
|Version|0.2.7|
//{{{
[
  {
    "url": "https://github.com/YakovL/TiddlyWiki_YL_ExtensionsIndex/blob/master/YLPrereleasesAndExperimentsCollection.txt",
    "description": "Pre-releases and experiments by Yakov Litvin",
    "type": "collection"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_DisqusPlugin/blob/master/DisqusPlugin.js",
    "description": "Add Disqus threads (comments) to your tiddlers"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_HandsontablePlugin/blob/master/HandsontablePlugin.js",
    "description": "Create spreadsheet-like tables with inline editing and keyboard hotkeys for manipulating rows and columns"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_EncryptionPlugin/blob/master/EncryptionPlugin.js",
    "description": "Save selected parts of content with encryption"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_TwFormulaPlugin/blob/master/TwFormulaPlugin.js",
    "description": "Render beautiful formulas using LaTeX syntax (also provides WYSIWYGish editing with MathQuill)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_GraphTools/blob/master/VisGraphPlugin.js",
    "description": "View and edit graphs in a WYSIWYGish manner with the <<graph>> macro (see additional installation steps inside the plugin)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_JumpKeysPlugin/blob/master/JumpKeysPlugin.js",
    "description": "Jump between tiddlers and do more via hotkeys and a UI similar to what browsers use for tabs"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_QuickSavePlugin/blob/master/QuickSavePlugin.js",
    "description": "Save changes via Ctrl + S hotkey (without getting browser default action)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ContinuousSavingPlugin/blob/main/ContinuousSavingPlugin.js",
    "description": "Get loading and saving work via just one file picking per session"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ShowUnsavedPlugin/blob/master/ShowUnsavedPlugin.js",
    "description": "See when TW has unsaved changes: in the tab/window title (adds '*'), on the saveChanges button (bold red), and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_DarkModePlugin/blob/master/DarkModePlugin.js",
    "description": "Introduces \"dark mode\" (styles) and switching it by the darkMode macro and operating system settings"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin/blob/master/TiddlerInFilePlugin.js",
    "description": "Store specific tiddlers as external files and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ExtraFilters/blob/master/ExtraFiltersPlugin.js",
    "description": "Various additional filters, including 'all', 'and', 'not', 'tagTree', 'unclassified', 'taggedOnly', 'hasPart', 'from', 'sortByText', and more"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_FromPlaceToPlacePlugin/blob/master/FromPlaceToPlacePlugin.js",
    "description": "Open a tiddler or a page in place of the current one (as opposed to opening in addition) via hotkeys"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_SimplifiedUpgradingPlugin/blob/master/SimplifiedUpgradingPlugin.js",
    "description": "Get core upgrading work with savers with I/O limitations, like Timimi, Tiddloid, or MTS 1.7.0 and above; optionally get notified on start if an upgrade is available; have upgrading more git-friendly"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ExtensionsExplorerPlugin/blob/master/ExtensionsExplorerPlugin.js",
    "description": "Get notified about extensions updates; explore, install and update extensions"
  },
  {
    "url": "https://yakovl.github.io/TiddlyWiki_SharedTiddlersPlugin/#SharedTiddlersPlugin",
    "description": "Load and use tiddlers from other TiddlyWikis (both content and settings, optionally with importing them)"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_ImageGalleries/blob/master/FancyBox2Plugin.js",
    "description": "Create image galleries"
  },
  {
    "url": "https://github.com/YakovL/TiddlyWiki_MarkdeepPlugin/blob/master/MarkdeepPlugin.js",
    "description": "Create diagrams using the Markdeep syntax"
  }
]
//}}}