mirror of
https://github.com/Jermolene/TiddlyWiki5
synced 2026-07-15 16:22:44 +00:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b92b17bb67 | |||
| 4bf7e9d192 | |||
| 04cd3081e4 | |||
| d27716c1e0 | |||
| 7f7f36d986 | |||
| 86a4e18134 | |||
| 304858c7c5 | |||
| 7ececf9e0f | |||
| 521e530e11 | |||
| 4c29dae4af | |||
| f5317dc225 | |||
| c952450c2e | |||
| 1eb7ec4402 | |||
| 869557f7d1 | |||
| c9f1154643 | |||
| 748f04c9e3 | |||
| d391595836 | |||
| 967140a148 | |||
| 95e68b0437 | |||
| 8ddede1611 | |||
| ceb200b08c | |||
| 00e5f48a59 | |||
| ea0e9105bc | |||
| 51459815ba | |||
| 8c62935a01 | |||
| 839fa2417d | |||
| 1b8610e4d8 | |||
| 7a80bd4a6f | |||
| 82bde9bffc | |||
| 9830d4338c | |||
| 75267b730a | |||
| 3bc78e6641 | |||
| 765386a4f9 | |||
| ca8dcb690a | |||
| 99a00ab1b4 | |||
| f95966d5bb | |||
| 3a20837c96 | |||
| e3e49bb61e | |||
| 748969322b | |||
| 4611e3569f | |||
| c1e145ca12 | |||
| 882504c8d1 | |||
| b8f542458b |
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
# Default to the current version number for building the plugin library
|
# Default to the current version number for building the plugin library
|
||||||
|
|
||||||
if [ -z "$TW5_BUILD_VERSION" ]; then
|
if [ -z "$TW5_BUILD_VERSION" ]; then
|
||||||
TW5_BUILD_VERSION=v5.4.1.
|
TW5_BUILD_VERSION=v5.5.0.
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
|
echo "Using TW5_BUILD_VERSION as [$TW5_BUILD_VERSION]"
|
||||||
|
|||||||
+60
-8
@@ -1539,8 +1539,8 @@ Register all the module tiddlers that have a module type
|
|||||||
$tw.Wiki.prototype.defineShadowModules = function() {
|
$tw.Wiki.prototype.defineShadowModules = function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.eachShadow(function(tiddler,title) {
|
this.eachShadow(function(tiddler,title) {
|
||||||
// Don't define the module if it is overidden by an ordinary tiddler
|
// Don't define the module if it is overidden by an ordinary tiddler, or has already been defined
|
||||||
if(!self.tiddlerExists(title) && tiddler.hasField("module-type")) {
|
if(!self.tiddlerExists(title) && tiddler.hasField("module-type") && !$tw.utils.hop($tw.modules.titles,title)) {
|
||||||
if(tiddler.hasField("draft.of")) {
|
if(tiddler.hasField("draft.of")) {
|
||||||
// Report a fundamental problem
|
// Report a fundamental problem
|
||||||
console.warn(`TiddlyWiki: Plugins should not contain tiddlers with a 'draft.of' field: ${tiddler.fields.title}`);
|
console.warn(`TiddlyWiki: Plugins should not contain tiddlers with a 'draft.of' field: ${tiddler.fields.title}`);
|
||||||
@@ -1971,7 +1971,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper to process a file
|
// Helper to process a file
|
||||||
var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) {
|
var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath,dynamicStoreId) {
|
||||||
var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)],
|
var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)],
|
||||||
type = (extInfo || {}).type || fields.type || "text/plain",
|
type = (extInfo || {}).type || fields.type || "text/plain",
|
||||||
typeInfo = $tw.config.contentTypeInfo[type] || {},
|
typeInfo = $tw.config.contentTypeInfo[type] || {},
|
||||||
@@ -2046,9 +2046,9 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
if(isEditableFile) {
|
if(isEditableFile) {
|
||||||
tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, tiddlers: fileTiddlers});
|
tiddlers.push({filepath: pathname, hasMetaFile: !!metadata && !isTiddlerFile, isEditableFile: true, dynamicStoreId: dynamicStoreId, tiddlers: fileTiddlers});
|
||||||
} else {
|
} else {
|
||||||
tiddlers.push({tiddlers: fileTiddlers});
|
tiddlers.push({dynamicStoreId: dynamicStoreId, tiddlers: fileTiddlers});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Helper to recursively search subdirectories
|
// Helper to recursively search subdirectories
|
||||||
@@ -2089,6 +2089,31 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
|
|||||||
// Process directory specifier
|
// Process directory specifier
|
||||||
var dirPath = path.resolve(filepath,dirSpec.path);
|
var dirPath = path.resolve(filepath,dirSpec.path);
|
||||||
if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
if(fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory()) {
|
||||||
|
// Register a dynamic store if requested
|
||||||
|
var dynamicStoreId = null;
|
||||||
|
if(dirSpec.dynamicStore && $tw.boot.dynamicStores) {
|
||||||
|
dynamicStoreId = dirPath;
|
||||||
|
var existing = null;
|
||||||
|
for(var ds=0; ds<$tw.boot.dynamicStores.length; ds++) {
|
||||||
|
if($tw.boot.dynamicStores[ds].id === dynamicStoreId) {
|
||||||
|
existing = $tw.boot.dynamicStores[ds];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!existing) {
|
||||||
|
$tw.boot.dynamicStores.push({
|
||||||
|
id: dynamicStoreId,
|
||||||
|
directory: dirPath,
|
||||||
|
saveFilter: dirSpec.dynamicStore.saveFilter || "",
|
||||||
|
watch: dirSpec.dynamicStore.watch !== false,
|
||||||
|
debounce: dirSpec.dynamicStore.debounce || 400,
|
||||||
|
filesRegExp: dirSpec.filesRegExp || "^.*$",
|
||||||
|
searchSubdirectories: !!dirSpec.searchSubdirectories,
|
||||||
|
isTiddlerFile: !!dirSpec.isTiddlerFile,
|
||||||
|
fields: dirSpec.fields || {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
var files = getAllFiles(dirPath, dirSpec.searchSubdirectories),
|
var files = getAllFiles(dirPath, dirSpec.searchSubdirectories),
|
||||||
fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"),
|
fileRegExp = new RegExp(dirSpec.filesRegExp || "^.*$"),
|
||||||
metaRegExp = /^.*\.meta$/;
|
metaRegExp = /^.*\.meta$/;
|
||||||
@@ -2097,7 +2122,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) {
|
|||||||
filename = path.basename(thisPath);
|
filename = path.basename(thisPath);
|
||||||
if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) {
|
if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) {
|
||||||
dirSpec.fields = dirSpec.fields || {};
|
dirSpec.fields = dirSpec.fields || {};
|
||||||
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path);
|
processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile || !!dirSpec.dynamicStore,dirSpec.path,dynamicStoreId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2284,6 +2309,24 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
|
|||||||
$tw.loadPlugins(wikiInfo.plugins,$tw.config.pluginsPath,$tw.config.pluginsEnvVar);
|
$tw.loadPlugins(wikiInfo.plugins,$tw.config.pluginsPath,$tw.config.pluginsEnvVar);
|
||||||
$tw.loadPlugins(wikiInfo.themes,$tw.config.themesPath,$tw.config.themesEnvVar);
|
$tw.loadPlugins(wikiInfo.themes,$tw.config.themesPath,$tw.config.themesEnvVar);
|
||||||
$tw.loadPlugins(wikiInfo.languages,$tw.config.languagesPath,$tw.config.languagesEnvVar);
|
$tw.loadPlugins(wikiInfo.languages,$tw.config.languagesPath,$tw.config.languagesEnvVar);
|
||||||
|
// Register plugin-provided tiddlerdeserializer and tiddlerserializer modules now,
|
||||||
|
// so they are available when the wiki tiddler files are read from disk below.
|
||||||
|
// We also apply the supporting `utils`, `tiddlerfield`, and `tiddlermethod`
|
||||||
|
// modules so deserializers can call into them (e.g. core's text/html
|
||||||
|
// deserializer needs $tw.utils.extractEncryptedStoreArea).
|
||||||
|
// (All of these steps run again later in execStartup; they are idempotent.)
|
||||||
|
$tw.wiki.readPluginInfo();
|
||||||
|
$tw.wiki.registerPluginTiddlers("plugin");
|
||||||
|
$tw.wiki.unpackPluginTiddlers();
|
||||||
|
$tw.wiki.defineShadowModules();
|
||||||
|
$tw.modules.applyMethods("utils",$tw.utils);
|
||||||
|
if($tw.node) {
|
||||||
|
$tw.modules.applyMethods("utils-node",$tw.utils);
|
||||||
|
}
|
||||||
|
$tw.Tiddler.fieldModules = $tw.modules.getModulesByTypeAsHashmap("tiddlerfield");
|
||||||
|
$tw.modules.applyMethods("tiddlermethod",$tw.Tiddler.prototype);
|
||||||
|
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
||||||
|
$tw.modules.applyMethods("tiddlerserializer",$tw.Wiki.tiddlerSerializerModules);
|
||||||
// Load the wiki files, registering them as writable
|
// Load the wiki files, registering them as writable
|
||||||
var resolvedWikiPath = path.resolve(wikiPath,$tw.config.wikiTiddlersSubDir);
|
var resolvedWikiPath = path.resolve(wikiPath,$tw.config.wikiTiddlersSubDir);
|
||||||
$tw.utils.each($tw.loadTiddlersFromPath(resolvedWikiPath),function(tiddlerFile) {
|
$tw.utils.each($tw.loadTiddlersFromPath(resolvedWikiPath),function(tiddlerFile) {
|
||||||
@@ -2293,7 +2336,8 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
|
|||||||
filepath: tiddlerFile.filepath,
|
filepath: tiddlerFile.filepath,
|
||||||
type: tiddlerFile.type,
|
type: tiddlerFile.type,
|
||||||
hasMetaFile: tiddlerFile.hasMetaFile,
|
hasMetaFile: tiddlerFile.hasMetaFile,
|
||||||
isEditableFile: config["retain-original-tiddler-path"] || tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0
|
isEditableFile: config["retain-original-tiddler-path"] || tiddlerFile.isEditableFile || tiddlerFile.filepath.indexOf($tw.boot.wikiTiddlersPath) !== 0,
|
||||||
|
dynamicStoreId: tiddlerFile.dynamicStoreId || null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -2305,7 +2349,10 @@ $tw.loadWikiTiddlers = function(wikiPath,options) {
|
|||||||
for(var title in $tw.boot.files) {
|
for(var title in $tw.boot.files) {
|
||||||
fileInfo = $tw.boot.files[title];
|
fileInfo = $tw.boot.files[title];
|
||||||
if(fileInfo.isEditableFile) {
|
if(fileInfo.isEditableFile) {
|
||||||
relativePath = path.relative($tw.boot.wikiTiddlersPath,fileInfo.filepath);
|
// For tiddlers loaded from a dynamic store, compute originalpath relative to the store's directory
|
||||||
|
// so that save-time path resolution against that directory yields the correct location.
|
||||||
|
var basePath = fileInfo.dynamicStoreId || $tw.boot.wikiTiddlersPath;
|
||||||
|
relativePath = path.relative(basePath,fileInfo.filepath);
|
||||||
fileInfo.originalpath = relativePath;
|
fileInfo.originalpath = relativePath;
|
||||||
output[title] =
|
output[title] =
|
||||||
path.sep === "/" ?
|
path.sep === "/" ?
|
||||||
@@ -2431,6 +2478,8 @@ $tw.boot.initStartup = function(options) {
|
|||||||
if(!$tw.boot.tasks.readBrowserTiddlers) {
|
if(!$tw.boot.tasks.readBrowserTiddlers) {
|
||||||
// For writable tiddler files, a hashmap of title to {filepath:,type:,hasMetaFile:}
|
// For writable tiddler files, a hashmap of title to {filepath:,type:,hasMetaFile:}
|
||||||
$tw.boot.files = Object.create(null);
|
$tw.boot.files = Object.create(null);
|
||||||
|
// Array of {id, directory, saveFilter, watch, debounce} registered via tiddlywiki.files dynamicStore directives
|
||||||
|
$tw.boot.dynamicStores = [];
|
||||||
// System paths and filenames
|
// System paths and filenames
|
||||||
$tw.boot.bootPath = options.bootPath || path.dirname(module.filename);
|
$tw.boot.bootPath = options.bootPath || path.dirname(module.filename);
|
||||||
$tw.boot.corePath = path.resolve($tw.boot.bootPath,"../core");
|
$tw.boot.corePath = path.resolve($tw.boot.bootPath,"../core");
|
||||||
@@ -2520,6 +2569,9 @@ $tw.boot.initStartup = function(options) {
|
|||||||
// Install the tiddler deserializer modules
|
// Install the tiddler deserializer modules
|
||||||
$tw.Wiki.tiddlerDeserializerModules = Object.create(null);
|
$tw.Wiki.tiddlerDeserializerModules = Object.create(null);
|
||||||
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
||||||
|
// Install the tiddler serializer modules
|
||||||
|
$tw.Wiki.tiddlerSerializerModules = Object.create(null);
|
||||||
|
$tw.modules.applyMethods("tiddlerserializer",$tw.Wiki.tiddlerSerializerModules);
|
||||||
// Call unload handlers in the browser
|
// Call unload handlers in the browser
|
||||||
if($tw.browser) {
|
if($tw.browser) {
|
||||||
window.onbeforeunload = function(event) {
|
window.onbeforeunload = function(event) {
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -238,6 +238,10 @@ exports.generateTiddlerFileInfo = function(tiddler,options) {
|
|||||||
// Save as a .tid file
|
// Save as a .tid file
|
||||||
fileInfo.type = "application/x-tiddler";
|
fileInfo.type = "application/x-tiddler";
|
||||||
fileInfo.hasMetaFile = false;
|
fileInfo.hasMetaFile = false;
|
||||||
|
} else if($tw.Wiki.tiddlerSerializerModules && $tw.Wiki.tiddlerSerializerModules[tiddlerType]) {
|
||||||
|
// A serializer is registered for this content type - save as a single self-contained file
|
||||||
|
fileInfo.type = tiddlerType;
|
||||||
|
fileInfo.hasMetaFile = false;
|
||||||
} else {
|
} else {
|
||||||
// Save as a text/binary file and a .meta file
|
// Save as a text/binary file and a .meta file
|
||||||
fileInfo.type = tiddlerType;
|
fileInfo.type = tiddlerType;
|
||||||
@@ -416,7 +420,16 @@ Save a tiddler to a file described by the fileInfo:
|
|||||||
*/
|
*/
|
||||||
exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
|
exports.saveTiddlerToFile = function(tiddler,fileInfo,callback) {
|
||||||
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));
|
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));
|
||||||
if(fileInfo.hasMetaFile) {
|
var serializer = $tw.Wiki.tiddlerSerializerModules && $tw.Wiki.tiddlerSerializerModules[fileInfo.type];
|
||||||
|
if(serializer && !fileInfo.hasMetaFile && fileInfo.type !== "application/x-tiddler" && fileInfo.type !== "application/json") {
|
||||||
|
var typeInfo = $tw.config.contentTypeInfo[fileInfo.type] || {encoding: "utf8"};
|
||||||
|
fs.writeFile(fileInfo.filepath,serializer(tiddler),typeInfo.encoding,function(err) {
|
||||||
|
if(err) {
|
||||||
|
return callback(err);
|
||||||
|
}
|
||||||
|
return callback(null,fileInfo);
|
||||||
|
});
|
||||||
|
} else if(fileInfo.hasMetaFile) {
|
||||||
// Save the tiddler as a separate body and meta file
|
// Save the tiddler as a separate body and meta file
|
||||||
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
|
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
|
||||||
fs.writeFile(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding,function(err) {
|
fs.writeFile(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding,function(err) {
|
||||||
@@ -458,7 +471,11 @@ Save a tiddler to a file described by the fileInfo:
|
|||||||
*/
|
*/
|
||||||
exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
|
exports.saveTiddlerToFileSync = function(tiddler,fileInfo) {
|
||||||
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));
|
$tw.utils.createDirectory(path.dirname(fileInfo.filepath));
|
||||||
if(fileInfo.hasMetaFile) {
|
var serializer = $tw.Wiki.tiddlerSerializerModules && $tw.Wiki.tiddlerSerializerModules[fileInfo.type];
|
||||||
|
if(serializer && !fileInfo.hasMetaFile && fileInfo.type !== "application/x-tiddler" && fileInfo.type !== "application/json") {
|
||||||
|
var typeInfo = $tw.config.contentTypeInfo[fileInfo.type] || {encoding: "utf8"};
|
||||||
|
fs.writeFileSync(fileInfo.filepath,serializer(tiddler),typeInfo.encoding);
|
||||||
|
} else if(fileInfo.hasMetaFile) {
|
||||||
// Save the tiddler as a separate body and meta file
|
// Save the tiddler as a separate body and meta file
|
||||||
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
|
var typeInfo = $tw.config.contentTypeInfo[tiddler.fields.type || "text/plain"] || {encoding: "utf8"};
|
||||||
fs.writeFileSync(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding);
|
fs.writeFileSync(fileInfo.filepath,tiddler.fields.text || "",typeInfo.encoding);
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ Serve tiddlers over http
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
let fs, url, path, querystring, crypto, zlib;
|
let fs, path, crypto, zlib, URL;
|
||||||
|
|
||||||
if($tw.node) {
|
if($tw.node) {
|
||||||
fs = require("fs"),
|
fs = require("fs"),
|
||||||
url = require("url"),
|
|
||||||
path = require("path"),
|
path = require("path"),
|
||||||
querystring = require("querystring"),
|
|
||||||
crypto = require("crypto"),
|
crypto = require("crypto"),
|
||||||
zlib = require("zlib");
|
zlib = require("zlib");
|
||||||
|
URL = require("url").URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -260,8 +259,8 @@ Server.prototype.requestHandler = function(request,response,options) {
|
|||||||
state.wiki = options.wiki || self.wiki;
|
state.wiki = options.wiki || self.wiki;
|
||||||
state.boot = options.boot || self.boot;
|
state.boot = options.boot || self.boot;
|
||||||
state.server = self;
|
state.server = self;
|
||||||
state.urlInfo = url.parse(request.url);
|
state.urlInfo = new URL(request.url, "http://localhost");
|
||||||
state.queryParameters = querystring.parse(state.urlInfo.query);
|
state.queryParameters = Object.fromEntries(state.urlInfo.searchParams);
|
||||||
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
|
state.pathPrefix = options.pathPrefix || this.get("path-prefix") || "";
|
||||||
// Enable CORS
|
// Enable CORS
|
||||||
if(this.corsEnable) {
|
if(this.corsEnable) {
|
||||||
|
|||||||
@@ -255,4 +255,5 @@ ViewTemplateTags/Caption: View Template Tags
|
|||||||
ViewTemplateTags/Hint: This rule cascade is used by the default view template to dynamically choose the template for displaying the tags area of a tiddler.
|
ViewTemplateTags/Hint: This rule cascade is used by the default view template to dynamically choose the template for displaying the tags area of a tiddler.
|
||||||
WikiInformation/Caption: Wiki Information
|
WikiInformation/Caption: Wiki Information
|
||||||
WikiInformation/Hint: This page summarises high level information about the configuration of this ~TiddlyWiki. It is designed to enable users to quickly share relevant aspects of the configuration of their ~TiddlyWiki with others, for example when seeking help in one of the forums. No private or personal information is included, and nothing is shared without being explicitly copied and pasted elsewhere
|
WikiInformation/Hint: This page summarises high level information about the configuration of this ~TiddlyWiki. It is designed to enable users to quickly share relevant aspects of the configuration of their ~TiddlyWiki with others, for example when seeking help in one of the forums. No private or personal information is included, and nothing is shared without being explicitly copied and pasted elsewhere
|
||||||
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
|
WikiInformation/Drag/Caption: Drag this link to copy this tool to another wiki
|
||||||
|
WikiInformation/Generate/Caption: Click to generate wiki information report
|
||||||
@@ -22,12 +22,11 @@ exports.tag = function(source,operator,options) {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Old semantics:
|
// Old semantics:
|
||||||
var tiddlers;
|
|
||||||
if(operator.prefix === "!") {
|
if(operator.prefix === "!") {
|
||||||
// Returns a copy of the input if operator.operand is missing
|
// Returns a copy of the input if operator.operand is missing
|
||||||
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
|
const excludeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
|
||||||
source(function(tiddler,title) {
|
source(function(tiddler,title) {
|
||||||
if(tiddlers.indexOf(title) === -1) {
|
if(!excludeTagSet.has(title)) {
|
||||||
results.push(title);
|
results.push(title);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -39,9 +38,9 @@ exports.tag = function(source,operator,options) {
|
|||||||
return indexedResults;
|
return indexedResults;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tiddlers = options.wiki.getTiddlersWithTag(operator.operand);
|
const includeTagSet = new Set(options.wiki.getTiddlersWithTag(operator.operand));
|
||||||
source(function(tiddler,title) {
|
source(function(tiddler,title) {
|
||||||
if(tiddlers.indexOf(title) !== -1) {
|
if(includeTagSet.has(title)) {
|
||||||
results.push(title);
|
results.push(title);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,10 +88,11 @@ BackSubIndexer.prototype.update = function(updateDescriptor) {
|
|||||||
var newTargets = [],
|
var newTargets = [],
|
||||||
oldTargets = [],
|
oldTargets = [],
|
||||||
self = this;
|
self = this;
|
||||||
if(updateDescriptor.old.exists) {
|
// System tiddlers are never indexed as sources, matching the _init() scan
|
||||||
|
if(updateDescriptor.old.exists && !this.wiki.isSystemTiddler(updateDescriptor.old.tiddler.fields.title)) {
|
||||||
oldTargets = this._getTarget(updateDescriptor.old.tiddler);
|
oldTargets = this._getTarget(updateDescriptor.old.tiddler);
|
||||||
}
|
}
|
||||||
if(updateDescriptor.new.exists) {
|
if(updateDescriptor.new.exists && !this.wiki.isSystemTiddler(updateDescriptor.new.tiddler.fields.title)) {
|
||||||
newTargets = this._getTarget(updateDescriptor.new.tiddler);
|
newTargets = this._getTarget(updateDescriptor.new.tiddler);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -528,67 +528,79 @@ exports.parseAttribute = function(source,pos) {
|
|||||||
pos = token.end;
|
pos = token.end;
|
||||||
// Skip whitespace
|
// Skip whitespace
|
||||||
pos = $tw.utils.skipWhiteSpace(source,pos);
|
pos = $tw.utils.skipWhiteSpace(source,pos);
|
||||||
// Look for a string literal
|
do {
|
||||||
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
|
// Look for a string literal
|
||||||
if(stringLiteral) {
|
var stringLiteral = $tw.utils.parseStringLiteral(source,pos);
|
||||||
pos = stringLiteral.end;
|
if(stringLiteral) {
|
||||||
node.type = "string";
|
pos = stringLiteral.end;
|
||||||
node.value = stringLiteral.value;
|
node.type = "string";
|
||||||
} else {
|
node.value = stringLiteral.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Look for a filtered value
|
// Look for a filtered value
|
||||||
var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
|
var filteredValue = $tw.utils.parseTokenRegExp(source,pos,reFilteredValue);
|
||||||
if(filteredValue) {
|
if(filteredValue) {
|
||||||
pos = filteredValue.end;
|
pos = filteredValue.end;
|
||||||
node.type = "filtered";
|
node.type = "filtered";
|
||||||
node.filter = filteredValue.match[1];
|
node.filter = filteredValue.match[1];
|
||||||
} else {
|
break;
|
||||||
// Look for an indirect value
|
|
||||||
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
|
|
||||||
if(indirectValue) {
|
|
||||||
pos = indirectValue.end;
|
|
||||||
node.type = "indirect";
|
|
||||||
node.textReference = indirectValue.match[1];
|
|
||||||
} else {
|
|
||||||
// Look for a macro invocation value
|
|
||||||
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
|
|
||||||
if(macroInvocation) {
|
|
||||||
pos = macroInvocation.end;
|
|
||||||
node.type = "macro";
|
|
||||||
node.value = macroInvocation;
|
|
||||||
} else {
|
|
||||||
// Look for an MVV reference value
|
|
||||||
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
|
|
||||||
if(mvvReference) {
|
|
||||||
pos = mvvReference.end;
|
|
||||||
node.type = "macro";
|
|
||||||
node.value = mvvReference;
|
|
||||||
node.isMVV = true;
|
|
||||||
} else {
|
|
||||||
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
|
|
||||||
if(substitutedValue) {
|
|
||||||
pos = substitutedValue.end;
|
|
||||||
node.type = "substituted";
|
|
||||||
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
|
|
||||||
} else {
|
|
||||||
// Look for a unquoted value
|
|
||||||
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
|
|
||||||
if(unquotedValue) {
|
|
||||||
pos = unquotedValue.end;
|
|
||||||
node.type = "string";
|
|
||||||
node.value = unquotedValue.match[1];
|
|
||||||
} else if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
|
|
||||||
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
node.type = "string";
|
|
||||||
node.value = "true";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// Look for an indirect value
|
||||||
|
var indirectValue = $tw.utils.parseTokenRegExp(source,pos,reIndirectValue);
|
||||||
|
if(indirectValue) {
|
||||||
|
pos = indirectValue.end;
|
||||||
|
node.type = "indirect";
|
||||||
|
node.textReference = indirectValue.match[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a macro invocation value
|
||||||
|
var macroInvocation = $tw.utils.parseMacroInvocationAsTransclusion(source,pos);
|
||||||
|
if(macroInvocation) {
|
||||||
|
pos = macroInvocation.end;
|
||||||
|
node.type = "macro";
|
||||||
|
node.value = macroInvocation;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for an MVV reference value
|
||||||
|
var mvvReference = $tw.utils.parseMVVReferenceAsTransclusion(source,pos);
|
||||||
|
if(mvvReference) {
|
||||||
|
pos = mvvReference.end;
|
||||||
|
node.type = "macro";
|
||||||
|
node.value = mvvReference;
|
||||||
|
node.isMVV = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a substituted value
|
||||||
|
var substitutedValue = $tw.utils.parseTokenRegExp(source,pos,reSubstitutedValue);
|
||||||
|
if(substitutedValue) {
|
||||||
|
pos = substitutedValue.end;
|
||||||
|
node.type = "substituted";
|
||||||
|
node.rawValue = substitutedValue.match[1] || substitutedValue.match[2];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for a unquoted value
|
||||||
|
var unquotedValue = $tw.utils.parseTokenRegExp(source,pos,reUnquotedAttribute);
|
||||||
|
if(unquotedValue) {
|
||||||
|
pos = unquotedValue.end;
|
||||||
|
node.type = "string";
|
||||||
|
node.value = unquotedValue.match[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(source.charAt(pos) === "<" && source.charAt(pos + 1) === "<" && source.indexOf(">>",pos) !== -1) {
|
||||||
|
// Value looks like a macro invocation (starts with << with a closing >> ahead) but does not parse as one. Return null so the enclosing tag fails to parse rather than silently binding the attribute to "true" and treating the remainder as further attributes (restores v5.3.8 behaviour)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.type = "string";
|
||||||
|
node.value = "true";
|
||||||
|
} while(false);
|
||||||
} else {
|
} else {
|
||||||
// If there is no equals sign or colon, then this is an attribute with no value, defaulting to "true"
|
// If there is no equals sign or colon, then this is an attribute with no value, defaulting to "true"
|
||||||
node.type = "string";
|
node.type = "string";
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ exports.startup = function() {
|
|||||||
$tw.modules.applyMethods("wikimethod",$tw.Wiki.prototype);
|
$tw.modules.applyMethods("wikimethod",$tw.Wiki.prototype);
|
||||||
$tw.wiki.addIndexersToWiki();
|
$tw.wiki.addIndexersToWiki();
|
||||||
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
$tw.modules.applyMethods("tiddlerdeserializer",$tw.Wiki.tiddlerDeserializerModules);
|
||||||
|
$tw.modules.applyMethods("tiddlerserializer",$tw.Wiki.tiddlerSerializerModules);
|
||||||
$tw.macros = $tw.modules.getModulesByTypeAsHashmap("macro");
|
$tw.macros = $tw.modules.getModulesByTypeAsHashmap("macro");
|
||||||
$tw.wiki.initParsers();
|
$tw.wiki.initParsers();
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ var getCellInfo = function(text, start, length, SEPARATOR) {
|
|||||||
var isCellQuoted = text.charAt(start) === QUOTE;
|
var isCellQuoted = text.charAt(start) === QUOTE;
|
||||||
var cellStart = isCellQuoted ? start + 1 : start;
|
var cellStart = isCellQuoted ? start + 1 : start;
|
||||||
|
|
||||||
if(text.charAt(i) === SEPARATOR) {
|
// A quote licenses a separator inside the cell, so only an unquoted cell reads an immediate separator as empty
|
||||||
|
if(!isCellQuoted && text.charAt(cellStart) === SEPARATOR) {
|
||||||
return [cellStart, cellStart, false];
|
return [cellStart, cellStart, false];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,9 @@ var TW_Element = function(tag, namespace) {
|
|||||||
this.children = [];
|
this.children = [];
|
||||||
this._style = {}; // Internal style object
|
this._style = {}; // Internal style object
|
||||||
this.style = new TW_Style(this); // Proxy for style management
|
this.style = new TW_Style(this); // Proxy for style management
|
||||||
this.namespaceURI = namespace || "http://www.w3.org/1999/xhtml";
|
// createElementNS with empty-string or null normalises to null (no namespace) per spec.
|
||||||
|
// https://dom.spec.whatwg.org/#dom-document-createelementns
|
||||||
|
this.namespaceURI = namespace !== undefined ? (namespace || null) : "http://www.w3.org/1999/xhtml";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -206,7 +208,16 @@ TW_Element.prototype.addEventListener = function(type,listener,useCapture) {
|
|||||||
|
|
||||||
Object.defineProperty(TW_Element.prototype, "tagName", {
|
Object.defineProperty(TW_Element.prototype, "tagName", {
|
||||||
get: function() {
|
get: function() {
|
||||||
return this.tag || "";
|
if(!this.tag) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
// HTML elements report uppercase tagName per DOM spec. Other namespaces
|
||||||
|
// preserve case. Fakedom only models HTML documents.
|
||||||
|
// https://dom.spec.whatwg.org/#dom-element-tagname
|
||||||
|
if(this.namespaceURI === "http://www.w3.org/1999/xhtml") {
|
||||||
|
return this.tag.toUpperCase();
|
||||||
|
}
|
||||||
|
return this.tag;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ Inherit from the base widget class
|
|||||||
DiffTextWidget.prototype = new Widget();
|
DiffTextWidget.prototype = new Widget();
|
||||||
|
|
||||||
DiffTextWidget.prototype.invisibleCharacters = {
|
DiffTextWidget.prototype.invisibleCharacters = {
|
||||||
"\n": "↩︎\n",
|
"\n": "↲\n",
|
||||||
"\r": "⇠",
|
"\r": "⇠",
|
||||||
"\t": "⇥\t"
|
"\t": "⇥\t"
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ description: create a new journal tiddler
|
|||||||
|
|
||||||
\whitespace trim
|
\whitespace trim
|
||||||
\function get-tags() [<textFieldTags>] [<tagsFieldTags>] +[join[ ]]
|
\function get-tags() [<textFieldTags>] [<tagsFieldTags>] +[join[ ]]
|
||||||
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} textFieldTags={{$:/config/NewJournal/Tags}} tagsFieldTags={{$:/config/NewJournal/Tags!!tags}} journalText={{$:/config/NewJournal/Text}}>
|
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
|
||||||
<$wikify name="journalTitle" text="<$transclude $variable='now' format=<<journalTitleTemplate>>/>">
|
textFieldTags={{$:/config/NewJournal/Tags}}
|
||||||
|
tagsFieldTags={{$:/config/NewJournal/Tags!!tags}}
|
||||||
|
journalText={{$:/config/NewJournal/Text}}
|
||||||
|
>
|
||||||
<$reveal type="nomatch" state=<<journalTitle>> text="">
|
<$reveal type="nomatch" state=<<journalTitle>> text="">
|
||||||
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text={{{ [<journalTitle>get[]] }}}/>
|
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text={{{ [<journalTitle>get[]] }}}/>
|
||||||
</$reveal>
|
</$reveal>
|
||||||
<$reveal type="match" state=<<journalTitle>> text="">
|
<$reveal type="match" state=<<journalTitle>> text="">
|
||||||
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text=<<journalText>>/>
|
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=<<get-tags>> text=<<journalText>>/>
|
||||||
</$reveal>
|
</$reveal>
|
||||||
</$wikify>
|
|
||||||
</$let>
|
</$let>
|
||||||
|
|||||||
@@ -6,30 +6,23 @@ tags: $:/tags/EditTemplate
|
|||||||
\procedure lingo-base() $:/language/EditTemplate/
|
\procedure lingo-base() $:/language/EditTemplate/
|
||||||
|
|
||||||
\procedure tag-body-inner(colour,fallbackTarget,colourA,colourB,icon,tagField:"tags")
|
\procedure tag-body-inner(colour,fallbackTarget,colourA,colourB,icon,tagField:"tags")
|
||||||
<$wikify name="foregroundColor"
|
<$let foregroundColor=<<contrastcolour target=<<colour>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >>
|
||||||
text="""<$macrocall $name="contrastcolour"
|
backgroundColor=<<colour>>
|
||||||
target=<<colour>>
|
>
|
||||||
fallbackTarget=<<fallbackTarget>>
|
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
|
||||||
colourA=<<colourA>>
|
data-tag-title=<<currentTiddler>>
|
||||||
colourB=<<colourB>>/>
|
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
|
||||||
"""
|
>
|
||||||
>
|
<$transclude tiddler=<<icon>>/>
|
||||||
<$let backgroundColor=<<colour>> >
|
<$view field="title" format="text"/>
|
||||||
<span class="tc-tag-label tc-tag-list-item tc-small-gap-right"
|
<$button class="tc-btn-invisible tc-remove-tag-button"
|
||||||
data-tag-title=<<currentTiddler>>
|
style.fill=<<foregroundColor>>
|
||||||
style=`color:$(foregroundColor)$; fill:$(foregroundColor)$; background-color:$(backgroundColor)$;`
|
|
||||||
>
|
>
|
||||||
<$transclude tiddler=<<icon>>/>
|
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
|
||||||
<$view field="title" format="text"/>
|
{{$:/core/images/close-button}}
|
||||||
<$button class="tc-btn-invisible tc-remove-tag-button"
|
</$button>
|
||||||
style.fill=<<foregroundColor>>
|
</span>
|
||||||
>
|
</$let>
|
||||||
<$action-listops $tiddler=<<saveTiddler>> $field=<<tagField>> $subfilter="-[{!!title}]"/>
|
|
||||||
{{$:/core/images/close-button}}
|
|
||||||
</$button>
|
|
||||||
</span>
|
|
||||||
</$let>
|
|
||||||
</$wikify>
|
|
||||||
\end
|
\end
|
||||||
|
|
||||||
\procedure tag-body(colour,palette,icon,tagField:"tags")
|
\procedure tag-body(colour,palette,icon,tagField:"tags")
|
||||||
|
|||||||
@@ -16,15 +16,13 @@ title: $:/core/ui/TagPickerTagTemplate
|
|||||||
<$set name="backgroundColor"
|
<$set name="backgroundColor"
|
||||||
value={{{ [<currentTiddler>] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}}
|
value={{{ [<currentTiddler>] :cascade[all[shadows+tiddlers]tag[$:/tags/TiddlerColourFilter]!is[draft]get[text]] }}}
|
||||||
>
|
>
|
||||||
<$wikify name="foregroundColor"
|
<$let foregroundColor=<<contrastcolour target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>> >> >
|
||||||
text="""<$macrocall $name="contrastcolour" target=<<backgroundColor>> fallbackTarget=<<fallbackTarget>> colourA=<<colourA>> colourB=<<colourB>>/>"""
|
|
||||||
>
|
|
||||||
<span class="tc-tag-label tc-btn-invisible"
|
<span class="tc-tag-label tc-btn-invisible"
|
||||||
style=<<tag-pill-styles>>
|
style=<<tag-pill-styles>>
|
||||||
data-tag-title=<<currentTiddler>>
|
data-tag-title=<<currentTiddler>>
|
||||||
>
|
>
|
||||||
{{||$:/core/ui/TiddlerIcon}}<$view field="title" format="text"/>
|
{{||$:/core/ui/TiddlerIcon}}<$view field="title" format="text"/>
|
||||||
</span>
|
</span>
|
||||||
</$wikify>
|
</$let>
|
||||||
</$set>
|
</$set>
|
||||||
</$button>
|
</$button>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
|
|||||||
\whitespace trim
|
\whitespace trim
|
||||||
\procedure journalButton()
|
\procedure journalButton()
|
||||||
<$button tooltip={{$:/language/Buttons/NewJournalHere/Hint}} aria-label={{$:/language/Buttons/NewJournalHere/Caption}} class=<<tv-config-toolbar-class>>>
|
<$button tooltip={{$:/language/Buttons/NewJournalHere/Hint}} aria-label={{$:/language/Buttons/NewJournalHere/Caption}} class=<<tv-config-toolbar-class>>>
|
||||||
<$wikify name="journalTitle" text="""<$transclude $variable="now" format=<<journalTitleTemplate>>/>""">
|
|
||||||
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=`[[$(currentTiddlerTag)$]] $(journalTags)$`/>
|
<$action-sendmessage $message="tm-new-tiddler" title=<<journalTitle>> tags=`[[$(currentTiddlerTag)$]] $(journalTags)$`/>
|
||||||
<%if [<tv-config-toolbar-icons>match[yes]] %>
|
<%if [<tv-config-toolbar-icons>match[yes]] %>
|
||||||
{{$:/core/images/new-journal-button}}
|
{{$:/core/images/new-journal-button}}
|
||||||
@@ -16,9 +15,11 @@ description: {{$:/language/Buttons/NewJournalHere/Hint}}
|
|||||||
<$text text={{$:/language/Buttons/NewJournalHere/Caption}}/>
|
<$text text={{$:/language/Buttons/NewJournalHere/Caption}}/>
|
||||||
</span>
|
</span>
|
||||||
<%endif%>
|
<%endif%>
|
||||||
</$wikify>
|
|
||||||
</$button>
|
</$button>
|
||||||
\end
|
\end
|
||||||
<$let journalTitleTemplate={{$:/config/NewJournal/Title}} journalTags={{$:/config/NewJournal/Tags}} currentTiddlerTag=<<currentTiddler>>>
|
<$let journalTitle=<<now format={{$:/config/NewJournal/Title}}>>
|
||||||
|
journalTags={{$:/config/NewJournal/Tags}}
|
||||||
|
currentTiddlerTag=<<currentTiddler>>
|
||||||
|
>
|
||||||
<<journalButton>>
|
<<journalButton>>
|
||||||
</$let>
|
</$let>
|
||||||
|
|||||||
@@ -17,6 +17,10 @@ This page summarises high level information about the configuration of this ~Tid
|
|||||||
Drag this link to copy this tool to another wiki
|
Drag this link to copy this tool to another wiki
|
||||||
\end intrinsic-lingo-Drag/Caption
|
\end intrinsic-lingo-Drag/Caption
|
||||||
|
|
||||||
|
\procedure intrinsic-lingo-Generate/Caption()
|
||||||
|
Click to generate wiki information report
|
||||||
|
\end intrinsic-lingo-Generate/Caption
|
||||||
|
|
||||||
\procedure lingo(title,mode:"inline")
|
\procedure lingo(title,mode:"inline")
|
||||||
<%if [<title>addprefix<lingo-base>is[shadow]] %>
|
<%if [<title>addprefix<lingo-base>is[shadow]] %>
|
||||||
<$transclude $tiddler={{{ [<title>addprefix<lingo-base>] }}} $mode=<<mode>>/>
|
<$transclude $tiddler={{{ [<title>addprefix<lingo-base>] }}} $mode=<<mode>>/>
|
||||||
@@ -105,7 +109,7 @@ Drag this link to copy this tool to another wiki
|
|||||||
|
|
||||||
<$button>
|
<$button>
|
||||||
<<display-wiki-info-modal>>
|
<<display-wiki-info-modal>>
|
||||||
Click to generate wiki information report
|
<<lingo title:"Generate/Caption">>
|
||||||
</$button>
|
</$button>
|
||||||
|
|
||||||
<$link to="$:/core/ui/ControlPanel/WikiInformation">
|
<$link to="$:/core/ui/ControlPanel/WikiInformation">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
title: $:/config/OfficialPluginLibrary
|
title: $:/config/OfficialPluginLibrary
|
||||||
tags: $:/tags/PluginLibrary
|
tags: $:/tags/PluginLibrary
|
||||||
url: https://tiddlywiki.com/library/v5.4.1/index.html
|
url: https://tiddlywiki.com/library/v5.5.0/index.html
|
||||||
caption: {{$:/language/OfficialPluginLibrary}}
|
caption: {{$:/language/OfficialPluginLibrary}}
|
||||||
|
|
||||||
{{$:/language/OfficialPluginLibrary/Hint}}
|
{{$:/language/OfficialPluginLibrary/Hint}}
|
||||||
|
|||||||
@@ -8,9 +8,8 @@ type: text/vnd.tiddlywiki
|
|||||||
# Ensure the new release banner image is up to date
|
# Ensure the new release banner image is up to date
|
||||||
# Update ''master'' with changes from ''tiddlywiki-com''
|
# Update ''master'' with changes from ''tiddlywiki-com''
|
||||||
# Verify the version numbers in [[$:/config/OfficialPluginLibrary]] in `core/wiki/config/OfficialPluginLibrary.tid`
|
# Verify the version numbers in [[$:/config/OfficialPluginLibrary]] in `core/wiki/config/OfficialPluginLibrary.tid`
|
||||||
# Move the latest release note from the prerelease edition into the tw5.com edition
|
# Adjust the release date and the ''released'' field of the release tiddler (eg, [[Release 5.1.3]])
|
||||||
# Adjust the release date and the ''released'' field of the latest release tiddler (eg, [[Release 5.1.3]])
|
# Update the ''release-introduction'' definition with the new release text, and if necessary the ''description'' field of the release tiddler
|
||||||
# Also adjust the github.com comparison link to point to the tag for the new release
|
|
||||||
# Adjust the tiddler [[TiddlyWiki Archive]] to include the new version number
|
# Adjust the tiddler [[TiddlyWiki Archive]] to include the new version number
|
||||||
# Ensure [[TiddlyWiki Releases]] has the new version as the default tab
|
# Ensure [[TiddlyWiki Releases]] has the new version as the default tab
|
||||||
# Adjust the modified time of HelloThere
|
# Adjust the modified time of HelloThere
|
||||||
@@ -23,7 +22,6 @@ type: text/vnd.tiddlywiki
|
|||||||
# Run `./bin/readme-bld.sh` to build the readme files
|
# Run `./bin/readme-bld.sh` to build the readme files
|
||||||
# Commit the new readme files to ''master''
|
# Commit the new readme files to ''master''
|
||||||
# Restore `package.json` to the previous version number
|
# Restore `package.json` to the previous version number
|
||||||
# Adjust the link for "GitHub for detailed change history of this release" in the release note
|
|
||||||
# Add the credits for the new release banner to the release note, including a link to the GitHub instance of the image from the commit history
|
# Add the credits for the new release banner to the release note, including a link to the GitHub instance of the image from the commit history
|
||||||
|
|
||||||
!! Make New Release
|
!! Make New Release
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"description": "Developer documentation from https://tiddlywiki.com/dev/",
|
"description": "Developer documentation from https://tiddlywiki.com/dev/",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"tiddlywiki/highlight",
|
"tiddlywiki/highlight",
|
||||||
"tiddlywiki/nodewebkitsaver",
|
|
||||||
"tiddlywiki/github-fork-ribbon",
|
"tiddlywiki/github-fork-ribbon",
|
||||||
"tiddlywiki/menubar",
|
"tiddlywiki/menubar",
|
||||||
"tiddlywiki/internals",
|
"tiddlywiki/internals",
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
{
|
{
|
||||||
"description": "A step by step introduction to TiddlyWiki",
|
"description": "A step by step introduction to TiddlyWiki",
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"tiddlywiki/cecily",
|
|
||||||
"tiddlywiki/codemirror",
|
"tiddlywiki/codemirror",
|
||||||
"tiddlywiki/highlight",
|
"tiddlywiki/highlight",
|
||||||
"tiddlywiki/katex"
|
"tiddlywiki/katex"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
title: $:/config/LocalPluginLibrary
|
title: $:/config/LocalPluginLibrary
|
||||||
tags: $:/tags/PluginLibrary
|
tags: $:/tags/PluginLibrary
|
||||||
url: http://127.0.0.1:8080/prerelease/library/v5.4.1/index.html
|
url: http://127.0.0.1:8080/prerelease/library/v5.5.0/index.html
|
||||||
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease Local)
|
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease Local)
|
||||||
|
|
||||||
A locally installed version of the official ~TiddlyWiki plugin library at tiddlywiki.com for testing and debugging. //Requires a local web server to share the library//
|
A locally installed version of the official ~TiddlyWiki plugin library at tiddlywiki.com for testing and debugging. //Requires a local web server to share the library//
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
title: $:/config/OfficialPluginLibrary
|
title: $:/config/OfficialPluginLibrary
|
||||||
tags: $:/tags/PluginLibrary
|
tags: $:/tags/PluginLibrary
|
||||||
url: https://tiddlywiki.com/prerelease/library/v5.4.1/index.html
|
url: https://tiddlywiki.com/prerelease/library/v5.5.0/index.html
|
||||||
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)
|
caption: {{$:/language/OfficialPluginLibrary}} (Prerelease)
|
||||||
|
|
||||||
The prerelease version of the official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
|
The prerelease version of the official ~TiddlyWiki plugin library at tiddlywiki.com. Plugins, themes and language packs are maintained by the core team.
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
created: 20260714055110474
|
||||||
|
modified: 20260714055129225
|
||||||
|
tags:
|
||||||
|
title: TiddlyWiki Pre-release Size Comparison
|
||||||
|
|
||||||
|
|
||||||
|
\function bytes.to.mib(size) [abs[]divide[1048576]fixed[2]addsuffix[ MiB]]
|
||||||
|
\function bytes.to.kib(size) [abs[]divide[1024]fixed[2]addsuffix[ KiB]]
|
||||||
|
\function format.file.size(size) [<size>abs[]compare:number:gteq[1048576]bytes.to.mib<size>] :else[<size>abs[]compare:number:gteq[1024]bytes.to.kib<size>] :else[<size>addsuffix[ bytes]]
|
||||||
|
|
||||||
|
|
||||||
|
\procedure get-wiki-filesize(filepath)
|
||||||
|
\procedure completion-get-json()
|
||||||
|
<!-- Success -->
|
||||||
|
<$list filter="[<status>compare:number:gteq[200]compare:number:lteq[299]]" variable="ignore">
|
||||||
|
<$action-log msg="completed" size={{{ [<data>jsonget[size]] }}}/>
|
||||||
|
<$action-setfield $tiddler=`$:/temp/file-size-comparison/$(filepath)$` text={{{ [<data>jsonget[size]] }}} />
|
||||||
|
</$list>
|
||||||
|
\end completion-get-json
|
||||||
|
|
||||||
|
<$action-sendmessage
|
||||||
|
$message="tm-http-request"
|
||||||
|
url=`https://api.github.com/repos/TiddlyWiki/tiddlywiki.com-gh-pages/contents/$(filepath)$?ref=master`
|
||||||
|
method="GET"
|
||||||
|
oncompletion=<<completion-get-json>>
|
||||||
|
var-filepath=<<filepath>>
|
||||||
|
/>
|
||||||
|
\end get-wiki-filesize
|
||||||
|
|
||||||
|
\procedure get-wiki-filesizes()
|
||||||
|
<$list filter="empty.html prerelease/empty.html" variable="filepath">
|
||||||
|
<$action-log />
|
||||||
|
<<$transclude $variable="get-wiki-filesize" filepath=<<filepath>> >>
|
||||||
|
|
||||||
|
</$list>
|
||||||
|
\end get-wiki-filesizes
|
||||||
|
|
||||||
|
|
||||||
|
<%if [[$:/temp/file-size-comparison/empty.html]is[tiddler]] [[$:/temp/file-size-comparison/prerelease/empty.html]is[tiddler]] :and[count[]match[2]]%>
|
||||||
|
<$let delta={{{ [{$:/temp/file-size-comparison/empty.html}subtract{$:/temp/file-size-comparison/prerelease/empty.html}] }}}
|
||||||
|
message={{{ [<delta>sign[]match[-1]then[Size increased]] :else[<delta>sign[]match[1]then[Size decreased]] :else[[Size has not changed]] }}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="158" height="20" role="img" style.zoom="1.5" aria-label="<$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /> bytes">
|
||||||
|
<title><$text text=<<message>>/>: <$text text={{{ [format.file.size<delta>] }}} /></title>
|
||||||
|
<filter id="blur">
|
||||||
|
<feGaussianBlur stdDeviation="16"/>
|
||||||
|
</filter>
|
||||||
|
<linearGradient id="s" x2="0" y2="100%">
|
||||||
|
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
|
||||||
|
<stop offset="1" stop-opacity=".1"/>
|
||||||
|
</linearGradient>
|
||||||
|
<clipPath id="r">
|
||||||
|
<rect width="158" height="20" rx="3"/>
|
||||||
|
</clipPath>
|
||||||
|
<g clip-path="url(#r)">
|
||||||
|
<rect width="93" height="20" fill="#555"/>
|
||||||
|
<rect x="93" width="65" height="20" fill="#67ac09"/>
|
||||||
|
<rect width="158" height="20" fill="url(#s)"/>
|
||||||
|
</g>
|
||||||
|
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110">
|
||||||
|
<g transform="scale(.1)">
|
||||||
|
<g aria-hidden="true" fill="#010101">
|
||||||
|
<text x="475" y="150" fill-opacity=".8" filter="url(#blur)" textLength="830"><$text text=<<message>>/></text>
|
||||||
|
<text x="475" y="150" fill-opacity=".3" textLength="830"><$text text=<<message>>/></text>
|
||||||
|
</g>
|
||||||
|
<text x="475" y="140" textLength="830"><$text text=<<message>>/></text>
|
||||||
|
</g>
|
||||||
|
<g transform="scale(.1)">
|
||||||
|
<g aria-hidden="true" fill="#010101">
|
||||||
|
<text x="1245" y="150" fill-opacity=".8" filter="url(#blur)" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
|
||||||
|
<text x="1245" y="150" fill-opacity=".3" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
|
||||||
|
</g>
|
||||||
|
<text x="1245" y="140" textLength="550"><$text text={{{ [format.file.size<delta>] }}} /></text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
; Pre-release empty.html
|
||||||
|
: <$text text={{{ [{$:/temp/file-size-comparison/prerelease/empty.html}] }}} /> bytes
|
||||||
|
; <$text text={{{ [tag[ReleaseNotes]sort[released]last[]] }}} /> empty.html
|
||||||
|
: <$text text={{{ [{$:/temp/file-size-comparison/empty.html}] }}} /> bytes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
</$let>
|
||||||
|
<%else %>
|
||||||
|
<$button actions=<<get-wiki-filesizes>> class="tc-btn-big-green">
|
||||||
|
Compare size of empty.html
|
||||||
|
</$button>
|
||||||
|
<%endif%>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ The pre-release is also available as an [[empty wiki|https://tiddlywiki.com/prer
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{ TiddlyWiki Pre-release Size Comparison }}
|
||||||
|
|
||||||
<$list filter="[tag[ReleaseNotes]!has[released]!sort[created]]">
|
<$list filter="[tag[ReleaseNotes]!has[released]!sort[created]]">
|
||||||
<div class="tc-titlebar">
|
<div class="tc-titlebar">
|
||||||
<h2 class="tc-title"><$text text=<<currentTiddler>>/></h2>
|
<h2 class="tc-title"><$text text=<<currentTiddler>>/></h2>
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
modified: 20260413092032887
|
||||||
|
title: TabFour
|
||||||
|
|
||||||
|
Text tab 4
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
caption: t 1
|
||||||
|
modified: 20260413092032887
|
||||||
|
title: TabOne
|
||||||
|
|
||||||
|
Text tab 1
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
caption: t 3
|
||||||
|
description: desc
|
||||||
|
modified: 20260413092032887
|
||||||
|
title: TabThree
|
||||||
|
|
||||||
|
Text tab 3
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
caption: t 2
|
||||||
|
modified: 20260413092032887
|
||||||
|
title: TabTwo
|
||||||
|
|
||||||
|
Text tab 2
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
code-body: yes
|
||||||
|
modified: 20260413092032887
|
||||||
|
title: body-template
|
||||||
|
|
||||||
|
!! <<currentTab>>
|
||||||
|
|
||||||
|
<$transclude tiddler=<<currentTab>> mode="block"/>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
code-body: yes
|
||||||
|
modified: 20260413092032887
|
||||||
|
title: button-template
|
||||||
|
|
||||||
|
<$transclude tiddler=<<currentTab>> field="description"><$transclude tiddler=<<currentTab>> field="caption"><$macrocall $name="currentTab" $type="text/plain" $output="text/plain"/></$transclude></$transclude>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
code-body: yes
|
||||||
|
modified: 20260413092032887
|
||||||
|
title: tabs-macro-definition
|
||||||
|
|
||||||
|
\define tabs-button()
|
||||||
|
\whitespace trim
|
||||||
|
<$button
|
||||||
|
set=<<tabsState>>
|
||||||
|
setTo=<<currentTab>>
|
||||||
|
default=<<__default__>>
|
||||||
|
selectedClass="tc-tab-selected"
|
||||||
|
selectedAria="aria-selected"
|
||||||
|
tooltip={{!!tooltip}}
|
||||||
|
role="tab"
|
||||||
|
data-tab-title=<<currentTab>>
|
||||||
|
>
|
||||||
|
<$tiddler tiddler=<<save-currentTiddler>>>
|
||||||
|
<$set name="tv-wikilinks" value="no">
|
||||||
|
<$transclude tiddler=<<__buttonTemplate__>> mode="inline">
|
||||||
|
<$transclude tiddler=<<currentTab>> field="caption">
|
||||||
|
<$macrocall $name="currentTab" $type="text/plain" $output="text/plain"/>
|
||||||
|
</$transclude>
|
||||||
|
</$transclude>
|
||||||
|
</$set>
|
||||||
|
</$tiddler>
|
||||||
|
<<__actions__>>
|
||||||
|
</$button>
|
||||||
|
\end
|
||||||
|
|
||||||
|
\define tabs-tab()
|
||||||
|
\whitespace trim
|
||||||
|
<$set name="save-currentTiddler" value=<<currentTiddler>>>
|
||||||
|
<$tiddler tiddler=<<currentTab>>>
|
||||||
|
<<tabs-button>>
|
||||||
|
</$tiddler>
|
||||||
|
</$set>
|
||||||
|
\end
|
||||||
|
|
||||||
|
\define tabs-tab-list()
|
||||||
|
\whitespace trim
|
||||||
|
<$list filter=<<__tabsList__>> variable="currentTab" storyview="pop">
|
||||||
|
<<tabs-tab>>
|
||||||
|
</$list>
|
||||||
|
\end
|
||||||
|
|
||||||
|
\define tabs-tab-body()
|
||||||
|
\whitespace trim
|
||||||
|
<$list filter=<<__tabsList__>> variable="currentTab">
|
||||||
|
<$reveal type="match" state=<<tabsState>> text=<<currentTab>> default=<<__default__>> retain=<<__retain__>> tag="div">
|
||||||
|
<$transclude tiddler=<<__template__>> mode="block">
|
||||||
|
<$transclude tiddler=<<currentTab>> mode="block"/>
|
||||||
|
</$transclude>
|
||||||
|
</$reveal>
|
||||||
|
</$list>
|
||||||
|
\end
|
||||||
|
|
||||||
|
\define tabs(tabsList,default,state:"$:/state/tab",class,template,buttonTemplate,retain,actions,explicitState)
|
||||||
|
\whitespace trim
|
||||||
|
<$qualify title=<<__state__>> name="qualifiedState">
|
||||||
|
<$let tabsState={{{ [<__explicitState__>minlength[1]] ~[<qualifiedState>] }}}>
|
||||||
|
<div class={{{ [[tc-tab-set]addsuffix[ ]addsuffix<__class__>] }}} role="tablist">
|
||||||
|
<div class={{{ [[tc-tab-buttons]addsuffix[ ]addsuffix<__class__>] }}}>
|
||||||
|
<<tabs-tab-list>>
|
||||||
|
</div>
|
||||||
|
<div class={{{ [[tc-tab-divider]addsuffix[ ]addsuffix<__class__>] }}}/>
|
||||||
|
<div class={{{ [[tc-tab-content]addsuffix[ ]addsuffix<__class__>] }}} role="tabpanel">
|
||||||
|
<<tabs-tab-body>>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</$let>
|
||||||
|
</$qualify>
|
||||||
|
\end
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
modified: 20260413092032887
|
||||||
|
title: test-tabs-horizontal-all
|
||||||
|
|
||||||
|
\import [[tabs-macro-definition]]
|
||||||
|
<<tabs "TabOne TabTwo TabThree TabFour" "TabTwo" "$:/state/test-tab-01" template:"body-template" buttonTemplate:"button-template" explicitState:"$:/state/explicit">>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
modified: 20260413092032887
|
||||||
|
title: test-tabs-horizontal
|
||||||
|
|
||||||
|
\import [[tabs-macro-definition]]
|
||||||
|
<<tabs "TabOne TabTwo TabThree TabFour" "TabTwo" "$:/state/test-tab-01">>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
modified: 20260413092032887
|
||||||
|
title: test-tabs-vertical
|
||||||
|
|
||||||
|
\import [[tabs-macro-definition]]
|
||||||
|
<<tabs "TabOne TabTwo TabThree TabFour" "TabTwo" "$:/state/test-tab-02" "tc-vertical">>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
/*\
|
||||||
|
title: test-back-indexer.js
|
||||||
|
type: application/javascript
|
||||||
|
tags: [[$:/tags/test-spec]]
|
||||||
|
|
||||||
|
Regression tests for #9916: the back-indexer must never record system
|
||||||
|
tiddlers as backlink or backtransclude sources, neither when the index is
|
||||||
|
first built nor when it is incrementally updated.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("Back-indexer system source tests (#9916)", function() {
|
||||||
|
function setupWiki() {
|
||||||
|
// Create a wiki with indexers and one primed backlink pair
|
||||||
|
var wiki = new $tw.Wiki();
|
||||||
|
wiki.addIndexersToWiki();
|
||||||
|
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "TestIncoming",
|
||||||
|
text: ""});
|
||||||
|
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "TestOutgoing",
|
||||||
|
text: "A link to [[TestIncoming]]"});
|
||||||
|
return wiki;
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should never report a system tiddler as a backlink source", function() {
|
||||||
|
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
|
||||||
|
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"}))
|
||||||
|
// and run the filter again; $:/temp/demo must not appear.
|
||||||
|
var wiki = setupWiki();
|
||||||
|
// The first lookup builds the lazy index; its initial scan skips system tiddlers
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
// The incremental update() must skip them too
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "$:/temp/system-source",
|
||||||
|
text: "A link to [[TestIncoming]]"});
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep backlinks stable while a linking system tiddler is modified and deleted", function() {
|
||||||
|
// Browser console: $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") to prime the index,
|
||||||
|
// then $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
|
||||||
|
// change its text, $tw.wiki.deleteTiddler("$:/temp/demo"); the filter result never changes.
|
||||||
|
var wiki = setupWiki();
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "$:/temp/system-source",
|
||||||
|
text: "A link to [[TestIncoming]]"});
|
||||||
|
// Modify: both the old and the new side of the index update are system tiddlers
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "$:/temp/system-source",
|
||||||
|
text: "Links to [[TestIncoming]] and [[TestOutgoing]]"});
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.deleteTiddler("$:/temp/system-source");
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should drop the backlink when its source is renamed to a system title", function() {
|
||||||
|
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[HelloThere]]"})),
|
||||||
|
// confirm Demo is in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then
|
||||||
|
// $tw.wiki.renameTiddler("Demo","$:/Demo"); neither Demo nor $:/Demo remains a source.
|
||||||
|
var wiki = setupWiki();
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.renameTiddler("TestOutgoing","$:/TestOutgoing");
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should gain the backlink when a linking system tiddler is renamed to a normal title", function() {
|
||||||
|
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "$:/temp/demo", text: "[[HelloThere]]"})),
|
||||||
|
// then $tw.wiki.renameTiddler("$:/temp/demo","DemoVisible");
|
||||||
|
// DemoVisible now appears in $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]").
|
||||||
|
var wiki = setupWiki();
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "$:/temp/system-source",
|
||||||
|
text: "A link to [[TestIncoming]]"});
|
||||||
|
wiki.renameTiddler("$:/temp/system-source","VisibleSource");
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,VisibleSource");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still report normal sources for a system tiddler target", function() {
|
||||||
|
// Only sources are filtered, targets are not. Browser console:
|
||||||
|
// $tw.wiki.addTiddler(new $tw.Tiddler({title: "Demo", text: "[[$:/config/NewJournal/Tags]]"}));
|
||||||
|
// $tw.wiki.filterTiddlers("[[$:/config/NewJournal/Tags]backlinks[]]") contains Demo.
|
||||||
|
var wiki = setupWiki();
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "TestSystemLinker",
|
||||||
|
text: "A link to [[$:/config/Target]]"});
|
||||||
|
expect(wiki.filterTiddlers("[[$:/config/Target]backlinks[]]").join(",")).toBe("TestSystemLinker");
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Adversarial probes", function() {
|
||||||
|
it("should never index a shadow tiddler as a source, even when revealed by deleting its override", function() {
|
||||||
|
// Goes red when a refactor of BackSubIndexer.update() checks the tiddler instead of
|
||||||
|
// the exists flag:
|
||||||
|
// if(updateDescriptor["new"].tiddler) { ... } // broken: goes red here
|
||||||
|
// if(updateDescriptor["new"].exists) { ... } // correct: stays green
|
||||||
|
// After deleteTiddler() on an override, boot.js fills new.tiddler via getTiddler(),
|
||||||
|
// which falls back to the revealed shadow, while new.exists stays false because
|
||||||
|
// only the real store counts. The broken variant indexes the shadow's links and
|
||||||
|
// the final expect fails with "TestOutgoing,ShadowSource".
|
||||||
|
// The first expect also goes red if _init() ever starts scanning shadows; the
|
||||||
|
// middle expect pins that a real override IS indexed, so this probe cannot be
|
||||||
|
// satisfied by indexing nothing at all.
|
||||||
|
// Browser console: override a plugin shadow with text [[HelloThere]], check it appears in
|
||||||
|
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]"), then $tw.wiki.deleteTiddler(title);
|
||||||
|
// the title disappears from the filter result even though the shadow still renders.
|
||||||
|
var wiki = setupWiki();
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "$:/plugins/test/shadow-plugin",
|
||||||
|
type: "application/json",
|
||||||
|
"plugin-type": "plugin",
|
||||||
|
text: JSON.stringify({tiddlers: {
|
||||||
|
"ShadowSource": {title: "ShadowSource", text: "A shadow link to [[TestIncoming]]"}
|
||||||
|
}})});
|
||||||
|
wiki.readPluginInfo();
|
||||||
|
wiki.registerPluginTiddlers("plugin");
|
||||||
|
wiki.unpackPluginTiddlers();
|
||||||
|
// The initial scan sees only real tiddlers, not the shadow
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "ShadowSource",
|
||||||
|
text: "An overriding link to [[TestIncoming]]"});
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,ShadowSource");
|
||||||
|
// Deleting the override reveals the shadow; it must not enter the index
|
||||||
|
wiki.deleteTiddler("ShadowSource");
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle hostile titles like __proto__ as source and target", function() {
|
||||||
|
// Goes red when any title-keyed hashmap on the backlinks path is created like this:
|
||||||
|
// this.index = {}; // broken: goes red here
|
||||||
|
// instead of:
|
||||||
|
// this.index = Object.create(null); // correct: stays green
|
||||||
|
// this.index = new Map(); // a Map/Set refactor also stays green
|
||||||
|
// The same applies to the boot tiddler store and to the per-target source maps
|
||||||
|
// (self.index[target] = ...). On a plain {} the assignment index["__proto__"] = x
|
||||||
|
// stores no key; it silently replaces the object's prototype. Depending on which
|
||||||
|
// map regresses, the __proto__ tiddler never registers as a source (second expect),
|
||||||
|
// or its target entry lands in the shared prototype, where it pollutes every other
|
||||||
|
// lookup and lookup("__proto__") returns garbage (third expect).
|
||||||
|
// Browser console: $tw.wiki.addTiddler(new $tw.Tiddler({title: "__proto__",
|
||||||
|
// text: "[[HelloThere]]"})); __proto__ appears in
|
||||||
|
// $tw.wiki.filterTiddlers("[[HelloThere]backlinks[]]") and
|
||||||
|
// $tw.wiki.filterTiddlers("[[__proto__]backlinks[]]") lists tiddlers linking to it.
|
||||||
|
var wiki = setupWiki();
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing");
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "__proto__",
|
||||||
|
text: "A link to [[TestIncoming]]"});
|
||||||
|
expect(wiki.filterTiddlers("TestIncoming +[backlinks[]]").join(",")).toBe("TestOutgoing,__proto__");
|
||||||
|
wiki.addTiddler({
|
||||||
|
title: "ProtoLinker",
|
||||||
|
text: "A link to [[__proto__]]"});
|
||||||
|
expect(wiki.filterTiddlers("[[__proto__]backlinks[]]").join(",")).toBe("ProtoLinker");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*\
|
||||||
|
title: test-csv.js
|
||||||
|
type: application/javascript
|
||||||
|
tags: [[$:/tags/test-spec]]
|
||||||
|
|
||||||
|
Tests the CSV parser.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("CSV tests", function() {
|
||||||
|
|
||||||
|
it("parses a simple table", function() {
|
||||||
|
expect($tw.utils.parseCsvString("a,b,c\n1,2,3")).toEqual([["a","b","c"],["1","2","3"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses a table whose first cell reads empty", function() {
|
||||||
|
expect($tw.utils.parseCsvString(",b,c\n1,2,3")).toEqual([["","b","c"],["1","2","3"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses an empty cell in the middle of a row", function() {
|
||||||
|
expect($tw.utils.parseCsvString("a,,c")).toEqual([["a","","c"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses quoted cells", function() {
|
||||||
|
expect($tw.utils.parseCsvString('a,"b,still b",c')).toEqual([["a","b,still b","c"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses a quoted cell holding an escaped quote", function() {
|
||||||
|
expect($tw.utils.parseCsvString('a,"b ""quoted""",c')).toEqual([["a",'b "quoted"',"c"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("honours a custom separator", function() {
|
||||||
|
expect($tw.utils.parseCsvString("a;b;c",{separator: ";"})).toEqual([["a","b","c"]]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("parses a table with a header row into hashmaps", function() {
|
||||||
|
expect($tw.utils.parseCsvStringWithHeader("a,b\n1,2")).toEqual([{a: "1", b: "2"}]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -19,6 +19,56 @@ describe("fakedom tests", function() {
|
|||||||
expect($tw.fakeDocument.createTextNode("text").TEXT_NODE).toBe(3);
|
expect($tw.fakeDocument.createTextNode("text").TEXT_NODE).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Per DOM spec, tagName returns the HTML-uppercased qualified name for HTML
|
||||||
|
// elements. Other namespaces preserve case.
|
||||||
|
// https://dom.spec.whatwg.org/#dom-element-tagname
|
||||||
|
var HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||||
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
||||||
|
|
||||||
|
it("tagName uppercases for HTML namespace", function() {
|
||||||
|
// Default namespace is HTML
|
||||||
|
expect($tw.fakeDocument.createElement("div").tagName).toBe("DIV");
|
||||||
|
// The exact predicate the select widget relies on (#9839)
|
||||||
|
expect($tw.fakeDocument.createElement("optgroup").tagName === "OPTGROUP").toBe(true);
|
||||||
|
// Already-uppercase input stays uppercase
|
||||||
|
expect($tw.fakeDocument.createElement("OPTGROUP").tagName).toBe("OPTGROUP");
|
||||||
|
// Mixed-case input is normalised
|
||||||
|
expect($tw.fakeDocument.createElement("Div").tagName).toBe("DIV");
|
||||||
|
// Hyphenated custom-element names uppercase whole tag, hyphens survive
|
||||||
|
expect($tw.fakeDocument.createElement("my-button").tagName).toBe("MY-BUTTON");
|
||||||
|
// Empty tag returns empty string
|
||||||
|
expect($tw.fakeDocument.createElement("").tagName).toBe("");
|
||||||
|
// Explicit HTML namespace via createElementNS uppercases the same way
|
||||||
|
expect($tw.fakeDocument.createElementNS(HTML_NS,"Div").tagName).toBe("DIV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tagName preserves case for non-HTML namespaces", function() {
|
||||||
|
// SVG: lowercase preserved
|
||||||
|
expect($tw.fakeDocument.createElementNS(SVG_NS,"circle").tagName).toBe("circle");
|
||||||
|
// SVG: camelCase preserved (linearGradient is the canonical example)
|
||||||
|
expect($tw.fakeDocument.createElementNS(SVG_NS,"linearGradient").tagName).toBe("linearGradient");
|
||||||
|
// SVG: already-uppercase input is also preserved (NOT lowercased)
|
||||||
|
expect($tw.fakeDocument.createElementNS(SVG_NS,"DIV").tagName).toBe("DIV");
|
||||||
|
// Empty namespace string is "no namespace", not HTML. Case preserved.
|
||||||
|
expect($tw.fakeDocument.createElementNS("","div").tagName).toBe("div");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("tagName reflects current state without mutating it", function() {
|
||||||
|
// Reading tagName must not overwrite the internal `tag` field
|
||||||
|
var el = $tw.fakeDocument.createElement("div");
|
||||||
|
expect(el.tagName).toBe("DIV");
|
||||||
|
expect(el.tag).toBe("div");
|
||||||
|
// Idempotent: two reads return identical values
|
||||||
|
var first = el.tagName, second = el.tagName;
|
||||||
|
expect(first).toBe(second);
|
||||||
|
// Dynamic namespace change is reflected. The getter must read current
|
||||||
|
// state, not a value cached at construction time.
|
||||||
|
var dynamic = $tw.fakeDocument.createElement("foo");
|
||||||
|
expect(dynamic.tagName).toBe("FOO");
|
||||||
|
dynamic.namespaceURI = SVG_NS;
|
||||||
|
expect(dynamic.tagName).toBe("foo");
|
||||||
|
});
|
||||||
|
|
||||||
// Real CSSStyleDeclaration returns undefined for Symbol property keys.
|
// Real CSSStyleDeclaration returns undefined for Symbol property keys.
|
||||||
// Without a guard, the TW_Style Proxy throws on Symbol access. This bites
|
// Without a guard, the TW_Style Proxy throws on Symbol access. This bites
|
||||||
// in practice when Jasmine pretty-prints fakedom elements on failure.
|
// in practice when Jasmine pretty-prints fakedom elements on failure.
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/*\
|
||||||
|
title: test-filesystem-dynamic-store.js
|
||||||
|
type: application/javascript
|
||||||
|
tags: [[$:/tags/test-spec]]
|
||||||
|
|
||||||
|
Tests for the filesystem syncadaptor dynamic store feature: save routing
|
||||||
|
driven by saveFilter, and chokidar-based watching of out-of-band edits.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
if($tw.node) {
|
||||||
|
|
||||||
|
var fs = require("fs"),
|
||||||
|
path = require("path"),
|
||||||
|
os = require("os");
|
||||||
|
|
||||||
|
// Load the filesystem adaptor source as if it were a TW module, so that
|
||||||
|
// $tw is provided without having to include the plugin in the edition
|
||||||
|
// (which would pull in the server-side syncer and keep the test runner alive).
|
||||||
|
var adaptorPath = path.resolve($tw.boot.bootPath,"..","plugins","tiddlywiki","filesystem","filesystemadaptor.js"),
|
||||||
|
adaptorTitle = "$:/plugins/tiddlywiki/filesystem/filesystemadaptor.js";
|
||||||
|
if(!$tw.modules.titles[adaptorTitle]) {
|
||||||
|
$tw.modules.titles[adaptorTitle] = {
|
||||||
|
moduleType: "syncadaptor",
|
||||||
|
definition: fs.readFileSync(adaptorPath,"utf8")
|
||||||
|
};
|
||||||
|
$tw.wiki.addTiddler({
|
||||||
|
title: adaptorTitle,
|
||||||
|
type: "application/javascript",
|
||||||
|
"module-type": "syncadaptor",
|
||||||
|
text: ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var FileSystemAdaptor = $tw.modules.execute(adaptorTitle).adaptorClass;
|
||||||
|
|
||||||
|
function makeTempDir(prefix) {
|
||||||
|
return fs.mkdtempSync(path.join(os.tmpdir(),prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeDirRecursive(dir) {
|
||||||
|
if(fs.existsSync(dir)) {
|
||||||
|
fs.rmSync(dir,{recursive: true, force: true});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("filesystem dynamic store", function() {
|
||||||
|
|
||||||
|
var tmpRoot, wikiTiddlers, storeDir, origDynamicStores, origFiles, originalBootPath;
|
||||||
|
var adaptor, wiki;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
tmpRoot = makeTempDir("tw-dyn-");
|
||||||
|
wikiTiddlers = path.join(tmpRoot,"tiddlers");
|
||||||
|
storeDir = path.join(tmpRoot,"content");
|
||||||
|
fs.mkdirSync(wikiTiddlers);
|
||||||
|
fs.mkdirSync(storeDir);
|
||||||
|
|
||||||
|
origDynamicStores = $tw.boot.dynamicStores;
|
||||||
|
origFiles = $tw.boot.files;
|
||||||
|
originalBootPath = $tw.boot.wikiTiddlersPath;
|
||||||
|
|
||||||
|
$tw.boot.dynamicStores = [{
|
||||||
|
id: storeDir,
|
||||||
|
directory: storeDir,
|
||||||
|
saveFilter: "[type[text/x-markdown]]",
|
||||||
|
watch: true,
|
||||||
|
debounce: 40,
|
||||||
|
filesRegExp: ".*\\.tid$",
|
||||||
|
searchSubdirectories: false,
|
||||||
|
isTiddlerFile: true,
|
||||||
|
fields: {}
|
||||||
|
}];
|
||||||
|
$tw.boot.files = Object.create(null);
|
||||||
|
$tw.boot.wikiTiddlersPath = wikiTiddlers;
|
||||||
|
|
||||||
|
wiki = new $tw.Wiki();
|
||||||
|
adaptor = new FileSystemAdaptor({wiki: wiki, boot: $tw.boot});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function(done) {
|
||||||
|
adaptor.close().then(function() {
|
||||||
|
$tw.boot.dynamicStores = origDynamicStores;
|
||||||
|
$tw.boot.files = origFiles;
|
||||||
|
$tw.boot.wikiTiddlersPath = originalBootPath;
|
||||||
|
removeDirRecursive(tmpRoot);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes saves for matching tiddlers into the dynamic store directory", function(done) {
|
||||||
|
wiki.addTiddler(new $tw.Tiddler({title: "note1", type: "text/x-markdown", text: "hello"}));
|
||||||
|
var tiddler = wiki.getTiddler("note1");
|
||||||
|
adaptor.getTiddlerFileInfo(tiddler,function(err,fileInfo) {
|
||||||
|
expect(err).toBeFalsy();
|
||||||
|
expect(fileInfo.filepath.indexOf(storeDir)).toBe(0);
|
||||||
|
expect(fileInfo.dynamicStoreId).toBe(storeDir);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes saves for non-matching tiddlers into the default wiki tiddlers path", function(done) {
|
||||||
|
wiki.addTiddler(new $tw.Tiddler({title: "note2", type: "text/vnd.tiddlywiki", text: "plain"}));
|
||||||
|
var tiddler = wiki.getTiddler("note2");
|
||||||
|
adaptor.getTiddlerFileInfo(tiddler,function(err,fileInfo) {
|
||||||
|
expect(err).toBeFalsy();
|
||||||
|
expect(fileInfo.filepath.indexOf(wikiTiddlers)).toBe(0);
|
||||||
|
expect(fileInfo.dynamicStoreId).toBeFalsy();
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps saving a tiddler into the store it originally came from", function(done) {
|
||||||
|
// Simulate a tiddler that was loaded at boot from the dynamic store
|
||||||
|
$tw.boot.files["frozen"] = {
|
||||||
|
filepath: path.join(storeDir,"frozen.tid"),
|
||||||
|
type: "application/x-tiddler",
|
||||||
|
hasMetaFile: false,
|
||||||
|
isEditableFile: true,
|
||||||
|
dynamicStoreId: storeDir
|
||||||
|
};
|
||||||
|
// Its current type no longer matches the saveFilter — store id must still win
|
||||||
|
wiki.addTiddler(new $tw.Tiddler({title: "frozen", type: "text/vnd.tiddlywiki", text: "still here"}));
|
||||||
|
adaptor.getTiddlerFileInfo(wiki.getTiddler("frozen"),function(err,fileInfo) {
|
||||||
|
expect(err).toBeFalsy();
|
||||||
|
expect(fileInfo.filepath.indexOf(storeDir)).toBe(0);
|
||||||
|
expect(fileInfo.dynamicStoreId).toBe(storeDir);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Note: the chokidar watcher's only job is to call processFileEvent in
|
||||||
|
// response to fs events. We invoke processFileEvent directly here so the
|
||||||
|
// tests don't depend on real fs notifications being delivered (some CI
|
||||||
|
// sandboxes do not propagate inotify events to chokidar).
|
||||||
|
|
||||||
|
it("processes external additions, changes and deletions", function(done) {
|
||||||
|
var store = $tw.boot.dynamicStores[0];
|
||||||
|
var filepath = path.join(storeDir,"external.tid");
|
||||||
|
fs.writeFileSync(filepath,"title: external\ntype: text/x-markdown\n\nInitial\n");
|
||||||
|
adaptor.processFileEvent(store,filepath,"change");
|
||||||
|
adaptor.getUpdatedTiddlers({},function(err,updates) {
|
||||||
|
expect(err).toBeFalsy();
|
||||||
|
expect(updates.modifications).toContain("external");
|
||||||
|
adaptor.loadTiddler("external",function(err,fields) {
|
||||||
|
expect(err).toBeFalsy();
|
||||||
|
expect(fields).toBeTruthy();
|
||||||
|
expect(fields.title).toBe("external");
|
||||||
|
expect(fields.text).toContain("Initial");
|
||||||
|
// Edit
|
||||||
|
fs.writeFileSync(filepath,"title: external\ntype: text/x-markdown\n\nChanged\n");
|
||||||
|
adaptor.processFileEvent(store,filepath,"change");
|
||||||
|
adaptor.getUpdatedTiddlers({},function(err,updates) {
|
||||||
|
expect(updates.modifications).toContain("external");
|
||||||
|
// Delete
|
||||||
|
fs.unlinkSync(filepath);
|
||||||
|
adaptor.processFileEvent(store,filepath,"unlink");
|
||||||
|
adaptor.getUpdatedTiddlers({},function(err,updates) {
|
||||||
|
expect(updates.deletions).toContain("external");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("suppresses echoes when the file on disk matches the current wiki tiddler", function(done) {
|
||||||
|
var store = $tw.boot.dynamicStores[0];
|
||||||
|
wiki.addTiddler(new $tw.Tiddler({title: "echo", type: "text/x-markdown", text: "same\n"}));
|
||||||
|
var filepath = path.join(storeDir,"echo.tid");
|
||||||
|
fs.writeFileSync(filepath,"title: echo\ntype: text/x-markdown\n\nsame\n");
|
||||||
|
adaptor.processFileEvent(store,filepath,"change");
|
||||||
|
adaptor.getUpdatedTiddlers({},function(err,updates) {
|
||||||
|
expect(updates.modifications).not.toContain("echo");
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1062,7 +1062,7 @@ describe("Filter tests", function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle the deserializers operator", function() {
|
it("should handle the deserializers operator", function() {
|
||||||
var expectedDeserializers = ["application/javascript","application/json","application/x-tiddler","application/x-tiddler-html-div","application/x-tiddlers","text/css","text/html","text/plain"];
|
var expectedDeserializers = ["application/javascript","application/json","application/x-tiddler","application/x-tiddler-html-div","application/x-tiddlers","text/css","text/html","text/markdown","text/plain","text/x-markdown"];
|
||||||
if($tw.browser) {
|
if($tw.browser) {
|
||||||
expectedDeserializers.unshift("(DOM)");
|
expectedDeserializers.unshift("(DOM)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,301 @@
|
|||||||
|
/*\
|
||||||
|
title: test-markdown-frontmatter.js
|
||||||
|
type: application/javascript
|
||||||
|
tags: [[$:/tags/test-spec]]
|
||||||
|
|
||||||
|
Tests for the markdown plugin's YAML frontmatter parser, deserializer,
|
||||||
|
and serializer.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
|
||||||
|
/* eslint-env node, browser, jasmine */
|
||||||
|
/* eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
describe("markdown YAML frontmatter", function() {
|
||||||
|
|
||||||
|
var yaml = require("$:/plugins/tiddlywiki/markdown/yaml.js");
|
||||||
|
var deserializer = require("$:/plugins/tiddlywiki/markdown/frontmatter-deserializer.js");
|
||||||
|
var serializer = require("$:/plugins/tiddlywiki/markdown/frontmatter-serializer.js");
|
||||||
|
|
||||||
|
// --- YAML parser ---
|
||||||
|
|
||||||
|
describe("yaml.load scalars", function() {
|
||||||
|
it("parses null forms", function() {
|
||||||
|
expect(yaml.load("null")).toBe(null);
|
||||||
|
expect(yaml.load("~")).toBe(null);
|
||||||
|
expect(yaml.load("")).toBe(null);
|
||||||
|
});
|
||||||
|
it("parses booleans", function() {
|
||||||
|
expect(yaml.load("true")).toBe(true);
|
||||||
|
expect(yaml.load("True")).toBe(true);
|
||||||
|
expect(yaml.load("false")).toBe(false);
|
||||||
|
});
|
||||||
|
it("parses numbers", function() {
|
||||||
|
expect(yaml.load("42")).toBe(42);
|
||||||
|
expect(yaml.load("-7")).toBe(-7);
|
||||||
|
expect(yaml.load("3.14")).toBe(3.14);
|
||||||
|
expect(yaml.load("1e10")).toBe(1e10);
|
||||||
|
expect(yaml.load("0xFF")).toBe(255);
|
||||||
|
expect(yaml.load("0o17")).toBe(15);
|
||||||
|
});
|
||||||
|
it("parses special floats", function() {
|
||||||
|
expect(yaml.load(".inf")).toBe(Infinity);
|
||||||
|
expect(yaml.load("-.inf")).toBe(-Infinity);
|
||||||
|
});
|
||||||
|
it("parses quoted strings", function() {
|
||||||
|
expect(yaml.load('"hello world"')).toBe("hello world");
|
||||||
|
expect(yaml.load("'hello world'")).toBe("hello world");
|
||||||
|
expect(yaml.load('"line1\\nline2"')).toBe("line1\nline2");
|
||||||
|
});
|
||||||
|
it("parses plain strings", function() {
|
||||||
|
expect(yaml.load("hello")).toBe("hello");
|
||||||
|
});
|
||||||
|
it("rejects non-strings", function() {
|
||||||
|
expect(function() { yaml.load(123); }).toThrowError(yaml.YAMLException);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("yaml.load flow collections", function() {
|
||||||
|
it("parses flow sequences", function() {
|
||||||
|
expect(yaml.load("[a, b, c]")).toEqual(["a","b","c"]);
|
||||||
|
expect(yaml.load("[1, 2, 3]")).toEqual([1,2,3]);
|
||||||
|
expect(yaml.load('[1, "two", true, null]')).toEqual([1,"two",true,null]);
|
||||||
|
expect(yaml.load("[]")).toEqual([]);
|
||||||
|
expect(yaml.load('["multi word", simple]')).toEqual(["multi word","simple"]);
|
||||||
|
});
|
||||||
|
it("parses flow mappings", function() {
|
||||||
|
expect(yaml.load("{a: 1, b: 2}")).toEqual({a:1,b:2});
|
||||||
|
expect(yaml.load("{}")).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("yaml.load block collections", function() {
|
||||||
|
it("parses simple block mappings", function() {
|
||||||
|
expect(yaml.load("title: Hello\ntags: foo bar\nrating: 6")).toEqual({
|
||||||
|
title: "Hello",
|
||||||
|
tags: "foo bar",
|
||||||
|
rating: 6
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("parses block mapping with flow array value", function() {
|
||||||
|
expect(yaml.load("title: Test\ntags: [concept, synthesis, multi word tag]")).toEqual({
|
||||||
|
title: "Test",
|
||||||
|
tags: ["concept","synthesis","multi word tag"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("parses block mapping with quoted value", function() {
|
||||||
|
expect(yaml.load('title: "A: Subtitle"')).toEqual({title: "A: Subtitle"});
|
||||||
|
});
|
||||||
|
it("parses block mapping with null value", function() {
|
||||||
|
expect(yaml.load("title: Test\ndescription:")).toEqual({
|
||||||
|
title: "Test",
|
||||||
|
description: null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("parses block sequences", function() {
|
||||||
|
expect(yaml.load("- alpha\n- beta\n- gamma")).toEqual(["alpha","beta","gamma"]);
|
||||||
|
expect(yaml.load("- 1\n- two\n- true")).toEqual([1,"two",true]);
|
||||||
|
});
|
||||||
|
it("parses nested block mappings", function() {
|
||||||
|
expect(yaml.load("outer:\n inner: value\n count: 3")).toEqual({
|
||||||
|
outer: {inner: "value", count: 3}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("parses block mapping with block sequence value", function() {
|
||||||
|
expect(yaml.load("title: Test\ntags:\n - concept\n - synthesis")).toEqual({
|
||||||
|
title: "Test",
|
||||||
|
tags: ["concept","synthesis"]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("ignores comments and blank lines", function() {
|
||||||
|
expect(yaml.load("# comment\ntitle: Test\n# more\nrating: 5")).toEqual({
|
||||||
|
title: "Test",
|
||||||
|
rating: 5
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("yaml.dump", function() {
|
||||||
|
it("dumps simple mappings", function() {
|
||||||
|
expect(yaml.dump({title: "Hello", rating: 6}).trim()).toBe("title: Hello\nrating: 6");
|
||||||
|
});
|
||||||
|
it("dumps arrays", function() {
|
||||||
|
expect(yaml.dump({tags: ["a","b"]}).trim()).toBe("tags:\n - a\n - b");
|
||||||
|
});
|
||||||
|
it("dumps null and booleans", function() {
|
||||||
|
expect(yaml.dump({x: null}).trim()).toBe("x: null");
|
||||||
|
expect(yaml.dump({x: true, y: false}).trim()).toBe("x: true\ny: false");
|
||||||
|
});
|
||||||
|
it("dumps empty containers", function() {
|
||||||
|
expect(yaml.dump({}).trim()).toBe("{}");
|
||||||
|
expect(yaml.dump({x: []}).trim()).toBe("x: []");
|
||||||
|
});
|
||||||
|
it("quotes string values that look like numbers", function() {
|
||||||
|
expect(yaml.dump({rating: "9"}).trim()).toBe('rating: "9"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Deserializer ---
|
||||||
|
|
||||||
|
describe("frontmatter deserializer", function() {
|
||||||
|
var ds = deserializer["text/x-markdown"];
|
||||||
|
|
||||||
|
it("extracts simple frontmatter into fields", function() {
|
||||||
|
var result = ds("---\ntitle: Foo\ntags: [a, b]\n---\n\nBody text.",{});
|
||||||
|
expect(result.length).toBe(1);
|
||||||
|
expect(result[0].title).toBe("Foo");
|
||||||
|
expect(result[0].tags).toBe("a b");
|
||||||
|
expect(result[0].text).toBe("Body text.");
|
||||||
|
expect(result[0].type).toBe("text/x-markdown");
|
||||||
|
});
|
||||||
|
it("converts YAML arrays for list fields to TW bracketed lists", function() {
|
||||||
|
var result = ds("---\ntags: [concept, multi word tag, simple]\n---\n\nbody",{});
|
||||||
|
expect(result[0].tags).toBe("concept [[multi word tag]] simple");
|
||||||
|
});
|
||||||
|
it("falls back to plain body when no frontmatter present", function() {
|
||||||
|
var result = ds("Just a body, no frontmatter.",{});
|
||||||
|
expect(result[0].text).toBe("Just a body, no frontmatter.");
|
||||||
|
expect(result[0].title).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("falls back to plain body when frontmatter is malformed", function() {
|
||||||
|
var result = ds("---\nnot: [valid yaml: at all\n---\n\nbody",{});
|
||||||
|
// Malformed YAML still parses something; we just ensure body is set
|
||||||
|
expect(result[0].text).toBeDefined();
|
||||||
|
});
|
||||||
|
it("parses ISO-8601 created and modified into TW native format", function() {
|
||||||
|
var result = ds("---\ntitle: T\ncreated: 2025-01-02T03:04:05.006Z\nmodified: 2026-02-03T04:05:06Z\n---\n\nb",{});
|
||||||
|
expect(result[0].created).toBe("20250102030405006");
|
||||||
|
expect(result[0].modified).toBe("20260203040506000");
|
||||||
|
});
|
||||||
|
it("accepts a bare YYYY-MM-DD date for created/modified", function() {
|
||||||
|
var result = ds("---\ntitle: T\ncreated: 2025-03-15\n---\n\nb",{});
|
||||||
|
expect(result[0].created).toBe("20250315000000000");
|
||||||
|
});
|
||||||
|
it("passes through TW native timestamps for created/modified", function() {
|
||||||
|
var result = ds("---\ntitle: T\ncreated: \"20250101000000000\"\nmodified: \"20260101000000\"\n---\n\nb",{});
|
||||||
|
expect(result[0].created).toBe("20250101000000000");
|
||||||
|
expect(result[0].modified).toBe("20260101000000000");
|
||||||
|
});
|
||||||
|
it("drops unparseable created/modified values", function() {
|
||||||
|
var result = ds("---\ntitle: T\ncreated: not-a-date\n---\n\nb",{});
|
||||||
|
expect(result[0].created).toBeUndefined();
|
||||||
|
});
|
||||||
|
it("merges existing tags with frontmatter tags", function() {
|
||||||
|
var result = ds("---\ntags: [b, c]\n---\n\nbody",{tags: "a"});
|
||||||
|
// Order: existing first, then new uniques
|
||||||
|
expect(result[0].tags).toBe("a b c");
|
||||||
|
});
|
||||||
|
it("emits non-string non-array values as JSON", function() {
|
||||||
|
var result = ds("---\ntitle: T\nmeta: {nested: deep}\n---\n\nb",{});
|
||||||
|
expect(result[0].meta).toBe('{"nested":"deep"}');
|
||||||
|
});
|
||||||
|
it("handles CRLF line endings around frontmatter", function() {
|
||||||
|
var result = ds("---\r\ntitle: T\r\n---\r\n\r\nbody",{});
|
||||||
|
expect(result[0].title).toBe("T");
|
||||||
|
expect(result[0].text).toBe("body");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Serializer ---
|
||||||
|
|
||||||
|
describe("frontmatter serializer", function() {
|
||||||
|
var ser = serializer["text/x-markdown"];
|
||||||
|
|
||||||
|
it("emits frontmatter and body", function() {
|
||||||
|
var t = new $tw.Tiddler({title: "Foo", text: "body", tags: "a b"});
|
||||||
|
var out = ser(t);
|
||||||
|
expect(out).toContain("---\n");
|
||||||
|
expect(out).toContain("title: Foo");
|
||||||
|
expect(out).toContain("tags:\n - a\n - b");
|
||||||
|
expect(out.split("\n---\n\n")[1]).toBe("body");
|
||||||
|
});
|
||||||
|
it("emits list fields as YAML arrays preserving multi-word tags", function() {
|
||||||
|
var t = new $tw.Tiddler({title: "X", tags: "concept [[multi word tag]] simple", text: "b"});
|
||||||
|
var out = ser(t);
|
||||||
|
expect(out).toContain("- concept");
|
||||||
|
expect(out).toContain("- multi word tag");
|
||||||
|
expect(out).toContain("- simple");
|
||||||
|
});
|
||||||
|
it("skips text, bag, revision", function() {
|
||||||
|
var t = new $tw.Tiddler({
|
||||||
|
title: "X",
|
||||||
|
text: "body",
|
||||||
|
bag: "default",
|
||||||
|
revision: "1"
|
||||||
|
});
|
||||||
|
var out = ser(t);
|
||||||
|
expect(out).not.toContain("bag:");
|
||||||
|
expect(out).not.toContain("revision:");
|
||||||
|
expect(out).not.toContain("text:");
|
||||||
|
});
|
||||||
|
it("emits created and modified as ISO-8601 strings", function() {
|
||||||
|
var t = new $tw.Tiddler({
|
||||||
|
title: "X",
|
||||||
|
text: "b",
|
||||||
|
created: "20250102030405006",
|
||||||
|
modified: "20260203040506000"
|
||||||
|
});
|
||||||
|
var out = ser(t);
|
||||||
|
expect(out).toContain('created: "2025-01-02T03:04:05.006Z"');
|
||||||
|
expect(out).toContain('modified: "2026-02-03T04:05:06.000Z"');
|
||||||
|
});
|
||||||
|
it("drops unparseable created/modified values", function() {
|
||||||
|
var t = new $tw.Tiddler({title: "X", text: "b", created: "garbage"});
|
||||||
|
var out = ser(t);
|
||||||
|
expect(out).not.toContain("created:");
|
||||||
|
});
|
||||||
|
it("skips type when it equals text/x-markdown", function() {
|
||||||
|
var t = new $tw.Tiddler({title: "X", type: "text/x-markdown", text: "b"});
|
||||||
|
expect(ser(t)).not.toContain("type:");
|
||||||
|
});
|
||||||
|
it("emits type when it differs from text/x-markdown", function() {
|
||||||
|
var t = new $tw.Tiddler({title: "X", type: "text/html", text: "b"});
|
||||||
|
expect(ser(t)).toContain("type: text/html");
|
||||||
|
});
|
||||||
|
it("emits no frontmatter when only skipped fields are present", function() {
|
||||||
|
var t = new $tw.Tiddler({text: "body only"});
|
||||||
|
expect(ser(t)).toBe("body only");
|
||||||
|
});
|
||||||
|
it("returns empty string for null tiddler", function() {
|
||||||
|
expect(ser(null)).toBe("");
|
||||||
|
});
|
||||||
|
it("title appears first in output", function() {
|
||||||
|
var t = new $tw.Tiddler({title: "Z", rating: "9", tags: "a", text: "b"});
|
||||||
|
var out = ser(t);
|
||||||
|
var lines = out.split("\n");
|
||||||
|
// First line is "---", second should be "title: Z"
|
||||||
|
expect(lines[0]).toBe("---");
|
||||||
|
expect(lines[1]).toBe("title: Z");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Round-trip ---
|
||||||
|
|
||||||
|
describe("frontmatter round-trip", function() {
|
||||||
|
var ds = deserializer["text/x-markdown"];
|
||||||
|
var ser = serializer["text/x-markdown"];
|
||||||
|
|
||||||
|
it("preserves title, tags, and body across deserialize → serialize", function() {
|
||||||
|
var input = "---\ntitle: My Tiddler\ntags: [concept, synthesis]\nrating: \"7\"\n---\n\nThis is the body.";
|
||||||
|
var fields = ds(input,{})[0];
|
||||||
|
var t = new $tw.Tiddler(fields);
|
||||||
|
var out = ser(t);
|
||||||
|
var reparsed = ds(out,{})[0];
|
||||||
|
expect(reparsed.title).toBe("My Tiddler");
|
||||||
|
expect(reparsed.tags).toBe("concept synthesis");
|
||||||
|
expect(reparsed.rating).toBe("7");
|
||||||
|
expect(reparsed.text).toBe("This is the body.");
|
||||||
|
});
|
||||||
|
it("preserves created and modified across deserialize → serialize", function() {
|
||||||
|
var input = "---\ntitle: T\ncreated: 2025-01-02T03:04:05.006Z\nmodified: 2026-02-03T04:05:06.007Z\n---\n\nbody";
|
||||||
|
var fields = ds(input,{})[0];
|
||||||
|
var t = new $tw.Tiddler(fields);
|
||||||
|
var out = ser(t);
|
||||||
|
var reparsed = ds(out,{})[0];
|
||||||
|
expect(reparsed.created).toBe("20250102030405006");
|
||||||
|
expect(reparsed.modified).toBe("20260203040506007");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
"plugins": [
|
"plugins": [
|
||||||
"tiddlywiki/jasmine",
|
"tiddlywiki/jasmine",
|
||||||
"tiddlywiki/wikitext-serialize",
|
"tiddlywiki/wikitext-serialize",
|
||||||
"tiddlywiki/geospatial"
|
"tiddlywiki/geospatial",
|
||||||
|
"tiddlywiki/markdown"
|
||||||
],
|
],
|
||||||
"themes": [
|
"themes": [
|
||||||
"tiddlywiki/vanilla",
|
"tiddlywiki/vanilla",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
created: 20231005205623086
|
created: 20231005205623086
|
||||||
modified: 20250807100434131
|
modified: 20260710090951727
|
||||||
tags: About
|
tags: About
|
||||||
title: TiddlyWiki Archive
|
title: TiddlyWiki Archive
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
\procedure versions()
|
\procedure versions()
|
||||||
5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9
|
5.1.0 5.1.1 5.1.2 5.1.3 5.1.4 5.1.5 5.1.6 5.1.7 5.1.8 5.1.9
|
||||||
@@ -9,7 +10,7 @@ title: TiddlyWiki Archive
|
|||||||
5.1.20 5.1.21 5.1.22 5.1.23
|
5.1.20 5.1.21 5.1.22 5.1.23
|
||||||
5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7
|
5.2.0 5.2.1 5.2.2 5.2.3 5.2.4 5.2.5 5.2.6 5.2.7
|
||||||
5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8
|
5.3.0 5.3.1 5.3.2 5.3.3 5.3.4 5.3.5 5.3.6 5.3.7 5.3.8
|
||||||
5.4.0
|
5.4.0 5.4.1
|
||||||
\end
|
\end
|
||||||
|
|
||||||
Older versions of TiddlyWiki are available in the [[archive|https://github.com/TiddlyWiki/tiddlywiki.com-gh-pages/tree/master/archive]]:
|
Older versions of TiddlyWiki are available in the [[archive|https://github.com/TiddlyWiki/tiddlywiki.com-gh-pages/tree/master/archive]]:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
created: 20130822170200000
|
created: 20130822170200000
|
||||||
icon: $:/core/icon
|
icon: $:/core/icon
|
||||||
list: [[A Gentle Guide to TiddlyWiki]] [[Discover TiddlyWiki]] [[Some of the things you can do with TiddlyWiki]] [[Ten reasons to switch to TiddlyWiki]] Examples [[What happened to the original TiddlyWiki?]]
|
list: [[A Gentle Guide to TiddlyWiki]] [[Discover TiddlyWiki]] [[Some of the things you can do with TiddlyWiki]] [[Ten reasons to switch to TiddlyWiki]] Examples [[What happened to the original TiddlyWiki?]]
|
||||||
modified: 20260420192600833
|
modified: 20260710091122803
|
||||||
tags: Welcome
|
tags: Welcome
|
||||||
title: HelloThere
|
title: HelloThere
|
||||||
type: text/vnd.tiddlywiki
|
type: text/vnd.tiddlywiki
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 50 KiB |
@@ -56,6 +56,11 @@ Directory specifications in the `directories` array may take the following forms
|
|||||||
** ''isEditableFile'' - <<.from-version "5.1.23">> (optional) if `true`, changes to the tiddler be saved back to the original file. The tiddler will be saved back to the original filepath as long as it does not generate a result from the $:/config/FileSystemPath filters, which will override the final filepath generated if a result is returned from a filter.
|
** ''isEditableFile'' - <<.from-version "5.1.23">> (optional) if `true`, changes to the tiddler be saved back to the original file. The tiddler will be saved back to the original filepath as long as it does not generate a result from the $:/config/FileSystemPath filters, which will override the final filepath generated if a result is returned from a filter.
|
||||||
** ''searchSubdirectories'' - <<.from-version "5.1.23">> (optional) if `true`, all subdirectories of the //path// are searched recursively for files that match the (optional) //filesRegExp//. If no //filesRegExp// is provided, all files in all subdirectories of the //path// are loaded. Tiddler titles generated via the //source// attribute //filename// (see above) will only include the filename, not any of the subdirectories of the path. If this results in multiple files with loaded with the same tiddler title, then only the last file loaded under that tiddler title will be in memory. In order to prevent this, you can use the //filepath// attribute instead of //filename//. Alternately, you can include multiple directory objects and customise the title field with a //prefix// or //suffix// alongside the //source// attribute.
|
** ''searchSubdirectories'' - <<.from-version "5.1.23">> (optional) if `true`, all subdirectories of the //path// are searched recursively for files that match the (optional) //filesRegExp//. If no //filesRegExp// is provided, all files in all subdirectories of the //path// are loaded. Tiddler titles generated via the //source// attribute //filename// (see above) will only include the filename, not any of the subdirectories of the path. If this results in multiple files with loaded with the same tiddler title, then only the last file loaded under that tiddler title will be in memory. In order to prevent this, you can use the //filepath// attribute instead of //filename//. Alternately, you can include multiple directory objects and customise the title field with a //prefix// or //suffix// alongside the //source// attribute.
|
||||||
** ''fields'' - (required) an object containing values that override or customise the fields provided in the tiddler file (see above)
|
** ''fields'' - (required) an object containing values that override or customise the fields provided in the tiddler file (see above)
|
||||||
|
** ''dynamicStore'' - <<.from-version "5.5.0">> (optional) an object marking the directory as a //dynamic store// that is both loaded at boot and actively watched on disk. The filesystem syncadaptor uses the watcher to pick up out-of-band changes (e.g. edits made by an external editor) and folds them into the running wiki. The object has the following properties:
|
||||||
|
*** ''saveFilter'' - (optional) a filter evaluated against each tiddler the wiki tries to save. Tiddlers that match are saved back into this directory instead of the default tiddlers folder. The first matching dynamic store wins, so specificity matters when multiple stores are registered.
|
||||||
|
*** ''watch'' - (optional, defaults to `true`) set to `false` to disable the chokidar watcher for this store (tiddlers are still loaded and save-routed, but external changes are not picked up live)
|
||||||
|
*** ''debounce'' - (optional, defaults to `400`) the per-file debounce window in milliseconds. File events that occur within this window of a previous event for the same file are coalesced, which avoids duplicated reloads during atomic-rename saves performed by many editors.
|
||||||
|
*** Changes to a file on disk are diffed against the tiddler currently in the wiki before being reported, so self-writes performed by TiddlyWiki itself do not trigger spurious reload events. JavaScript module tiddlers (tiddlers with `type: application/javascript` and a `module-type` field) are never hot-reloaded, because that would require restarting the TiddlyWiki process.
|
||||||
|
|
||||||
Fields can also be overridden for particular files by creating a file with the same name plus the suffix `.meta` -- see TiddlerFiles.
|
Fields can also be overridden for particular files by creating a file with the same name plus the suffix `.meta` -- see TiddlerFiles.
|
||||||
|
|
||||||
@@ -135,6 +140,29 @@ From the examples in [[Customising Tiddler File Naming]] we see that the final `
|
|||||||
|
|
||||||
Then, the `[tag[.txt]then[.txt]]` filter in the $:/config/FileSystemExtensions tiddler forces all these tiddlers to be saved back to disk as *.txt and accompanying *.txt.meta files (overriding the normal tiddler-type to file-type mapping). In this case, allowing the snippets of Tiddlywiki wikitext or markdown-text to be saved back to "text" *.txt files.
|
Then, the `[tag[.txt]then[.txt]]` filter in the $:/config/FileSystemExtensions tiddler forces all these tiddlers to be saved back to disk as *.txt and accompanying *.txt.meta files (overriding the normal tiddler-type to file-type mapping). In this case, allowing the snippets of Tiddlywiki wikitext or markdown-text to be saved back to "text" *.txt files.
|
||||||
|
|
||||||
|
!! Dynamic store for markdown files
|
||||||
|
|
||||||
|
<<.from-version "5.5.0">> This example treats a sibling `content/` folder as a dynamic store: every `.md` file is loaded at boot, every markdown tiddler saved by the wiki is routed back into `content/`, and external edits to those files are picked up automatically by chokidar.
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"directories": [
|
||||||
|
{
|
||||||
|
"path": "../content",
|
||||||
|
"filesRegExp": "^.*\\.md$",
|
||||||
|
"isTiddlerFile": true,
|
||||||
|
"searchSubdirectories": true,
|
||||||
|
"dynamicStore": {
|
||||||
|
"saveFilter": "[type[text/x-markdown]]",
|
||||||
|
"watch": true,
|
||||||
|
"debounce": 400
|
||||||
|
},
|
||||||
|
"fields": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
!! Importing and auto-tagging images
|
!! Importing and auto-tagging images
|
||||||
|
|
||||||
This example imports all the image files in the `files` directory and all its subdirectories as external-image tiddlers, and tags them based on their filepath. Each tiddler is set up with the following fields:
|
This example imports all the image files in the `files` directory and all its subdirectories as external-image tiddlers, and tags them based on their filepath. Each tiddler is set up with the following fields:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
title: $:/changenotes/5.4.0/#9715
|
||||||
|
change-type: performance
|
||||||
|
change-category: filters
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
github-contributors: linonetwo
|
||||||
|
release: 5.4.0
|
||||||
|
description: Optimized tag[] and !tag[] filter operators to use Set for O(1) lookup, matching search:tags[] performance.
|
||||||
|
github-links: [[https://github.com/Jermolene/TiddlyWiki5/pull/9715]]
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
change-category: internal
|
||||||
|
change-type: bugfix
|
||||||
|
created: 20260710092852681
|
||||||
|
description: Fixes an issue with the minheight of textareas.
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9828
|
||||||
|
modified: 20260710093223971
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.4.1/#9828
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
Fixes an issue with the minheight of textareas.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
title: $:/changenotes/5.4.0/#9829
|
||||||
|
description: Update Polish translation
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: enhancement
|
||||||
|
change-category: translation
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9829
|
||||||
|
github-contributors: EvidentlyCube
|
||||||
|
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
title: $:/changenotes/5.4.1/#9836
|
|
||||||
description: Fix mid-text link cutoff, fix disabling freelinks toggle blanks view, and add configurable MaxLinks cap
|
|
||||||
tags: $:/tags/ChangeNote
|
|
||||||
release: 5.4.1
|
|
||||||
change-type: bugfix
|
|
||||||
change-category: plugin
|
change-category: plugin
|
||||||
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9836
|
change-type: bugfix
|
||||||
|
created: 20260710092644515
|
||||||
|
description: Freelinks: Fix mid-text link cutoff, fix disabling freelinks toggle blanks view, and add configurable ~MaxLinks cap
|
||||||
github-contributors: s793016
|
github-contributors: s793016
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9836
|
||||||
|
modified: 20260710092700225
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.4.1/#9836
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
Fixes correctness and reliability issues in the freelinks plugin.
|
Fixes correctness and reliability issues in the freelinks plugin.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
change-category: widget
|
||||||
|
change-type: bugfix
|
||||||
|
created: 20260710093059614
|
||||||
|
description: Fixes a regression in the SelectWidget when selecting multiple values.
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9841
|
||||||
|
modified: 20260710093210171
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.4.1/#9841
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
Fixes a regression in the SelectWidget when selecting multiple values.
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
title: $:/changenotes/5.4.0/#9870
|
||||||
|
description: Update Japanese translation
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: enhancement
|
||||||
|
change-category: translation
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9870
|
||||||
|
github-contributors: IchijikuIchigo
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
change-category: plugin
|
||||||
|
change-type: bugfix
|
||||||
|
created: 20260710093318232
|
||||||
|
description: Katex: restores a missing font
|
||||||
|
github-contributors: Leilei332
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9873
|
||||||
|
modified: 20260710093432151
|
||||||
|
release: 5.4.1
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.4.1/#9873
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
Restores the missing KaTeX_Caligraphic-Regular.woff2
|
||||||
|
font in the Katex plugin
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
caption: 5.4.1
|
caption: 5.4.1
|
||||||
created: 20260508181012812
|
created: 20260508181012812
|
||||||
modified: 20260508181012812
|
description: Release v5.4.1 addresses regressions and bugs introduced in the v5.4.0 release.
|
||||||
|
modified: 20260710091945414
|
||||||
|
released: 20260710090509444
|
||||||
tags: ReleaseNotes
|
tags: ReleaseNotes
|
||||||
title: Release 5.4.1
|
title: Release 5.4.1
|
||||||
type: text/vnd.tiddlywiki
|
type: text/vnd.tiddlywiki
|
||||||
description: Under development
|
|
||||||
|
|
||||||
\procedure release-introduction()
|
\procedure release-introduction()
|
||||||
Release v5.4.1 is under development.
|
Release v5.4.1 addresses regressions and bugs introduced in the v5.4.0 release.
|
||||||
\end release-introduction
|
\end release-introduction
|
||||||
|
|
||||||
|
\define banner-credit-user-name() Peter
|
||||||
|
\define banner-credit-user-link() https://talk.tiddlywiki.org/u/peter
|
||||||
|
\define banner-credit-discussion-link() https://talk.tiddlywiki.org/t/vote-for-the-banner-tiddlywiki-v5-4-0/15016
|
||||||
|
\define banner-credit-permalink() https://raw.githubusercontent.com/TiddlyWiki/TiddlyWiki5/92caa7312ebc51c59cd345cc81b4a326661a0650/editions/tw5.com/tiddlers/images/New%20Release%20Banner.webp
|
||||||
|
|
||||||
<<releasenote 5.4.1>>
|
<<releasenote 5.4.1>>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
change-category: widget
|
||||||
|
change-type: enhancement
|
||||||
|
created: 20260711020921000
|
||||||
|
description: The diff-text widget shows removed or added newlines with a visible ↲ glyph
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9736
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.5.0/#9736
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
* The diff-text widget marks newline characters in a diff with the visible `↲` glyph (U+21B2) instead of an invisible character, so line break changes are recognisable in the rendered diff (fixes [[Issue #9461|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9461]])
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
change-category: translation
|
||||||
|
change-type: enhancement
|
||||||
|
created: 20260711020729000
|
||||||
|
description: The "Click to generate wiki info" button in the control panel is now translatable
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9737
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.5.0/#9737
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
* The "Click to generate wiki info" button on the control panel's wiki information tab uses a language string instead of hard coded English text, so translators can localise it (fixes [[Issue #9654|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9654]])
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
change-category: nodejs
|
||||||
|
change-type: bugfix
|
||||||
|
created: 20260711020057000
|
||||||
|
description: The Node.js server no longer emits the DEP0169 url.parse() deprecation warning
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9742
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.5.0/#9742
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
* The server module parses request URLs with the WHATWG `URL` API instead of the deprecated `url.parse()`, so recent Node.js versions no longer print the DEP0169 deprecation warning on startup (fixes [[Issue #9628|https://github.com/TiddlyWiki/TiddlyWiki5/issues/9628]])
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
title: $:/changenotes/5.5.0/#9806
|
||||||
|
description: Filesystem dynamic stores with live reload via chokidar
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: feature
|
||||||
|
change-category: nodejs
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9806
|
||||||
|
github-contributors: Jermolene
|
||||||
|
|
||||||
|
The filesystem syncadaptor can now be configured to treat a folder as a [[dynamic store|tiddlywiki.files Files]]: tiddlers in the folder are loaded at boot, a configurable filter decides which tiddlers are saved back into that folder instead of the default `tiddlers/` directory, and external edits to files in the folder are picked up live by a [[chokidar|https://github.com/paulmillr/chokidar]] watcher.
|
||||||
|
|
||||||
|
Dynamic stores are declared inside a `tiddlywiki.files` specification via the new `dynamicStore` property on a directory entry. For example, the following declaration loads every `.md` file in a sibling `content/` folder, routes any tiddler with `type: text/x-markdown` back to that folder on save, and hot-reloads changes made by an external editor:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"directories": [{
|
||||||
|
"path": "../content",
|
||||||
|
"filesRegExp": "^.*\\.md$",
|
||||||
|
"isTiddlerFile": true,
|
||||||
|
"dynamicStore": {
|
||||||
|
"saveFilter": "[type[text/x-markdown]]",
|
||||||
|
"watch": true,
|
||||||
|
"debounce": 400
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
* File events are debounced per file (default 400ms, configurable via `debounce`) to cope with editors that save atomically via rename.
|
||||||
|
* Each detected change is diffed against the current in-wiki tiddler before being reported, so self-writes performed by TiddlyWiki itself do not cause reload loops.
|
||||||
|
* Deletions on disk propagate to the wiki via the syncer's standard server-side-deletion path.
|
||||||
|
* JavaScript module tiddlers (those with `type: application/javascript` and a `module-type` field) are never hot-reloaded; reloading them would require restarting the TiddlyWiki process.
|
||||||
|
|
||||||
|
This feature adds [[chokidar|https://github.com/paulmillr/chokidar]] as a new runtime dependency of TiddlyWiki on Node.js.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
title: $:/changenotes/5.5.0/#9816
|
||||||
|
description: Replaces some wikify widget with call dynamic syntax
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: enhancement
|
||||||
|
change-category: internal
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9816
|
||||||
|
github-contributors: Leilei332
|
||||||
|
|
||||||
|
Replaces unnecessary wikify widget usage with call dynamic attribute syntax.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
change-category: internal
|
||||||
|
change-type: bugfix
|
||||||
|
created: 20260711014241000
|
||||||
|
description: fakedom tagName is uppercase for HTML elements, matching the DOM spec
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9843
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.5.0/#9843
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
* `$tw.fakeDocument` elements now report `tagName` in uppercase for HTML elements, matching the DOM specification and real browsers; code that compares fakedom `tagName` against lowercase strings must be updated
|
||||||
|
* Creating an element with an empty or null namespace now produces a plain element without a namespace
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
change-category: filters
|
||||||
|
change-type: bugfix
|
||||||
|
created: 20260711011920000
|
||||||
|
description: backlinks[] and backtranscludes[] no longer report edited system tiddlers as sources
|
||||||
|
github-contributors: pmario
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/issues/9917
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
title: $:/changenotes/5.5.0/#9917
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
* Editing a system tiddler that links or transcludes another tiddler no longer adds it as a [[backlinks|backlinks Operator]] or [[backtranscludes|backtranscludes Operator]] source; the incremental back-index update now skips system tiddlers like the initial scan always did
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
title: $:/changenotes/5.5.0/#9919
|
||||||
|
created: 20260712000000000
|
||||||
|
modified: 20260712000000000
|
||||||
|
description: Fix the CSV parser returning every cell empty
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: bugfix
|
||||||
|
change-category: internal
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9919
|
||||||
|
github-contributors: joshuafontany
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
|
||||||
|
`getCellInfo` read its loop variable before the loop declared it, so hoisting made it undefined and the parser tested the first character of the whole text rather than the first character of the cell. Any CSV opening with a separator therefore returned every cell empty, and the table rendered blank.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
title: $:/changenotes/5.5.0/#9933
|
||||||
|
description: Update Chinese translations
|
||||||
|
release: 5.5.0
|
||||||
|
tags: $:/tags/ChangeNote
|
||||||
|
change-type: enhancement
|
||||||
|
change-category: translation
|
||||||
|
github-links: https://github.com/TiddlyWiki/TiddlyWiki5/pull/9933
|
||||||
|
github-contributors: BramChen
|
||||||
|
|
||||||
|
* Add `WikiInformation/Generate/Caption` in `ControlPanel.multids`
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
caption: 5.5.0
|
||||||
|
created: 20260710103826404
|
||||||
|
modified: 20260710103826404
|
||||||
|
tags: ReleaseNotes
|
||||||
|
title: Release 5.5.0
|
||||||
|
type: text/vnd.tiddlywiki
|
||||||
|
description: Under development
|
||||||
|
|
||||||
|
\procedure release-introduction()
|
||||||
|
Release v5.5.0 is under development.
|
||||||
|
\end release-introduction
|
||||||
|
|
||||||
|
<<releasenote 5.5.0>>
|
||||||
@@ -10,7 +10,6 @@ Advanced/ShadowInfo/Shadow/Hint: Tiddler <$link to=<<infoTiddler>>><$text text=<
|
|||||||
Advanced/ShadowInfo/Shadow/Source: Zdefiniiowany we wtyczce <$link to=<<pluginTiddler>>><$text text=<<pluginTiddler>>/></$link>
|
Advanced/ShadowInfo/Shadow/Source: Zdefiniiowany we wtyczce <$link to=<<pluginTiddler>>><$text text=<<pluginTiddler>>/></$link>
|
||||||
Advanced/ShadowInfo/OverriddenShadow/Hint: Nadpisany przez zwykłego tiddlera
|
Advanced/ShadowInfo/OverriddenShadow/Hint: Nadpisany przez zwykłego tiddlera
|
||||||
Advanced/CascadeInfo/Heading: Szczegóły Kaskady
|
Advanced/CascadeInfo/Heading: Szczegóły Kaskady
|
||||||
Advanced/CascadeInfo/Hint: These are the view template segments that are resolved for each of the system view template cascades
|
|
||||||
Advanced/CascadeInfo/Hint: Lista elementów widoku, których kaskady określają szablony użyte do wyświetlenia tiddlera
|
Advanced/CascadeInfo/Hint: Lista elementów widoku, których kaskady określają szablony użyte do wyświetlenia tiddlera
|
||||||
Advanced/CascadeInfo/Detail/View: Widok
|
Advanced/CascadeInfo/Detail/View: Widok
|
||||||
Advanced/CascadeInfo/Detail/ActiveCascadeFilter: Aktywny filtr kaskady
|
Advanced/CascadeInfo/Detail/ActiveCascadeFilter: Aktywny filtr kaskady
|
||||||
|
|||||||
@@ -256,4 +256,5 @@ ViewTemplateTags/Caption: 查看模板标签区
|
|||||||
ViewTemplateTags/Hint: 默认的查看模板使用此规则级联,动态选择模板以显示条目的标签区域。
|
ViewTemplateTags/Hint: 默认的查看模板使用此规则级联,动态选择模板以显示条目的标签区域。
|
||||||
WikiInformation/Caption: 维基信息
|
WikiInformation/Caption: 维基信息
|
||||||
WikiInformation/Hint: 此页面总结了有关此 ~TiddlyWiki 配置的高阶信息。旨在让用户能够快速与他人共享其 ~TiddlyWiki 配置的相关信息,例如,在其中一个论坛中寻求帮助时。不包含隐私或个人信息,没有明确复制和粘贴在其他地方的情况
|
WikiInformation/Hint: 此页面总结了有关此 ~TiddlyWiki 配置的高阶信息。旨在让用户能够快速与他人共享其 ~TiddlyWiki 配置的相关信息,例如,在其中一个论坛中寻求帮助时。不包含隐私或个人信息,没有明确复制和粘贴在其他地方的情况
|
||||||
WikiInformation/Drag/Caption: 拖动此链接可将此工具复制到另一个维基
|
WikiInformation/Drag/Caption: 拖动此链接可将此工具复制到另一个维基
|
||||||
|
WikiInformation/Generate/Caption: 生成维基信息报告
|
||||||
@@ -256,4 +256,5 @@ ViewTemplateTags/Caption: 檢視範本標籤
|
|||||||
ViewTemplateTags/Hint: 預設的檢視範本使用此規則級聯,動態選擇範本以顯示條目的標籤。
|
ViewTemplateTags/Hint: 預設的檢視範本使用此規則級聯,動態選擇範本以顯示條目的標籤。
|
||||||
WikiInformation/Caption: 維基資訊
|
WikiInformation/Caption: 維基資訊
|
||||||
WikiInformation/Hint: 此頁面總結了有關此 ~TiddlyWiki 配置的高階資訊。旨在讓使用者能夠快速與他人共享其 ~TiddlyWiki 配置的相關資訊,例如,在其中一個論壇中尋求幫助時。不包含隱私或個人資訊,沒有明確複製和粘貼在其他地方的情況
|
WikiInformation/Hint: 此頁面總結了有關此 ~TiddlyWiki 配置的高階資訊。旨在讓使用者能夠快速與他人共享其 ~TiddlyWiki 配置的相關資訊,例如,在其中一個論壇中尋求幫助時。不包含隱私或個人資訊,沒有明確複製和粘貼在其他地方的情況
|
||||||
WikiInformation/Drag/Caption: 拖動此連結可將此工具複製到另一個維基
|
WikiInformation/Drag/Caption: 拖動此連結可將此工具複製到另一個維基
|
||||||
|
WikiInformation/Generate/Caption: 產生維基資訊報告
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
TiddlyWiki created by Jeremy Ruston, (jeremy [at] jermolene [dot] com)
|
TiddlyWiki created by Jeremy Ruston, (jeremy [at] jermolene [dot] com)
|
||||||
|
|
||||||
Copyright (c) 2004-2007, Jeremy Ruston
|
Copyright (c) 2004-2007, Jeremy Ruston
|
||||||
Copyright (c) 2007-2026, UnaMesa Association
|
Copyright (c) 2007-2025, UnaMesa Association
|
||||||
All rights reserved.
|
All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
|||||||
Generated
+47
-6
@@ -1,13 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "tiddlywiki",
|
"name": "tiddlywiki",
|
||||||
"version": "5.4.0",
|
"version": "5.5.0-prerelease",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "tiddlywiki",
|
"name": "tiddlywiki",
|
||||||
"version": "5.4.0",
|
"version": "5.4.1",
|
||||||
"license": "BSD",
|
"license": "BSD",
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.3"
|
||||||
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"tiddlywiki": "tiddlywiki.js"
|
"tiddlywiki": "tiddlywiki.js"
|
||||||
},
|
},
|
||||||
@@ -19,7 +22,7 @@
|
|||||||
"globals": "16.4.0"
|
"globals": "16.4.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.8.2"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
@@ -329,6 +332,21 @@
|
|||||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^4.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.16.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -717,10 +735,20 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.1",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||||
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
|
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/puzrin"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/nodeca"
|
||||||
|
}
|
||||||
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
@@ -906,6 +934,19 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "4.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
|
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14.18.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "tiddlywiki",
|
"name": "tiddlywiki",
|
||||||
"preferGlobal": true,
|
"preferGlobal": true,
|
||||||
"version": "5.4.1-prerelease",
|
"version": "5.5.0-prerelease",
|
||||||
"author": "Jeremy Ruston <jeremy@jermolene.com>",
|
"author": "Jeremy Ruston <jeremy@jermolene.com>",
|
||||||
"description": "a non-linear personal web notebook",
|
"description": "a non-linear personal web notebook",
|
||||||
"contributors": [
|
"contributors": [
|
||||||
@@ -23,11 +23,14 @@
|
|||||||
"tiddlywiki5",
|
"tiddlywiki5",
|
||||||
"wiki"
|
"wiki"
|
||||||
],
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "9.36.0",
|
"@eslint/js": "9.36.0",
|
||||||
|
"@stylistic/eslint-plugin": "5.4.0",
|
||||||
"eslint": "9.36.0",
|
"eslint": "9.36.0",
|
||||||
"eslint-plugin-es-x": "9.1.0",
|
"eslint-plugin-es-x": "9.1.0",
|
||||||
"@stylistic/eslint-plugin": "5.4.0",
|
|
||||||
"globals": "16.4.0"
|
"globals": "16.4.0"
|
||||||
},
|
},
|
||||||
"license": "BSD",
|
"license": "BSD",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ A sync adaptor module for synchronising with the local filesystem via node.js AP
|
|||||||
|
|
||||||
// Get a reference to the file system
|
// Get a reference to the file system
|
||||||
var fs = $tw.node ? require("fs") : null;
|
var fs = $tw.node ? require("fs") : null;
|
||||||
|
var path = $tw.node ? require("path") : null;
|
||||||
|
|
||||||
function FileSystemAdaptor(options) {
|
function FileSystemAdaptor(options) {
|
||||||
this.wiki = options.wiki;
|
this.wiki = options.wiki;
|
||||||
@@ -20,6 +21,19 @@ function FileSystemAdaptor(options) {
|
|||||||
if(this.boot.wikiTiddlersPath) {
|
if(this.boot.wikiTiddlersPath) {
|
||||||
$tw.utils.createDirectory(this.boot.wikiTiddlersPath);
|
$tw.utils.createDirectory(this.boot.wikiTiddlersPath);
|
||||||
}
|
}
|
||||||
|
// Buffers for out-of-band file changes, drained by getUpdatedTiddlers
|
||||||
|
this.modifications = Object.create(null);
|
||||||
|
this.deletions = Object.create(null);
|
||||||
|
this.pendingTimers = Object.create(null);
|
||||||
|
this.watchers = [];
|
||||||
|
this.setupWatchers();
|
||||||
|
// Only advertise getUpdatedTiddlers (and so opt into syncer polling) when
|
||||||
|
// there is actually a dynamic store to report changes from. Otherwise the
|
||||||
|
// syncer would reschedule its poll forever and keep node alive past the
|
||||||
|
// natural end of headless commands like --build.
|
||||||
|
if(!(this.boot.dynamicStores && this.boot.dynamicStores.length > 0)) {
|
||||||
|
this.getUpdatedTiddlers = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileSystemAdaptor.prototype.name = "filesystem";
|
FileSystemAdaptor.prototype.name = "filesystem";
|
||||||
@@ -27,35 +41,56 @@ FileSystemAdaptor.prototype.name = "filesystem";
|
|||||||
FileSystemAdaptor.prototype.supportsLazyLoading = false;
|
FileSystemAdaptor.prototype.supportsLazyLoading = false;
|
||||||
|
|
||||||
FileSystemAdaptor.prototype.isReady = function() {
|
FileSystemAdaptor.prototype.isReady = function() {
|
||||||
// The file system adaptor is always ready
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
FileSystemAdaptor.prototype.getTiddlerInfo = function(tiddler) {
|
FileSystemAdaptor.prototype.getTiddlerInfo = function(tiddler) {
|
||||||
//Returns the existing fileInfo for the tiddler. To regenerate, call getTiddlerFileInfo().
|
|
||||||
var title = tiddler.fields.title;
|
var title = tiddler.fields.title;
|
||||||
return this.boot.files[title];
|
return this.boot.files[title];
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Return a fileInfo object for a tiddler, creating it if necessary:
|
Find the dynamic store (if any) that a tiddler should be saved into.
|
||||||
filepath: the absolute path to the file containing the tiddler
|
Precedence: existing boot.files entry wins; otherwise first matching saveFilter.
|
||||||
type: the type of the tiddler file (NOT the type of the tiddler -- see below)
|
*/
|
||||||
hasMetaFile: true if the file also has a companion .meta file
|
FileSystemAdaptor.prototype.findDynamicStoreForTiddler = function(tiddler) {
|
||||||
|
var stores = this.boot.dynamicStores || [];
|
||||||
|
if(stores.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var title = tiddler.fields.title,
|
||||||
|
existing = this.boot.files[title];
|
||||||
|
if(existing && existing.dynamicStoreId) {
|
||||||
|
for(var i=0; i<stores.length; i++) {
|
||||||
|
if(stores[i].id === existing.dynamicStoreId) {
|
||||||
|
return stores[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for(var j=0; j<stores.length; j++) {
|
||||||
|
var store = stores[j];
|
||||||
|
if(store.saveFilter) {
|
||||||
|
var source = this.wiki.makeTiddlerIterator([title]),
|
||||||
|
result = this.wiki.filterTiddlers(store.saveFilter,null,source);
|
||||||
|
if(result.length > 0) {
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
The boot process populates this.boot.files for each of the tiddler files that it loads.
|
/*
|
||||||
The type is found by looking up the extension in $tw.config.fileExtensionInfo (eg "application/x-tiddler" for ".tid" files).
|
Return a fileInfo object for a tiddler, creating it if necessary.
|
||||||
|
|
||||||
It is the responsibility of the filesystem adaptor to update this.boot.files for new files that are created.
|
|
||||||
*/
|
*/
|
||||||
FileSystemAdaptor.prototype.getTiddlerFileInfo = function(tiddler,callback) {
|
FileSystemAdaptor.prototype.getTiddlerFileInfo = function(tiddler,callback) {
|
||||||
// Error if we don't have a this.boot.wikiTiddlersPath
|
|
||||||
if(!this.boot.wikiTiddlersPath) {
|
if(!this.boot.wikiTiddlersPath) {
|
||||||
return callback("filesystemadaptor requires a valid wiki folder");
|
return callback("filesystemadaptor requires a valid wiki folder");
|
||||||
}
|
}
|
||||||
// Always generate a fileInfo object when this fuction is called
|
|
||||||
var title = tiddler.fields.title, newInfo, pathFilters, extFilters,
|
var title = tiddler.fields.title, newInfo, pathFilters, extFilters,
|
||||||
fileInfo = this.boot.files[title];
|
fileInfo = this.boot.files[title],
|
||||||
|
store = this.findDynamicStoreForTiddler(tiddler),
|
||||||
|
directory = store ? store.directory : this.boot.wikiTiddlersPath;
|
||||||
if(this.wiki.tiddlerExists("$:/config/FileSystemPaths")) {
|
if(this.wiki.tiddlerExists("$:/config/FileSystemPaths")) {
|
||||||
pathFilters = this.wiki.getTiddlerText("$:/config/FileSystemPaths","").split("\n");
|
pathFilters = this.wiki.getTiddlerText("$:/config/FileSystemPaths","").split("\n");
|
||||||
}
|
}
|
||||||
@@ -63,12 +98,15 @@ FileSystemAdaptor.prototype.getTiddlerFileInfo = function(tiddler,callback) {
|
|||||||
extFilters = this.wiki.getTiddlerText("$:/config/FileSystemExtensions","").split("\n");
|
extFilters = this.wiki.getTiddlerText("$:/config/FileSystemExtensions","").split("\n");
|
||||||
}
|
}
|
||||||
newInfo = $tw.utils.generateTiddlerFileInfo(tiddler,{
|
newInfo = $tw.utils.generateTiddlerFileInfo(tiddler,{
|
||||||
directory: this.boot.wikiTiddlersPath,
|
directory: directory,
|
||||||
pathFilters: pathFilters,
|
pathFilters: pathFilters,
|
||||||
extFilters: extFilters,
|
extFilters: extFilters,
|
||||||
wiki: this.wiki,
|
wiki: this.wiki,
|
||||||
fileInfo: fileInfo
|
fileInfo: fileInfo
|
||||||
});
|
});
|
||||||
|
if(store) {
|
||||||
|
newInfo.dynamicStoreId = store.id;
|
||||||
|
}
|
||||||
callback(null,newInfo);
|
callback(null,newInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -83,6 +121,7 @@ FileSystemAdaptor.prototype.saveTiddler = function(tiddler,callback,options) {
|
|||||||
if(err) {
|
if(err) {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
|
var dynamicStoreId = fileInfo && fileInfo.dynamicStoreId || null;
|
||||||
$tw.utils.saveTiddlerToFile(tiddler,fileInfo,function(err,fileInfo) {
|
$tw.utils.saveTiddlerToFile(tiddler,fileInfo,function(err,fileInfo) {
|
||||||
if(err) {
|
if(err) {
|
||||||
if((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "open") {
|
if((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "open") {
|
||||||
@@ -95,6 +134,9 @@ FileSystemAdaptor.prototype.saveTiddler = function(tiddler,callback,options) {
|
|||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(dynamicStoreId && fileInfo) {
|
||||||
|
fileInfo.dynamicStoreId = dynamicStoreId;
|
||||||
|
}
|
||||||
// Store new boot info only after successful writes
|
// Store new boot info only after successful writes
|
||||||
self.boot.files[tiddler.fields.title] = fileInfo;
|
self.boot.files[tiddler.fields.title] = fileInfo;
|
||||||
// Cleanup duplicates if the file moved or changed extensions
|
// Cleanup duplicates if the file moved or changed extensions
|
||||||
@@ -116,9 +158,28 @@ FileSystemAdaptor.prototype.saveTiddler = function(tiddler,callback,options) {
|
|||||||
/*
|
/*
|
||||||
Load a tiddler and invoke the callback with (err,tiddlerFields)
|
Load a tiddler and invoke the callback with (err,tiddlerFields)
|
||||||
|
|
||||||
We don't need to implement loading for the file system adaptor, because all the tiddler files will have been loaded during the boot process.
|
Most tiddlers are pre-loaded at boot, but the syncer may ask us to load
|
||||||
|
individual tiddlers in response to watcher-driven out-of-band changes.
|
||||||
*/
|
*/
|
||||||
FileSystemAdaptor.prototype.loadTiddler = function(title,callback) {
|
FileSystemAdaptor.prototype.loadTiddler = function(title,callback) {
|
||||||
|
var fileInfo = this.boot.files[title];
|
||||||
|
if(!fileInfo || !fileInfo.dynamicStoreId || !fs.existsSync(fileInfo.filepath)) {
|
||||||
|
return callback(null,null);
|
||||||
|
}
|
||||||
|
var loaded;
|
||||||
|
try {
|
||||||
|
loaded = $tw.loadTiddlersFromFile(fileInfo.filepath,{});
|
||||||
|
} catch(e) {
|
||||||
|
return callback(e);
|
||||||
|
}
|
||||||
|
if(!loaded || !loaded.tiddlers) {
|
||||||
|
return callback(null,null);
|
||||||
|
}
|
||||||
|
for(var i=0; i<loaded.tiddlers.length; i++) {
|
||||||
|
if(loaded.tiddlers[i] && loaded.tiddlers[i].title === title) {
|
||||||
|
return callback(null,loaded.tiddlers[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
callback(null,null);
|
callback(null,null);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -128,19 +189,16 @@ Delete a tiddler and invoke the callback with (err)
|
|||||||
FileSystemAdaptor.prototype.deleteTiddler = function(title,callback,options) {
|
FileSystemAdaptor.prototype.deleteTiddler = function(title,callback,options) {
|
||||||
var self = this,
|
var self = this,
|
||||||
fileInfo = this.boot.files[title];
|
fileInfo = this.boot.files[title];
|
||||||
// Only delete the tiddler if we have writable information for the file
|
|
||||||
if(fileInfo) {
|
if(fileInfo) {
|
||||||
$tw.utils.deleteTiddlerFile(fileInfo,function(err,fileInfo) {
|
$tw.utils.deleteTiddlerFile(fileInfo,function(err,fileInfo) {
|
||||||
if(err) {
|
if(err) {
|
||||||
if((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "unlink") {
|
if((err.code == "EPERM" || err.code == "EACCES") && err.syscall == "unlink") {
|
||||||
// Error deleting the file on disk, should fail gracefully
|
|
||||||
$tw.syncer.displayError("Server desynchronized. Error deleting file for deleted tiddler \"" + title + "\"",err);
|
$tw.syncer.displayError("Server desynchronized. Error deleting file for deleted tiddler \"" + title + "\"",err);
|
||||||
return callback(null,fileInfo);
|
return callback(null,fileInfo);
|
||||||
} else {
|
} else {
|
||||||
return callback(err);
|
return callback(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Remove the tiddler from self.boot.files & return null adaptorInfo
|
|
||||||
self.removeTiddlerFileInfo(title);
|
self.removeTiddlerFileInfo(title);
|
||||||
return callback(null,null);
|
return callback(null,null);
|
||||||
});
|
});
|
||||||
@@ -153,10 +211,201 @@ FileSystemAdaptor.prototype.deleteTiddler = function(title,callback,options) {
|
|||||||
Delete a tiddler in cache, without modifying file system.
|
Delete a tiddler in cache, without modifying file system.
|
||||||
*/
|
*/
|
||||||
FileSystemAdaptor.prototype.removeTiddlerFileInfo = function(title) {
|
FileSystemAdaptor.prototype.removeTiddlerFileInfo = function(title) {
|
||||||
// Only delete the tiddler info if we have writable information for the file
|
|
||||||
if(this.boot.files[title]) {
|
if(this.boot.files[title]) {
|
||||||
delete this.boot.files[title];
|
delete this.boot.files[title];
|
||||||
};
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Syncer hook: return modifications/deletions that have occurred on disk
|
||||||
|
since the last poll.
|
||||||
|
*/
|
||||||
|
FileSystemAdaptor.prototype.getUpdatedTiddlers = function(syncer,callback) {
|
||||||
|
var modifications = Object.keys(this.modifications),
|
||||||
|
deletions = Object.keys(this.deletions);
|
||||||
|
this.modifications = Object.create(null);
|
||||||
|
this.deletions = Object.create(null);
|
||||||
|
callback(null,{modifications: modifications, deletions: deletions});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set up chokidar watchers for each registered dynamic store.
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
Close all watchers and clear any pending debounce timers. Returns a promise
|
||||||
|
that resolves once chokidar has fully shut down, for clean teardown in tests.
|
||||||
|
*/
|
||||||
|
FileSystemAdaptor.prototype.close = function() {
|
||||||
|
$tw.utils.each(this.pendingTimers,function(timer) { clearTimeout(timer); });
|
||||||
|
this.pendingTimers = Object.create(null);
|
||||||
|
var closes = (this.watchers || []).map(function(w) {
|
||||||
|
try { return w.close(); } catch(e) { return null; }
|
||||||
|
});
|
||||||
|
this.watchers = [];
|
||||||
|
return Promise.all(closes.filter(Boolean));
|
||||||
|
};
|
||||||
|
|
||||||
|
FileSystemAdaptor.prototype.setupWatchers = function() {
|
||||||
|
var self = this,
|
||||||
|
stores = (this.boot.dynamicStores || []).filter(function(s) { return s.watch; });
|
||||||
|
if(stores.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var chokidar;
|
||||||
|
try {
|
||||||
|
chokidar = require("chokidar");
|
||||||
|
} catch(e) {
|
||||||
|
this.logger.log("chokidar not available; dynamic store watching disabled",e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
stores.forEach(function(store) {
|
||||||
|
self.setupWatcher(chokidar,store);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
FileSystemAdaptor.prototype.setupWatcher = function(chokidar,store) {
|
||||||
|
var self = this,
|
||||||
|
fileRegExp = new RegExp(store.filesRegExp || "^.*$");
|
||||||
|
var watcher = chokidar.watch(store.directory,{
|
||||||
|
ignoreInitial: true,
|
||||||
|
persistent: true,
|
||||||
|
depth: store.searchSubdirectories ? undefined : 0,
|
||||||
|
awaitWriteFinish: {stabilityThreshold: 100, pollInterval: 50},
|
||||||
|
ignored: function(p) {
|
||||||
|
// chokidar invokes `ignored` for the root too — don't ignore the root
|
||||||
|
if(p === store.directory) return false;
|
||||||
|
var base = path.basename(p);
|
||||||
|
if(/\.meta$/.test(base)) return false;
|
||||||
|
// Allow directories through so recursion works when enabled
|
||||||
|
try {
|
||||||
|
if(fs.existsSync(p) && fs.statSync(p).isDirectory()) return false;
|
||||||
|
} catch(e) {}
|
||||||
|
return !fileRegExp.test(base);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
watcher.on("add",function(filepath) { self.scheduleFileEvent(store,filepath,"change"); });
|
||||||
|
watcher.on("change",function(filepath) { self.scheduleFileEvent(store,filepath,"change"); });
|
||||||
|
watcher.on("unlink",function(filepath) { self.scheduleFileEvent(store,filepath,"unlink"); });
|
||||||
|
watcher.on("error",function(err) {
|
||||||
|
self.logger.log("chokidar error for " + store.directory,err && err.message);
|
||||||
|
});
|
||||||
|
this.watchers.push(watcher);
|
||||||
|
};
|
||||||
|
|
||||||
|
FileSystemAdaptor.prototype.scheduleFileEvent = function(store,filepath,eventType) {
|
||||||
|
var self = this,
|
||||||
|
key = filepath,
|
||||||
|
delay = store.debounce || 400;
|
||||||
|
// A .meta change should trigger re-read of its companion file
|
||||||
|
var targetPath = filepath;
|
||||||
|
if(/\.meta$/.test(filepath)) {
|
||||||
|
targetPath = filepath.replace(/\.meta$/,"");
|
||||||
|
}
|
||||||
|
if(this.pendingTimers[key]) {
|
||||||
|
clearTimeout(this.pendingTimers[key]);
|
||||||
|
}
|
||||||
|
var timer = setTimeout(function() {
|
||||||
|
delete self.pendingTimers[key];
|
||||||
|
try {
|
||||||
|
self.processFileEvent(store,targetPath,eventType);
|
||||||
|
} catch(e) {
|
||||||
|
self.logger.log("Error processing file event for " + targetPath,e.message);
|
||||||
|
}
|
||||||
|
},delay);
|
||||||
|
if(timer && typeof timer.unref === "function") {
|
||||||
|
timer.unref();
|
||||||
|
}
|
||||||
|
this.pendingTimers[key] = timer;
|
||||||
|
};
|
||||||
|
|
||||||
|
FileSystemAdaptor.prototype.processFileEvent = function(store,filepath,eventType) {
|
||||||
|
var self = this;
|
||||||
|
// Deletion: look up any titles that mapped to this filepath and queue deletion.
|
||||||
|
// Do NOT call wiki.deleteTiddler here — the syncer's SyncFromServerTask does that.
|
||||||
|
if(eventType === "unlink" || !fs.existsSync(filepath)) {
|
||||||
|
var deletedTitles = [];
|
||||||
|
$tw.utils.each(this.boot.files,function(info,title) {
|
||||||
|
if(info && info.filepath === filepath) {
|
||||||
|
deletedTitles.push(title);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
deletedTitles.forEach(function(title) {
|
||||||
|
delete self.boot.files[title];
|
||||||
|
self.deletions[title] = true;
|
||||||
|
delete self.modifications[title];
|
||||||
|
});
|
||||||
|
if(deletedTitles.length > 0) {
|
||||||
|
this.logger.log("Dynamic store: detected removal of " + deletedTitles.length + " tiddler(s) at " + filepath);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Add/change: re-parse the file and queue modifications
|
||||||
|
var loaded;
|
||||||
|
try {
|
||||||
|
loaded = $tw.loadTiddlersFromFile(filepath,{});
|
||||||
|
} catch(e) {
|
||||||
|
this.logger.log("Failed to load tiddler file " + filepath,e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(!loaded || !loaded.tiddlers) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var newTitles = {};
|
||||||
|
loaded.tiddlers.forEach(function(fields) {
|
||||||
|
if(!fields || !fields.title) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(fields.type === "application/javascript" && fields["module-type"]) {
|
||||||
|
self.logger.log("Skipping hot-reload of JS module tiddler " + fields.title + " (requires a restart)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var title = fields.title;
|
||||||
|
newTitles[title] = true;
|
||||||
|
// Ensure boot.files tracks the file so loadTiddler can find it on demand
|
||||||
|
self.boot.files[title] = {
|
||||||
|
filepath: loaded.filepath,
|
||||||
|
type: loaded.type,
|
||||||
|
hasMetaFile: loaded.hasMetaFile,
|
||||||
|
isEditableFile: true,
|
||||||
|
dynamicStoreId: store.id
|
||||||
|
};
|
||||||
|
// Diff against the current wiki tiddler to suppress self-write echoes
|
||||||
|
var existing = self.wiki.getTiddler(title);
|
||||||
|
if(existing && self.tiddlerFieldsEqual(existing.fields,fields)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.modifications[title] = true;
|
||||||
|
delete self.deletions[title];
|
||||||
|
});
|
||||||
|
// Handle tiddlers that were previously in this file but have now disappeared
|
||||||
|
$tw.utils.each(this.boot.files,function(info,title) {
|
||||||
|
if(info && info.filepath === filepath && !newTitles[title]) {
|
||||||
|
delete self.boot.files[title];
|
||||||
|
self.deletions[title] = true;
|
||||||
|
delete self.modifications[title];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
FileSystemAdaptor.prototype.tiddlerFieldsEqual = function(existingFields,newFields) {
|
||||||
|
// Ignore volatile fields that the syncer / server may add
|
||||||
|
var ignore = {revision: 1, bag: 1};
|
||||||
|
var keys = {};
|
||||||
|
$tw.utils.each(existingFields,function(v,k) { if(!ignore[k]) keys[k] = true; });
|
||||||
|
$tw.utils.each(newFields,function(v,k) { if(!ignore[k]) keys[k] = true; });
|
||||||
|
for(var k in keys) {
|
||||||
|
var a = existingFields[k],
|
||||||
|
b = newFields[k];
|
||||||
|
// Normalise arrays to string form
|
||||||
|
if($tw.utils.isArray(a)) a = $tw.utils.stringifyList(a);
|
||||||
|
if($tw.utils.isArray(b)) b = $tw.utils.stringifyList(b);
|
||||||
|
if(a instanceof Date) a = $tw.utils.stringifyDate(a);
|
||||||
|
if(b instanceof Date) b = $tw.utils.stringifyDate(b);
|
||||||
|
if((a === undefined ? "" : String(a)) !== (b === undefined ? "" : String(b))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if(fs) {
|
if(fs) {
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/plugins/tiddlywiki/markdown/frontmatter-deserializer.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: tiddlerdeserializer
|
||||||
|
|
||||||
|
Markdown deserializer with YAML frontmatter extraction.
|
||||||
|
|
||||||
|
Parses YAML frontmatter delimited by `---` markers and maps extracted
|
||||||
|
values to tiddler fields. Array values on list fields (tags, list, any
|
||||||
|
field with a registered `stringify` method) are converted to TiddlyWiki
|
||||||
|
bracketed lists. Non-string, non-array values are stored as their JSON
|
||||||
|
representation.
|
||||||
|
|
||||||
|
`created` and `modified` in the frontmatter are accepted in either
|
||||||
|
TiddlyWiki's native `YYYYMMDDHHMMSSmmm` UTC format or any ISO-8601
|
||||||
|
string that `Date()` can parse; both are normalised to TW's native
|
||||||
|
format. Values that cannot be parsed are dropped.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var yaml = require("$:/plugins/tiddlywiki/markdown/yaml.js");
|
||||||
|
|
||||||
|
function deserialize(text,fields) {
|
||||||
|
var result = Object.create(null),
|
||||||
|
body = text,
|
||||||
|
frontmatter = null;
|
||||||
|
// Copy incoming fields (e.g. from .meta file or filename)
|
||||||
|
for(var f in fields) {
|
||||||
|
result[f] = fields[f];
|
||||||
|
}
|
||||||
|
// Extract YAML frontmatter if present
|
||||||
|
if(text.indexOf("---") === 0) {
|
||||||
|
var endMarker = text.indexOf("\n---",3);
|
||||||
|
if(endMarker !== -1) {
|
||||||
|
var yamlText = text.substring(3,endMarker).trim();
|
||||||
|
// Body starts after the closing --- and its newline
|
||||||
|
var afterMarker = endMarker + 4;
|
||||||
|
if(text[afterMarker] === "\n") {
|
||||||
|
afterMarker++;
|
||||||
|
} else if(text[afterMarker] === "\r" && text[afterMarker + 1] === "\n") {
|
||||||
|
afterMarker += 2;
|
||||||
|
}
|
||||||
|
// Skip one blank line if present (conventional separator between frontmatter and body)
|
||||||
|
if(text[afterMarker] === "\n") {
|
||||||
|
afterMarker++;
|
||||||
|
} else if(text[afterMarker] === "\r" && text[afterMarker + 1] === "\n") {
|
||||||
|
afterMarker += 2;
|
||||||
|
}
|
||||||
|
body = text.substring(afterMarker);
|
||||||
|
try {
|
||||||
|
frontmatter = yaml.load(yamlText);
|
||||||
|
} catch(e) {
|
||||||
|
// If YAML parsing fails, treat the whole text as body
|
||||||
|
body = text;
|
||||||
|
frontmatter = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Map frontmatter fields to tiddler fields
|
||||||
|
if(frontmatter && typeof frontmatter === "object" && !Array.isArray(frontmatter)) {
|
||||||
|
var keys = Object.keys(frontmatter);
|
||||||
|
for(var i = 0; i < keys.length; i++) {
|
||||||
|
var key = keys[i],
|
||||||
|
value = frontmatter[key];
|
||||||
|
// Apply field collision policy
|
||||||
|
if(key === "created" || key === "modified") {
|
||||||
|
var normalised = normaliseDate(value);
|
||||||
|
if(normalised !== null) {
|
||||||
|
result[key] = normalised;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(key === "tags" && result[key]) {
|
||||||
|
// Merge: parse existing tags, add new ones
|
||||||
|
result[key] = mergeTagValue(result[key],value);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
result[key] = fieldValueToString(key,value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.text = body;
|
||||||
|
if(!result.type) {
|
||||||
|
result.type = "text/x-markdown";
|
||||||
|
}
|
||||||
|
return [result];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register under both types — text/x-markdown is the deserializer type
|
||||||
|
// registered for .md file extensions; text/markdown is the raw content type.
|
||||||
|
exports["text/x-markdown"] = deserialize;
|
||||||
|
exports["text/markdown"] = deserialize;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert a parsed YAML value to a tiddler field string.
|
||||||
|
- Arrays on list fields (tags, list, etc.) → TW bracketed list format
|
||||||
|
- Strings → as-is
|
||||||
|
- Everything else → JSON
|
||||||
|
*/
|
||||||
|
function fieldValueToString(key,value) {
|
||||||
|
if(value === null || value === undefined) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if(typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if(Array.isArray(value)) {
|
||||||
|
// Check if this field has a stringify method (i.e. it's a list field)
|
||||||
|
if($tw.Tiddler.fieldModules[key] && $tw.Tiddler.fieldModules[key].stringify) {
|
||||||
|
var stringItems = [];
|
||||||
|
for(var i = 0; i < value.length; i++) {
|
||||||
|
stringItems.push(value[i] == null ? "" : String(value[i]));
|
||||||
|
}
|
||||||
|
return $tw.utils.stringifyList(stringItems);
|
||||||
|
}
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
if(typeof value === "object") {
|
||||||
|
return JSON.stringify(value);
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Normalise a frontmatter date value to TiddlyWiki's YYYYMMDDHHMMSSmmm UTC
|
||||||
|
format. Accepts TW native strings (14 or 17 digits, optional leading "-"
|
||||||
|
for negative years) and anything `Date()` can parse (ISO 8601, RFC 2822,
|
||||||
|
Date objects). Returns null if the value cannot be interpreted as a date.
|
||||||
|
*/
|
||||||
|
function normaliseDate(value) {
|
||||||
|
if(value === null || value === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(typeof value === "string") {
|
||||||
|
if(/^-?\d{14}$/.test(value)) {
|
||||||
|
return value + "000";
|
||||||
|
}
|
||||||
|
if(/^-?\d{17}$/.test(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
var d = new Date(value);
|
||||||
|
if(!isNaN(d.getTime())) {
|
||||||
|
return $tw.utils.stringifyDate(d);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(value instanceof Date && !isNaN(value.getTime())) {
|
||||||
|
return $tw.utils.stringifyDate(value);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Merge incoming tag value with existing tags string.
|
||||||
|
The incoming value may be a string (TW bracketed list) or an array (from YAML).
|
||||||
|
*/
|
||||||
|
function mergeTagValue(existing,incoming) {
|
||||||
|
var existingTags = $tw.utils.parseStringArray(existing) || [];
|
||||||
|
var newTags;
|
||||||
|
if(Array.isArray(incoming)) {
|
||||||
|
newTags = incoming.map(function(t) { return t == null ? "" : String(t); });
|
||||||
|
} else if(typeof incoming === "string") {
|
||||||
|
newTags = $tw.utils.parseStringArray(incoming) || [];
|
||||||
|
} else {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
var seen = Object.create(null);
|
||||||
|
for(var i = 0; i < existingTags.length; i++) {
|
||||||
|
seen[existingTags[i]] = true;
|
||||||
|
}
|
||||||
|
for(var j = 0; j < newTags.length; j++) {
|
||||||
|
if(!seen[newTags[j]]) {
|
||||||
|
existingTags.push(newTags[j]);
|
||||||
|
seen[newTags[j]] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $tw.utils.stringifyList(existingTags);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/plugins/tiddlywiki/markdown/frontmatter-serializer.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: tiddlerserializer
|
||||||
|
|
||||||
|
Markdown serializer with YAML frontmatter.
|
||||||
|
|
||||||
|
Inverse of `frontmatter-deserializer.js`. Given a tiddler, returns a
|
||||||
|
Markdown file body whose first lines are a YAML frontmatter block
|
||||||
|
(`---` … `---`), followed by the tiddler's `text` field.
|
||||||
|
|
||||||
|
Field handling:
|
||||||
|
- `title` is always emitted (frontmatter wins over filename when reloaded).
|
||||||
|
- `text` is the body; not emitted in the frontmatter.
|
||||||
|
- `created`, `modified` are emitted as ISO-8601 strings (symmetric with
|
||||||
|
the deserializer, which accepts either ISO-8601 or TW's native format).
|
||||||
|
- `type` is skipped when it equals `text/x-markdown` (the default for `.md` files).
|
||||||
|
- `bag`, `revision` are skipped (sync metadata, not authored content).
|
||||||
|
- List fields (those with a registered `stringify` method) are emitted as YAML arrays.
|
||||||
|
- All other fields are emitted as YAML strings (preserving their on-disk type).
|
||||||
|
|
||||||
|
\*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var yaml = require("$:/plugins/tiddlywiki/markdown/yaml.js");
|
||||||
|
|
||||||
|
// Field names to skip when emitting frontmatter
|
||||||
|
var SKIP_FIELDS = {
|
||||||
|
text: true,
|
||||||
|
bag: true,
|
||||||
|
revision: true
|
||||||
|
};
|
||||||
|
|
||||||
|
function serialize(tiddler) {
|
||||||
|
if(!tiddler) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
var fields = tiddler.fields || {},
|
||||||
|
frontmatter = Object.create(null);
|
||||||
|
// Always include title first
|
||||||
|
if(fields.title) {
|
||||||
|
frontmatter.title = fields.title;
|
||||||
|
}
|
||||||
|
// Add other fields
|
||||||
|
$tw.utils.each(fields,function(value,name) {
|
||||||
|
if(SKIP_FIELDS[name] || name === "title") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(name === "type" && value === "text/x-markdown") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if(name === "created" || name === "modified") {
|
||||||
|
var iso = toIsoDate(value);
|
||||||
|
if(iso) {
|
||||||
|
frontmatter[name] = iso;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// List fields → YAML arrays
|
||||||
|
if($tw.Tiddler.fieldModules[name] && $tw.Tiddler.fieldModules[name].stringify) {
|
||||||
|
var items;
|
||||||
|
if(Array.isArray(value)) {
|
||||||
|
items = value.slice();
|
||||||
|
} else {
|
||||||
|
items = $tw.utils.parseStringArray(value || "") || [];
|
||||||
|
}
|
||||||
|
frontmatter[name] = items;
|
||||||
|
} else if(typeof value === "string") {
|
||||||
|
frontmatter[name] = value;
|
||||||
|
} else {
|
||||||
|
// Fallback: stringify whatever it is
|
||||||
|
frontmatter[name] = String(value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
var body = fields.text || "";
|
||||||
|
var hasFrontmatter = Object.keys(frontmatter).length > 0;
|
||||||
|
if(!hasFrontmatter) {
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
return "---\n" + yaml.dump(frontmatter) + "\n---\n\n" + body;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Convert a TiddlyWiki date field value to an ISO-8601 string. Accepts a
|
||||||
|
native `YYYYMMDDHHMMSSmmm` string or a Date. Returns null if the value
|
||||||
|
cannot be parsed.
|
||||||
|
*/
|
||||||
|
function toIsoDate(value) {
|
||||||
|
if(!value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var d;
|
||||||
|
if($tw.utils.isDate(value)) {
|
||||||
|
d = value;
|
||||||
|
} else {
|
||||||
|
d = $tw.utils.parseDate(String(value));
|
||||||
|
}
|
||||||
|
if(d && !isNaN(d.getTime())) {
|
||||||
|
return d.toISOString();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register under both types — text/markdown is what the "New Markdown" button
|
||||||
|
// sets; text/x-markdown is what the deserializer uses after content-type
|
||||||
|
// resolution for .md files loaded from disk.
|
||||||
|
exports["text/x-markdown"] = serialize;
|
||||||
|
exports["text/markdown"] = serialize;
|
||||||
@@ -0,0 +1,473 @@
|
|||||||
|
/*\
|
||||||
|
title: $:/plugins/tiddlywiki/markdown/yaml.js
|
||||||
|
type: application/javascript
|
||||||
|
module-type: library
|
||||||
|
|
||||||
|
Minimal YAML parser for frontmatter extraction.
|
||||||
|
API-compatible subset of js-yaml: load(string) → object, dump(object) → string.
|
||||||
|
Handles scalars, flow/block arrays, and simple nested maps.
|
||||||
|
|
||||||
|
\*/
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
function YAMLException(message, mark) {
|
||||||
|
this.name = "YAMLException";
|
||||||
|
this.message = message;
|
||||||
|
this.mark = mark || null;
|
||||||
|
}
|
||||||
|
YAMLException.prototype = Object.create(Error.prototype);
|
||||||
|
YAMLException.prototype.constructor = YAMLException;
|
||||||
|
|
||||||
|
// -- Scalar parsing --
|
||||||
|
|
||||||
|
function parseScalar(raw) {
|
||||||
|
if(raw === "" || raw === "null" || raw === "Null" || raw === "NULL" || raw === "~") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if(raw === "true" || raw === "True" || raw === "TRUE") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if(raw === "false" || raw === "False" || raw === "FALSE") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Quoted strings
|
||||||
|
if((raw[0] === '"' && raw[raw.length - 1] === '"') ||
|
||||||
|
(raw[0] === "'" && raw[raw.length - 1] === "'")) {
|
||||||
|
var inner = raw.slice(1, -1);
|
||||||
|
if(raw[0] === '"') {
|
||||||
|
// Handle basic escape sequences in double-quoted strings.
|
||||||
|
// Use a single pass so each escape consumes its backslash before
|
||||||
|
// later replacements can re-interpret it (e.g. "\\n" must become
|
||||||
|
// backslash + n, not backslash + newline).
|
||||||
|
inner = inner.replace(/\\(.)/g, function(_, c) {
|
||||||
|
switch(c) {
|
||||||
|
case "n": return "\n";
|
||||||
|
case "t": return "\t";
|
||||||
|
case "r": return "\r";
|
||||||
|
case "\\": return "\\";
|
||||||
|
case '"': return '"';
|
||||||
|
default: return c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
// Numbers: integers and floats
|
||||||
|
if(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(raw)) {
|
||||||
|
var num = Number(raw);
|
||||||
|
if(!isNaN(num)) {
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Hex integers
|
||||||
|
if(/^0x[0-9a-fA-F]+$/.test(raw)) {
|
||||||
|
return parseInt(raw, 16);
|
||||||
|
}
|
||||||
|
// Octal integers
|
||||||
|
if(/^0o[0-7]+$/.test(raw)) {
|
||||||
|
return parseInt(raw.slice(2), 8);
|
||||||
|
}
|
||||||
|
// Special floats
|
||||||
|
if(raw === ".inf" || raw === ".Inf" || raw === ".INF") {
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
if(raw === "-.inf" || raw === "-.Inf" || raw === "-.INF") {
|
||||||
|
return -Infinity;
|
||||||
|
}
|
||||||
|
if(raw === ".nan" || raw === ".NaN" || raw === ".NAN") {
|
||||||
|
return NaN;
|
||||||
|
}
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Flow sequence parser: [item, item, ...] --
|
||||||
|
|
||||||
|
function parseFlowSequence(str) {
|
||||||
|
// Strip outer brackets and split respecting nested brackets and quotes
|
||||||
|
var inner = str.slice(1, -1).trim();
|
||||||
|
if(inner === "") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
var items = [],
|
||||||
|
current = "",
|
||||||
|
depth = 0,
|
||||||
|
inSingle = false,
|
||||||
|
inDouble = false;
|
||||||
|
for(var i = 0; i < inner.length; i++) {
|
||||||
|
var ch = inner[i];
|
||||||
|
if(ch === "\\" && inDouble) {
|
||||||
|
current += ch + (inner[i + 1] || "");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(ch === '"' && !inSingle) {
|
||||||
|
inDouble = !inDouble;
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(ch === "'" && !inDouble) {
|
||||||
|
inSingle = !inSingle;
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!inSingle && !inDouble) {
|
||||||
|
if(ch === "[" || ch === "{") {
|
||||||
|
depth++;
|
||||||
|
} else if(ch === "]" || ch === "}") {
|
||||||
|
depth--;
|
||||||
|
} else if(ch === "," && depth === 0) {
|
||||||
|
items.push(parseScalar(current.trim()));
|
||||||
|
current = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
if(current.trim() !== "") {
|
||||||
|
items.push(parseScalar(current.trim()));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Flow mapping parser: {key: value, ...} --
|
||||||
|
|
||||||
|
function parseFlowMapping(str) {
|
||||||
|
var inner = str.slice(1, -1).trim();
|
||||||
|
if(inner === "") {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
var result = Object.create(null),
|
||||||
|
pairs = [],
|
||||||
|
current = "",
|
||||||
|
depth = 0,
|
||||||
|
inSingle = false,
|
||||||
|
inDouble = false;
|
||||||
|
for(var i = 0; i < inner.length; i++) {
|
||||||
|
var ch = inner[i];
|
||||||
|
if(ch === "\\" && inDouble) {
|
||||||
|
current += ch + (inner[i + 1] || "");
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(ch === '"' && !inSingle) {
|
||||||
|
inDouble = !inDouble;
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(ch === "'" && !inDouble) {
|
||||||
|
inSingle = !inSingle;
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if(!inSingle && !inDouble) {
|
||||||
|
if(ch === "[" || ch === "{") {
|
||||||
|
depth++;
|
||||||
|
} else if(ch === "]" || ch === "}") {
|
||||||
|
depth--;
|
||||||
|
} else if(ch === "," && depth === 0) {
|
||||||
|
pairs.push(current.trim());
|
||||||
|
current = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
if(current.trim() !== "") {
|
||||||
|
pairs.push(current.trim());
|
||||||
|
}
|
||||||
|
for(var p = 0; p < pairs.length; p++) {
|
||||||
|
var colonIdx = pairs[p].indexOf(":");
|
||||||
|
if(colonIdx !== -1) {
|
||||||
|
var key = pairs[p].slice(0, colonIdx).trim(),
|
||||||
|
val = pairs[p].slice(colonIdx + 1).trim();
|
||||||
|
result[parseScalar(key)] = parseScalar(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Block parser (indentation-based) --
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse block YAML from an array of {indent, raw} line objects.
|
||||||
|
Returns the parsed value (object, array, or scalar).
|
||||||
|
*/
|
||||||
|
function parseBlock(lines, start, baseIndent) {
|
||||||
|
if(start >= lines.length) {
|
||||||
|
return {value: null, nextIndex: start};
|
||||||
|
}
|
||||||
|
var firstLine = lines[start];
|
||||||
|
// Block sequence: lines starting with "- "
|
||||||
|
if(firstLine.raw.indexOf("- ") === 0 || firstLine.raw === "-") {
|
||||||
|
return parseBlockSequence(lines, start, firstLine.indent);
|
||||||
|
}
|
||||||
|
// Block mapping: lines containing ":"
|
||||||
|
if(firstLine.raw.indexOf(":") !== -1) {
|
||||||
|
return parseBlockMapping(lines, start, firstLine.indent);
|
||||||
|
}
|
||||||
|
// Bare scalar
|
||||||
|
return {value: parseScalar(firstLine.raw), nextIndex: start + 1};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlockSequence(lines, start, seqIndent) {
|
||||||
|
var result = [],
|
||||||
|
i = start;
|
||||||
|
while(i < lines.length && lines[i].indent === seqIndent && (lines[i].raw.indexOf("- ") === 0 || lines[i].raw === "-")) {
|
||||||
|
var itemRaw = lines[i].raw.slice(2); // After "- "
|
||||||
|
// Check for inline flow value
|
||||||
|
var trimmed = itemRaw.trim();
|
||||||
|
if(trimmed[0] === "[") {
|
||||||
|
result.push(parseFlowSequence(trimmed));
|
||||||
|
i++;
|
||||||
|
} else if(trimmed[0] === "{") {
|
||||||
|
result.push(parseFlowMapping(trimmed));
|
||||||
|
i++;
|
||||||
|
} else if(trimmed === "" || trimmed === undefined) {
|
||||||
|
// Multi-line block item — collect indented children
|
||||||
|
i++;
|
||||||
|
var childLines = [];
|
||||||
|
while(i < lines.length && lines[i].indent > seqIndent) {
|
||||||
|
childLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if(childLines.length > 0) {
|
||||||
|
var parsed = parseBlock(childLines, 0, childLines[0].indent);
|
||||||
|
result.push(parsed.value);
|
||||||
|
} else {
|
||||||
|
result.push(null);
|
||||||
|
}
|
||||||
|
} else if(trimmed.indexOf(":") !== -1 && !isQuotedColonValue(trimmed)) {
|
||||||
|
// Inline mapping start as sequence item
|
||||||
|
// Collect this line (re-indented) plus any deeper-indented children
|
||||||
|
var mappingLines = [{indent: seqIndent + 2, raw: trimmed}];
|
||||||
|
i++;
|
||||||
|
while(i < lines.length && lines[i].indent > seqIndent) {
|
||||||
|
mappingLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
var parsedMap = parseBlock(mappingLines, 0, mappingLines[0].indent);
|
||||||
|
result.push(parsedMap.value);
|
||||||
|
} else {
|
||||||
|
result.push(parseScalar(trimmed));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {value: result, nextIndex: i};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isQuotedColonValue(str) {
|
||||||
|
// Check if the colon is inside quotes (meaning it's a scalar, not a mapping)
|
||||||
|
var colonIdx = str.indexOf(":");
|
||||||
|
if(colonIdx === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If the value starts with a quote and the colon is inside, it's a quoted scalar
|
||||||
|
if((str[0] === '"' || str[0] === "'") && colonIdx > 0) {
|
||||||
|
var quote = str[0];
|
||||||
|
var closeIdx = str.indexOf(quote, 1);
|
||||||
|
if(closeIdx > colonIdx) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlockMapping(lines, start, mapIndent) {
|
||||||
|
var result = Object.create(null),
|
||||||
|
i = start;
|
||||||
|
while(i < lines.length && lines[i].indent === mapIndent) {
|
||||||
|
var line = lines[i].raw,
|
||||||
|
colonIdx = line.indexOf(":");
|
||||||
|
if(colonIdx === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var key = line.slice(0, colonIdx).trim(),
|
||||||
|
valRaw = line.slice(colonIdx + 1).trim();
|
||||||
|
if(valRaw !== "") {
|
||||||
|
// Inline value
|
||||||
|
if(valRaw[0] === "[") {
|
||||||
|
result[key] = parseFlowSequence(valRaw);
|
||||||
|
} else if(valRaw[0] === "{") {
|
||||||
|
result[key] = parseFlowMapping(valRaw);
|
||||||
|
} else {
|
||||||
|
result[key] = parseScalar(valRaw);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else {
|
||||||
|
// Block value on subsequent indented lines
|
||||||
|
i++;
|
||||||
|
var childLines = [];
|
||||||
|
while(i < lines.length && lines[i].indent > mapIndent) {
|
||||||
|
childLines.push(lines[i]);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
if(childLines.length > 0) {
|
||||||
|
var parsed = parseBlock(childLines, 0, childLines[0].indent);
|
||||||
|
result[key] = parsed.value;
|
||||||
|
} else {
|
||||||
|
result[key] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {value: result, nextIndex: i};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Main API --
|
||||||
|
|
||||||
|
/*
|
||||||
|
Parse a YAML string into a JavaScript value.
|
||||||
|
Compatible with js-yaml's load() function.
|
||||||
|
Handles the subset of YAML used in frontmatter:
|
||||||
|
scalars, flow/block arrays, flow/block mappings, nested maps.
|
||||||
|
*/
|
||||||
|
function load(text) {
|
||||||
|
if(typeof text !== "string") {
|
||||||
|
throw new YAMLException("Input must be a string");
|
||||||
|
}
|
||||||
|
text = text.trim();
|
||||||
|
if(text === "") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Tokenise into lines with indent tracking
|
||||||
|
var rawLines = text.split(/\r?\n/),
|
||||||
|
lines = [];
|
||||||
|
for(var i = 0; i < rawLines.length; i++) {
|
||||||
|
var raw = rawLines[i];
|
||||||
|
// Skip blank lines and comment-only lines
|
||||||
|
var trimmed = raw.trim();
|
||||||
|
if(trimmed === "" || trimmed[0] === "#") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var indent = 0;
|
||||||
|
while(indent < raw.length && raw[indent] === " ") {
|
||||||
|
indent++;
|
||||||
|
}
|
||||||
|
lines.push({indent: indent, raw: trimmed});
|
||||||
|
}
|
||||||
|
if(lines.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// Single-line flow values
|
||||||
|
if(lines.length === 1) {
|
||||||
|
var single = lines[0].raw;
|
||||||
|
if(single[0] === "[") {
|
||||||
|
return parseFlowSequence(single);
|
||||||
|
}
|
||||||
|
if(single[0] === "{") {
|
||||||
|
return parseFlowMapping(single);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var parsed = parseBlock(lines, 0, lines[0].indent);
|
||||||
|
return parsed.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Serialise a JavaScript value to a YAML string.
|
||||||
|
Compatible with js-yaml's dump() function.
|
||||||
|
Handles the subset of YAML used in frontmatter.
|
||||||
|
*/
|
||||||
|
function dump(obj, options) {
|
||||||
|
options = options || {};
|
||||||
|
var indent = options.indent || 2;
|
||||||
|
return dumpValue(obj, 0, indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dumpValue(val, level, indentSize) {
|
||||||
|
if(val === null || val === undefined) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
if(typeof val === "boolean") {
|
||||||
|
return val ? "true" : "false";
|
||||||
|
}
|
||||||
|
if(typeof val === "number") {
|
||||||
|
if(val !== val) { return ".nan"; }
|
||||||
|
if(val === Infinity) { return ".inf"; }
|
||||||
|
if(val === -Infinity) { return "-.inf"; }
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
if(typeof val === "string") {
|
||||||
|
return dumpString(val);
|
||||||
|
}
|
||||||
|
if(Array.isArray(val)) {
|
||||||
|
return dumpArray(val, level, indentSize);
|
||||||
|
}
|
||||||
|
if(typeof val === "object") {
|
||||||
|
return dumpObject(val, level, indentSize);
|
||||||
|
}
|
||||||
|
return String(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dumpString(str) {
|
||||||
|
// Use plain style if safe, otherwise double-quote
|
||||||
|
if(str === "") {
|
||||||
|
return "''";
|
||||||
|
}
|
||||||
|
if(/^[\w][\w\s\-\.\/]*$/.test(str) &&
|
||||||
|
str !== "true" && str !== "false" && str !== "null" &&
|
||||||
|
str !== "True" && str !== "False" && str !== "Null" &&
|
||||||
|
str !== "TRUE" && str !== "FALSE" && str !== "NULL" &&
|
||||||
|
!/^-?\d/.test(str)) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
// Double-quote with escaping
|
||||||
|
return '"' + str.replace(/\\/g, "\\\\")
|
||||||
|
.replace(/"/g, '\\"')
|
||||||
|
.replace(/\n/g, "\\n")
|
||||||
|
.replace(/\r/g, "\\r")
|
||||||
|
.replace(/\t/g, "\\t") + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
function dumpArray(arr, level, indentSize) {
|
||||||
|
if(arr.length === 0) {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
var prefix = repeat(" ", level * indentSize);
|
||||||
|
var lines = [];
|
||||||
|
for(var i = 0; i < arr.length; i++) {
|
||||||
|
var val = dumpValue(arr[i], level + 1, indentSize);
|
||||||
|
if(typeof arr[i] === "object" && arr[i] !== null && !Array.isArray(arr[i])) {
|
||||||
|
// Object items: first key on same line as dash, rest indented
|
||||||
|
var objLines = val.split("\n");
|
||||||
|
lines.push(prefix + "- " + objLines[0]);
|
||||||
|
for(var j = 1; j < objLines.length; j++) {
|
||||||
|
lines.push(prefix + " " + objLines[j]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lines.push(prefix + "- " + val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "\n" + lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function dumpObject(obj, level, indentSize) {
|
||||||
|
var keys = Object.keys(obj);
|
||||||
|
if(keys.length === 0) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
var prefix = repeat(" ", level * indentSize);
|
||||||
|
var lines = [];
|
||||||
|
for(var i = 0; i < keys.length; i++) {
|
||||||
|
var key = keys[i],
|
||||||
|
val = obj[key];
|
||||||
|
var dumpedVal = dumpValue(val, level + 1, indentSize);
|
||||||
|
if((typeof val === "object" && val !== null) &&
|
||||||
|
((Array.isArray(val) && val.length > 0) || (!Array.isArray(val) && Object.keys(val).length > 0))) {
|
||||||
|
lines.push(prefix + dumpString(key) + ":" + dumpedVal);
|
||||||
|
} else {
|
||||||
|
lines.push(prefix + dumpString(key) + ": " + dumpedVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function repeat(str, count) {
|
||||||
|
var result = "";
|
||||||
|
for(var i = 0; i < count; i++) {
|
||||||
|
result += str;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.load = load;
|
||||||
|
exports.dump = dump;
|
||||||
|
exports.YAMLException = YAMLException;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<h1 class="">Welcome</h1><p>Welcome to <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a>, a non-linear personal web notebook that anyone can use and keep forever, independently of any corporation.</p><p>TiddlyWiki is a complete interactive wiki in JavaScript. It can be used as a single HTML file in the browser or as a powerful Node.js application. It is highly customisable: the entire user interface is itself implemented in hackable <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/WikiText.html">WikiText</a>.</p><h2 class="">Demo</h2><p>Learn more and see it in action at <a class="tc-tiddlylink-external" href="https://tiddlywiki.com/" rel="noopener noreferrer" target="_blank">https://tiddlywiki.com/</a></p><h2 class="">Developer Documentation</h2><p>Developer documentation is in progress at <a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev/" rel="noopener noreferrer" target="_blank">https://tiddlywiki.com/dev/</a></p><h2 class="">Pull Request Previews</h2><p>Pull request previews courtesy of <a class="tc-tiddlylink-external" href="https://netlify.com" rel="noopener noreferrer" target="_blank">Netlify</a></p><p><a href="https://www.netlify.com" rel="noopener noreferrer" target="_blank"><img alt="Deploys by Netlify" src="https://www.netlify.com/v3/img/components/netlify-light.svg"></a></p><h1 class="">Join the Community</h1><p>
|
<h1 class="">Welcome</h1><p>Welcome to <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a>, a non-linear personal web notebook that anyone can use and keep forever, independently of any corporation.</p><p>TiddlyWiki is a complete interactive wiki in JavaScript. It can be used as a single HTML file in the browser or as a powerful Node.js application. It is highly customisable: the entire user interface is itself implemented in hackable <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/WikiText.html">WikiText</a>.</p><h2 class="">Demo</h2><p>Learn more and see it in action at <a class="tc-tiddlylink-external" href="https://tiddlywiki.com/" rel="noopener noreferrer" target="_blank">https://tiddlywiki.com/</a></p><h2 class="">Developer Documentation</h2><p>Developer documentation is in progress at <a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev/" rel="noopener noreferrer" target="_blank">https://tiddlywiki.com/dev/</a></p><h2 class="">Pull Request Previews</h2><p>Pull request previews courtesy of <a class="tc-tiddlylink-external" href="https://netlify.com" rel="noopener noreferrer" target="_blank">Netlify</a></p><p><a href="https://www.netlify.com" rel="noopener noreferrer" target="_blank"><img alt="Deploys by Netlify" src="https://www.netlify.com/v3/img/components/netlify-light.svg"></a></p><h1 class="">Join the Community</h1><p>
|
||||||
<h2 class="">User forums</h2><h3 class="">Talk TiddlyWiki</h3><p>As the official TiddlyWiki forum, Talk TiddlyWiki is a place to talk about TiddlyWiki: requests for help, <a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/c/announcements/20" rel="noopener noreferrer" target="_blank">announcements</a> of new releases and plugins, debating new features, or just sharing experiences. You can participate via the associated website, or subscribe via email.</p><p><a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/" rel="noopener noreferrer" target="_blank">https://talk.tiddlywiki.org/</a></p><h3 class="">Google Groups</h3><p>For the convenience of existing users, we also continue to operate the original TiddlyWiki group (hosted on Google Groups since 2005): <a class="tc-tiddlylink-external" href="https://groups.google.com/group/TiddlyWiki" rel="noopener noreferrer" target="_blank">https://groups.google.com/group/TiddlyWiki</a></p><h2 class="">Developer forums</h2><ul><li><a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev" rel="noopener noreferrer" target="_blank">tiddlywiki.com/dev</a> is the official developer documentation</li><li>Get involved in the <a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5" rel="noopener noreferrer" target="_blank">development on GitHub</a></li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/discussions" rel="noopener noreferrer" target="_blank">GitHub Discussions</a> are for Q&A and open-ended discussion</li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/issues" rel="noopener noreferrer" target="_blank">GitHub Issues</a> are for raising bug reports and proposing specific, actionable new ideas</li><li>See <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Contributing.html">Contributing</a> for guidelines on how to contribute to the project.</li></ul><h2 class="">Other forums</h2><ul><li><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> Subreddit: <a class="tc-tiddlylink-external" href="https://www.reddit.com/r/TiddlyWiki5/" rel="noopener noreferrer" target="_blank">/r/TiddlyWiki5</a></li><li>Chat on Discord at <a class="tc-tiddlylink-external" href="https://discord.gg/HFFZVQ8" rel="noopener noreferrer" target="_blank">https://discord.gg/HFFZVQ8</a></li></ul>
|
<h2 class="">User forums</h2><h3 class="">Talk TiddlyWiki</h3><p>As the official TiddlyWiki forum, Talk TiddlyWiki is a place to talk about TiddlyWiki: requests for help, <a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/c/announcements/20" rel="noopener noreferrer" target="_blank">announcements</a> of new releases and plugins, debating new features, or just sharing experiences. You can participate via the associated website, or subscribe via email.</p><p><a class="tc-tiddlylink-external" href="https://talk.tiddlywiki.org/" rel="noopener noreferrer" target="_blank">https://talk.tiddlywiki.org/</a></p><h3 class="">Google Groups</h3><p>For the convenience of existing users, we also continue to operate the original TiddlyWiki group (hosted on Google Groups since 2005): <a class="tc-tiddlylink-external" href="https://groups.google.com/group/TiddlyWiki" rel="noopener noreferrer" target="_blank">https://groups.google.com/group/TiddlyWiki</a></p><h2 class="">Developer forums</h2><ul><li><a class="tc-tiddlylink-external" href="https://tiddlywiki.com/dev" rel="noopener noreferrer" target="_blank">tiddlywiki.com/dev</a> is the official developer documentation</li><li>Get involved in the <a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5" rel="noopener noreferrer" target="_blank">development on GitHub</a></li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/discussions" rel="noopener noreferrer" target="_blank">GitHub Discussions</a> are for Q&A and open-ended discussion</li><li><a class="tc-tiddlylink-external" href="https://github.com/TiddlyWiki/TiddlyWiki5/issues" rel="noopener noreferrer" target="_blank">GitHub Issues</a> are for raising bug reports and proposing specific, actionable new ideas</li><li>See <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Contributing.html">Contributing</a> for guidelines on how to contribute to the project.</li></ul><h2 class="">Other forums</h2><ul><li><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> Subreddit: <a class="tc-tiddlylink-external" href="https://www.reddit.com/r/TiddlyWiki5/" rel="noopener noreferrer" target="_blank">/r/TiddlyWiki5</a></li><li>Chat on Discord at <a class="tc-tiddlylink-external" href="https://discord.gg/HFFZVQ8" rel="noopener noreferrer" target="_blank">https://discord.gg/HFFZVQ8</a></li></ul>
|
||||||
</p><hr><h1 class="">Installing TiddlyWiki on Node.js</h1><p>TiddlyWiki is a <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SingleFileApplication.html">SingleFileApplication</a>, which is easy to use. For advanced users and developers there is a possibility to use a Node.js client / server configuration. This configuration is also used to build the TiddlyWiki <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SinglePageApplication.html">SinglePageApplication</a></p><ol><li>Install <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Node.js.html">Node.js</a><ul><li>Linux: <blockquote><div><em>Debian/Ubuntu</em>:<br><code>apt install nodejs</code><br>May need to be followed up by:<br><code>apt install npm</code></div><div><em>Arch Linux</em><br><code>yay -S tiddlywiki</code> <br>(installs node and tiddlywiki)</div></blockquote></li><li>Mac<blockquote><div><code>brew install node</code></div></blockquote></li><li>Android<blockquote><div><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Serving%2520TW5%2520from%2520Android.html">Termux for Android</a></div></blockquote></li><li>Other <blockquote><div>See <a class="tc-tiddlylink-external" href="http://nodejs.org" rel="noopener noreferrer" target="_blank">http://nodejs.org</a></div></blockquote></li></ul></li><li>Open a command line terminal and type:<blockquote><div><code>npm install -g tiddlywiki</code></div><div>If it fails with an error you may need to re-run the command as an administrator:</div><div><code>sudo npm install -g tiddlywiki</code> (Mac/Linux)</div></blockquote></li><li>Ensure TiddlyWiki is installed by typing:<blockquote><div><code>tiddlywiki --version</code></div></blockquote><ul><li>In response, you should see <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> report its current version (eg "5.4.0". You may also see other debugging information reported.)</li></ul></li><li>Try it out:<ol><li><code>tiddlywiki mynewwiki --init server</code> to create a folder for a new wiki that includes server-related components</li><li><code>tiddlywiki mynewwiki --listen</code> to start <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a></li><li>Visit <a class="tc-tiddlylink-external" href="http://127.0.0.1:8080/" rel="noopener noreferrer" target="_blank">http://127.0.0.1:8080/</a> in your browser</li><li>Try editing and creating tiddlers</li></ol></li><li>Optionally, make an offline copy:<ul><li>click the <span class="doc-icon"><svg class="tc-image-save-button-dynamic tc-image-button" height="22pt" viewBox="0 0 128 128" width="22pt">
|
</p><hr><h1 class="">Installing TiddlyWiki on Node.js</h1><p>TiddlyWiki is a <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SingleFileApplication.html">SingleFileApplication</a>, which is easy to use. For advanced users and developers there is a possibility to use a Node.js client / server configuration. This configuration is also used to build the TiddlyWiki <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/SinglePageApplication.html">SinglePageApplication</a></p><ol><li>Install <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Node.js.html">Node.js</a><ul><li>Linux: <blockquote><div><em>Debian/Ubuntu</em>:<br><code>apt install nodejs</code><br>May need to be followed up by:<br><code>apt install npm</code></div><div><em>Arch Linux</em><br><code>yay -S tiddlywiki</code> <br>(installs node and tiddlywiki)</div></blockquote></li><li>Mac<blockquote><div><code>brew install node</code></div></blockquote></li><li>Android<blockquote><div><a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/Serving%2520TW5%2520from%2520Android.html">Termux for Android</a></div></blockquote></li><li>Other <blockquote><div>See <a class="tc-tiddlylink-external" href="http://nodejs.org" rel="noopener noreferrer" target="_blank">http://nodejs.org</a></div></blockquote></li></ul></li><li>Open a command line terminal and type:<blockquote><div><code>npm install -g tiddlywiki</code></div><div>If it fails with an error you may need to re-run the command as an administrator:</div><div><code>sudo npm install -g tiddlywiki</code> (Mac/Linux)</div></blockquote></li><li>Ensure TiddlyWiki is installed by typing:<blockquote><div><code>tiddlywiki --version</code></div></blockquote><ul><li>In response, you should see <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a> report its current version (eg "5.4.1". You may also see other debugging information reported.)</li></ul></li><li>Try it out:<ol><li><code>tiddlywiki mynewwiki --init server</code> to create a folder for a new wiki that includes server-related components</li><li><code>tiddlywiki mynewwiki --listen</code> to start <a class="tc-tiddlylink tc-tiddlylink-resolves" href="https://tiddlywiki.com/static/TiddlyWiki.html">TiddlyWiki</a></li><li>Visit <a class="tc-tiddlylink-external" href="http://127.0.0.1:8080/" rel="noopener noreferrer" target="_blank">http://127.0.0.1:8080/</a> in your browser</li><li>Try editing and creating tiddlers</li></ol></li><li>Optionally, make an offline copy:<ul><li>click the <span class="doc-icon"><svg class="tc-image-save-button-dynamic tc-image-button" height="22pt" viewBox="0 0 128 128" width="22pt">
|
||||||
<g class="tc-image-save-button-dynamic-clean">
|
<g class="tc-image-save-button-dynamic-clean">
|
||||||
<path d="M120.783 34.33c4.641 8.862 7.266 18.948 7.266 29.646 0 35.347-28.653 64-64 64-35.346 0-64-28.653-64-64 0-35.346 28.654-64 64-64 18.808 0 35.72 8.113 47.43 21.03l2.68-2.68c3.13-3.13 8.197-3.132 11.321-.008 3.118 3.118 3.121 8.193-.007 11.32l-4.69 4.691zm-12.058 12.058a47.876 47.876 0 013.324 17.588c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48c14.39 0 27.3 6.332 36.098 16.362L58.941 73.544 41.976 56.578c-3.127-3.127-8.201-3.123-11.32-.005-3.123 3.124-3.119 8.194.006 11.319l22.617 22.617a7.992 7.992 0 005.659 2.347c2.05 0 4.101-.783 5.667-2.349l44.12-44.12z" fill-rule="evenodd"></path>
|
<path d="M120.783 34.33c4.641 8.862 7.266 18.948 7.266 29.646 0 35.347-28.653 64-64 64-35.346 0-64-28.653-64-64 0-35.346 28.654-64 64-64 18.808 0 35.72 8.113 47.43 21.03l2.68-2.68c3.13-3.13 8.197-3.132 11.321-.008 3.118 3.118 3.121 8.193-.007 11.32l-4.69 4.691zm-12.058 12.058a47.876 47.876 0 013.324 17.588c0 26.51-21.49 48-48 48s-48-21.49-48-48 21.49-48 48-48c14.39 0 27.3 6.332 36.098 16.362L58.941 73.544 41.976 56.578c-3.127-3.127-8.201-3.123-11.32-.005-3.123 3.124-3.119 8.194.006 11.319l22.617 22.617a7.992 7.992 0 005.659 2.347c2.05 0 4.101-.783 5.667-2.349l44.12-44.12z" fill-rule="evenodd"></path>
|
||||||
</g>
|
</g>
|
||||||
|
|||||||
Reference in New Issue
Block a user