Welcome to TiddlyWiki created by Jeremy Ruston; Copyright © 2004-2007 Jeremy Ruston, Copyright © 2007-2011 UnaMesa Association
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>
<!--}}}-->
When getting started, you may want to:
* Set your username for signing your edits: <<option txtUserName>>
* Change the page [[title|SiteTitle]] (now "<<tiddler SiteTitle>>") and [[subtitle|SiteSubtitle]] (now "<<tiddler SiteSubtitle>>"); they also set the browser tab title
* Create a tiddler where your content "starts"
** Use the button on the sidebar or [[link|My first tiddler]] it here, follow the link, edit, and click "done"
** It will be shown in the Timeline (usually on the right), but you may want to link it in the MainMenu (usually on the left)
** and/or make it open when the ~TiddlyWiki is opened by editing the list of [[DefaultTiddlers]] (separate links with spaces or linebreaks)
* Save your ~TiddlyWiki
** Although "download saving" works in any browser, it's not that convenient, so you'll probably want to use [[a dedicated saver|https://classic.tiddlywiki.com/#%5B%5BSetting up saving%5D%5D]]
<!--{{{-->
<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>
<span class='siteSubtitle' refresh='content' tiddler='SiteSubtitle'></span>
</div>
<div class='headerForeground'>
<span class='siteTitle' refresh='content' tiddler='SiteTitle'></span>
<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]];}
.txtOptionInput {background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.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::Foreground]];
background:[[ColorPalette::Background]];
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 {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]];}
.wizardFooter .status a { color: [[ColorPalette::PrimaryPale]]; }
.wizard .button {
color:[[ColorPalette::Foreground]]; background:[[ColorPalette::SecondaryLight]]; border: 1px solid;
border-color:[[ColorPalette::SecondaryDark]];
}
.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; }
.tw-icon line { stroke: [[ColorPalette::TertiaryDark]]; }
.messageToolbar__button:hover .tw-icon line { stroke: [[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: 2px solid [[ColorPalette::TertiaryPale]]; }
.selected .tagging, .selected .tagged { border: 2px 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]]; background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.editor textarea {border:1px solid [[ColorPalette::PrimaryMid]]; width:100%; background:[[ColorPalette::Background]]; color:[[ColorPalette::Foreground]];}
.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; border-width: 1px; }
#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 { display: inline-block; line-height: 1.5; 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__button_withIcon { display: inline-block; }
.tw-icon { height: 1em; width: 1em; } /* width for IE */
.tw-icon line { stroke-width: 1; stroke-linecap: round; }
.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 {display: inline-block; white-space: nowrap; position: relative; bottom: -0.7px; margin: 0 0.25em 0 0; padding:0.2em;}
.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; }
.title { font-size: 1.6em; font-weight: bold; }
.subtitle { font-size: 1.1em; }
.missing .viewer, .missing .title { font-style: italic; }
.missing .subtitle { display: none; }
.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.3em 0.5em; display: inline-block;}
#backstage {position:relative; width:100%; z-index:50;}
#backstagePanel { display:none; z-index:100; position:absolute; width:90%; margin:0 5%; }
.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>
<!--}}}-->
JumpKeysPlugin
ExtensionsExplorerPlugin
TiddlerInFilePlugin
/***
|Description|checks and reports updates of installed extensions on startup, introduces a macro/backstage button to explore, install and update extensions|
|Version |0.6.2|
|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: {
installButtonLabel: "install",
installButtonPrompt: "get and install this extension",
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 = /\/(\w+)\.(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+(\/\/}}})$/.exec(text)
if(match) try {
return JSON.parse(match[2])
} catch (e) {
console.log(`problems with parsing ${centralSourcesListName}:`, e)
return null
}
},
//# use getAvailableRepositories to get lists of extensions
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)
}
}
//# move name normalizing to the reading method
// once we move the list of available extensions from hardcode
for(const extension of availableExtensions) {
extension.name = extension.name || this.guessNameByUrl(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)
tiddler.text = responseText
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)
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: "explore extensions",
tooltip: "see if there's any updates or install new ones",
content: '<<extensionsExplorer>>',
}
},
handler: function(place, macroName, params, wikifier, paramString) {
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>')
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()
// 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')
for(const e of cells.actionElements)
actionsCell.appendChild(e)
$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
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?
$tbody.append(jQuery(`<tr><td colspan="4" style="text-align: center;">Installed</td></tr>`))
for(const extensionTiddler of installedExtensionsTiddlers) {
//# limit the width of the Description column/whole table
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 ? document.createTextNode(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[centralSourcesListName] = '//{{{\n' +
JSON.stringify(config.macros.extensionsExplorer.defaultAvailableExtensions, null, 2) +
'\n//}}}'
config.annotations[centralSourcesListName] =
config.macros.extensionsExplorer.lingo.centralSourcesListAnnotation
//}}}
<<external [[JumpKeysPlugin]] file:"JumpKeysPlugin.js" plugin:true>>
/***
|''Name''|InnerExternalLinkPlugin|
|''Version''|0.9.1|
|''Author''|Yakov Litvin|
***/
//{{{
config.extensions.InnerExternalLinkPlugin_orig_createExternalLink = createExternalLink;
createExternalLink = function(place,url,label) {
var permaLinkRegExp = /([^#]+)#(?:(?:([^%\[]+))|(?:\[\[([^\]]+)\]\])|(?:%5B%5B([^\]]+)%5D%5D))/,
// better to use decoding
match = permaLinkRegExp.exec(url),
siteUrlText = store.getTiddlerText("SiteUrl"),
pageLocation = window.location,
urlRegExp = /^(([^:\/\?#]+):)?(\/\/([^\/\?#]*))?([^\?#]*)(\?([^#]*))?(#(.*))?/,
matched = false;
if(match) {
var bareUrl = match[1],
tiddlerName = match[4]? match[4] : (match[3]? match[3] : match[2]);
tiddlerName = decodeURIComponent(tiddlerName);
} else
return config.extensions.InnerExternalLinkPlugin_orig_createExternalLink(place,url,label);
if(siteUrlText == bareUrl)
matched = true;
match = urlRegExp.exec(pageLocation.toString());
if(match[1]+match[3]+match[5] == bareUrl)
matched = true;
// may be additional normalizing (stripping the protocol etc, encoding/decoding) should be added
if(matched)
return createTiddlyLink(place, tiddlerName, label);
else
return config.extensions.InnerExternalLinkPlugin_orig_createExternalLink(place,url,label);
};
//}}}
/***
|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.6.0|
|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
if(config.macros.upgrade.source == 'http://tiddlywiki-releases.tiddlyspace.com/upgrade')
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])
}
}
}
// fix the bug introduced in 2.9.3 and fixed in 2.9.4 version
if(!isAbove2_9_3) {
// not present before 2.9.2
config.macros.upgrade.getSourceURL = function() {
return config.options.txtUpgradeCoreURI || config.macros.upgrade.source
}
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() {
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))
}
})
}
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]));
}
};
}
//}}}
to quickly navigate between tiddlers via keyboard and more
https://yakovl.github.io/TiddlyWiki_JumpKeysPlugin/
/***
|Description |Allows to store any number of tiddlers as external files and more|
|Source |https://github.com/YakovL/TiddlyWiki_TiddlerInFilePlugin/blob/master/TiddlerInFilePlugin.js|
|Author |Yakov Litvin|
|Version |1.1.3*|
|License |[[MIT|https://github.com/YakovL/TiddlyWiki_YL_ExtensionsCollection/blob/master/Common%20License%20(MIT)]]|
!!!Usage
Once the plugin is installed (copy - tag {{{systemConfig}}} - reload) storing tiddlers in files is done via 2 steps:
# list (describe) those in [[ExternalTiddlersList]] by writing {{{<<external>>}}} macros there
# if the file exists and the tiddler doesn't, reload TW (external tiddler will be loaded on startup);<br>if the tiddler exists and the file doesn't, just save your TW
Here's how the macro is used:
{{{
<<external [[MyTestTiddler]]>>
}}}
will store the tiddler's text in {{{MyTestTiddler.txt}}} in the same folder as TW. There's a number of other options:
{{{
<<external [[tiddler name]]
[file:<relative path with or without filename and extension>]
[format:{externalized | text}]
[keepInternal:true]
[plugin:true]
>>
}}}
Examples of the {{{file}}} param usage:
* {{{<<external [[MyTestTiddler]] file:"other name">>}}} makes file name different from tiddler name
* {{{<<external [[MyPlugin]] file:"MyPlugin.js" plugin:true>>}}} sets a custom file extension (see also the {{{plugin}}} option below)
* {{{<<external [[MyLog]] file:"../logs/">>}}} will store the file in another folder (note that omitted filename after {{{/}}} means "use tiddler name as filename and default extension")
* the plugin doesn't take care of forbidden characters yet ({{{*}}}, {{{?}}}, {{{"}}} etc), so be careful about those
Supported formats are
* {{{text}}} (default) – only tiddler text is stored in the file with {{{.txt}}} extension; and
* {{{externalized}}} – whole tiddler is stored in the same format as in TW store, file has {{{.tid.html}}} extension and is displayed is monospace tiddler text when opened in browser
Formats can be added by extending {{{config.macros.external.fileFormats}}}.
The {{{keepInternal}}} option makes TW save the tiddler in both external file and TW itself. This, for instance, affects tiddlers stored in {{{text}}} format: without it, all fields like creator, modified etc are destroyed on reload (since are not saved), but with it they are preserved.
The {{{plugin}}} option makes TW evaluate the tiddler like it does with plugins. Note that it doesn't handle plugin dependencies yet. @@color:red;Warning@@: TIFP doesn't currently take care of installing only once, so use {{{plugin}}} with {{{keepInternal}}} ''only'' if you understand the outcome (for instance, some plugins may create infinite loops; also remember that internal version will be always installed first).
***/
//{{{
config.macros.external = {
fileFormats: {
text: {
extension: 'txt',
externalize: function(tiddler) { return tiddler.text },
// changes tiddler as a side-effect not to remove existing fields
internalize: function(tiddler, source) {
tiddler.text = source
}
},
externalized: {
extension: 'tid.html',
externalize: function(tiddler) {
return store.getSaver().externalizeTiddler(store, tiddler)
},
// like for 'text', extends tiddler, doesn't create from scratch
internalize: function(tiddler, source) {
var div = createTiddlyElement(document.body, 'div')
div.setAttribute('style','display:none;')
div.innerHTML = source
store.getLoader().internalizeTiddler(store, tiddler,
tiddler.title, div.firstChild)
div.remove()
}
}/*,
tid: { extension: 'tid' },
json: { extension: 'tid.json' }
externalizedWithFormatter? sane to implement?
*/
},
// here and below "meta" means "info about registered external tiddler,
// be it loaded or not"
getExtension: function(meta) {
const format = this.fileFormats[meta.fileFormat]
if(!format) return //# ok??
return format.extension
},
externalizeTiddler: function(meta) {
const format = this.fileFormats[meta.fileFormat]
if(!format) return //# ok??
return format.externalize(meta.tiddler)
},
internalizeTiddler: function(meta, source) {
const format = this.fileFormats[meta.fileFormat]
if(!format) return //# ok??
const tiddler = store.fetchTiddler(meta.tiddlerName) ||
new Tiddler(meta.tiddlerName)
format.internalize(tiddler, source) //# pass meta to tiddler?
tiddler.doNotSave = function() { return !meta.keepInTW }
meta.tiddler = tiddler
return tiddler
},
listName: "ExternalTiddlersList",
// read files list, load
init: function() {
const listTiddler = store.fetchTiddler(this.listName)
if(!listTiddler || !listTiddler.text) return
wikify(listTiddler.text, createTiddlyElement(null, 'div'))
for(let meta of this.tiddlersMeta) this.loadExternal(meta)
},
handler: function(place, macroName, params, wikifier, paramString, tiddler) {
// parse params, register
const defaultParam = 'tiddler'
const pParams = paramString.parseParams(defaultParam, null, true)
const meta = {}
meta.tiddlerName = getParam(pParams, defaultParam)
if(!meta.tiddlerName) return
// although called .fileName, it actually can contain relative part of a path
// fallback to meta.tiddlerName is set when calculating the full path
meta.fileName = getParam(pParams, 'file', '')
//# check if contains "bad" characters (like " or * ..for local paths only)
meta.fileFormat = getParam(pParams, 'format', 'text')
meta.isPlugin = getFlag(pParams, 'plugin') //# allow just "plugin" instead of "plugin:true"?
const keepInternal = getParam(pParams, 'keepInternal')
meta.keepInTW = !!keepInternal && keepInternal !== 'false' //# ~
this.registerExternal(meta)
// visual feedback
const macroText = wikifier.source.substring(wikifier.matchStart, wikifier.nextMatch)
createTiddlyText(place, 'external ')
createTiddlyLink(place, meta.tiddlerName, true)
createTiddlyText(place, ' (')
createTiddlyElement(place, 'code', '', '', macroText.slice(2 + macroName.length + 1, -2))
createTiddlyText(place, ')')
},
// describes tiddlers registered as external, not necessarily loaded
tiddlersMeta: [],
registerExternal: function(meta) {
//# check if already registered, don't register twice
this.tiddlersMeta.push(meta)
},
getMetaFor: function(tiddlerOrTitle) {
var isTitle = typeof tiddlerOrTitle == "string"
for(meta of this.tiddlersMeta)
if(isTitle && meta.tiddlerName == tiddlerOrTitle ||
!isTitle && meta.tiddler == tiddlerOrTitle)
return meta
},
loadExternal: function(meta) {
// sync loading fails on startup because TF injects new mozillaLoadFile too late
// var tiddlerText = loadFile(getLocalPath(getFullPath(meta.fileName)));
// onExternalTiddlerLoad(tiddlerText !== null, meta, tiddlerText);
// so we use async instead:
const callback = this.onExternalTiddlerLoad
const path = getFullPath(meta.fileName, meta.tiddlerName, this.getExtension(meta))
// httpReq("GET", path, callback, meta) uses default dataType,
// which causes js to get evaluated on load. To avoid this, we customize the ajax call:
jQuery.ajax({
type: "GET",
url: path,
dataType: "text",
processData: false,
cache: false,
complete: function(xhr, textStatus) {
if((!xhr.status && location.protocol === "file:") || (xhr.status >= 200 && xhr.status < 300) || xhr.status === 304)
callback(true, meta, xhr.responseText, path, xhr)
else
callback(false, meta, null, path, xhr)
}
})
//# rename onExternalTiddlerLoad into internalizeAndRegister?
},
onExternalTiddlerLoad: function(success, meta, responseText) {
if(!success) return //# notify somehow? may fail because file is not created yet or ...
const tiddler = config.macros.external.internalizeTiddler(meta, responseText)
store.addTiddler(tiddler)
//# what if tiddler already exists?
if(meta.isPlugin) {
// make it look normally
tiddler.tags.pushUnique('systemConfig')
const author = store.getTiddlerText(tiddler.title + "::Author")
if(author) {
tiddler.creator = tiddler.creator || author
tiddler.modifier = tiddler.modifier || tiddler.creator
}
eval(tiddler.text)
// for plugins introducing macros, formatters etc (may be adjusted in the future)
story.refreshAllTiddlers()
refreshAll()
// apply CSS
store.notifyAll()
}
//meta.lastLoaded = responseText
},
saveExternal: function(meta, callback) {
const fullPath = getFullPath(meta.fileName, meta.tiddlerName, this.getExtension(meta))
// we don't try to save remote files (yet)
if(!isLocalAbsolutePath(fullPath)) {
//# if(callback) callback(.., '[saving remote is not supported]')
return
}
const localPath = getLocalPath(fullPath)
const contentToSave = this.externalizeTiddler(meta)
// save only if have something to save
//if(contentToSave != meta.lastLoaded) {
saveFile(localPath, contentToSave)
//# get result of saving, return it
//# or move externalizing into a separate helper?
// meta.lastLoaded = contentToSave
//# this assumes saving didn't fail, which may be wrong
//}
//# if(callback) callback(.., '[...]')
},
saveAll: function() {
let overallSuccess = true
for(let meta of this.tiddlersMeta) {
// a tiddler may got created after registration
if(!meta.tiddler) {
let tiddlerInStore = store.fetchTiddler(meta.tiddlerName)
if(tiddlerInStore) {
meta.tiddler = tiddlerInStore
} else
// tiddler doesn't exist and we do nothing
continue
//# based on config, we can show a warning instead
}
//# if(meta.tiddler.title != meta.tiddlerName)
// means tiddler got renamed → change meta.tiddlerName &
// update this.listName . If store contains another tiddler
// with that name, still keep the registered one?
overallSuccess = this.saveExternal(meta) && overallSuccess
//# if saving failed, do something! (.oO dirty, notifying)
}
return overallSuccess
}
}
//# see implementations in STP (share to the core?)
function isAbsolutePath(path) {
// covers http:, https:, file:, other schemas, windows paths (D:\...)
if(/^\w+\:/.exec(path)) return true
// unix absolute paths, starting with /
if(/^\//.exec(path)) return true
return false
}
function isLocalAbsolutePath(path) {
//# rename? we're going to check whether an absolute path is local, not path is absolute local
return /^\w\:/.exec(path) || /^\//.exec(path) || /^file\:/.exec(path)
}
function getFullPath(subPath, nameFallback, extension) {
const fileNamePosition = subPath.lastIndexOf('/') + 1
const fileName = subPath.substr(fileNamePosition)
if(fileName && fileName.indexOf('.') == -1)
subPath += '.' + extension
if(!fileName)
subPath += nameFallback + '.' + extension
if(isAbsolutePath(subPath)) return subPath
const url = window.location.toString()
const base = url.substr(0, url.lastIndexOf('/') + 1)
return base + subPath
}
//# ideally, don't save main store if it were not changed
if(!config.macros.external.orig_saveChanges) {
config.macros.external.orig_saveChanges = saveChanges
saveChanges = function(onlyIfDirty, tiddlers) {
config.macros.external.saveAll()
//# should we do smth about setDirty (if saving of a tiddler failed)?
return config.macros.external.orig_saveChanges.apply(this, arguments)
}
}
// hijack method of store (since not present in TiddlyWiki.prototype)
if(!config.macros.external.orig_deleteTiddler) {
config.macros.external.orig_deleteTiddler = store.deleteTiddler
store.deleteTiddler = function(title) {
var registeredMeta = config.macros.external.getMetaFor(title)
if(registeredMeta) registeredMeta.tiddler = null
return config.macros.external.orig_deleteTiddler.apply(this, arguments)
}
}
//}}}
|~ViewToolbar|closeTiddler closeOthers +editTiddler jump permalink > fields references deleteTiddler|
|~EditToolbar|+saveTiddler -cancelTiddler references deleteTiddler|