diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed16d707d..f6fb58f7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE_VERSION }}" @@ -30,7 +30,7 @@ jobs: TW5_BUILD_MAIN_EDITION: "./editions/prerelease" TW5_BUILD_OUTPUT: "./output/prerelease" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE_VERSION }}" @@ -62,7 +62,7 @@ jobs: TW5_BUILD_OUTPUT: "./output" TW5_BUILD_ARCHIVE: "./output" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v5 - uses: actions/setup-node@v4 with: node-version: "${{ env.NODE_VERSION }}" diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml new file mode 100644 index 000000000..eae1d2c46 --- /dev/null +++ b/.github/workflows/eslint.yml @@ -0,0 +1,40 @@ +name: ESLint + +on: + pull_request: + types: [opened, synchronize, reopened] + workflow_dispatch: + +concurrency: + group: lint-${{ github.event.pull_request.number || github.ref_name }} + cancel-in-progress: true + +permissions: + contents: read + # Needed for GitHub Checks API + checks: write + +jobs: + eslint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Install dependencies + run: npm install --include=dev + + - name: Run ESLint with reviewdog (GitHub Checks) + uses: reviewdog/action-eslint@v1 + with: + eslint_flags: '.' + reporter: github-pr-check + fail_level: error + level: error + tool_name: ESLint PR code diff --git a/bin/build-site.sh b/bin/build-site.sh index f1859e139..a68dc0752 100755 --- a/bin/build-site.sh +++ b/bin/build-site.sh @@ -73,10 +73,8 @@ rm $TW5_BUILD_OUTPUT/dev/static/* echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/tw2parser/index.html" > $TW5_BUILD_OUTPUT/classicparserdemo.html echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/codemirror/index.html" > $TW5_BUILD_OUTPUT/codemirrordemo.html -echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/d3/index.html" > $TW5_BUILD_OUTPUT/d3demo.html echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/highlight/index.html" > $TW5_BUILD_OUTPUT/highlightdemo.html echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/markdown/index.html" > $TW5_BUILD_OUTPUT/markdowndemo.html -echo "Moved to http://tiddlywiki.com/plugins/tiddlywiki/tahoelafs/index.html" > $TW5_BUILD_OUTPUT/tahoelafs.html # Put the build details into a .tid file so that it can be included in each build (deleted at the end of this script) @@ -301,26 +299,6 @@ node $TW5_BUILD_TIDDLYWIKI \ --rendertiddler $:/core/save/empty plugins/tiddlywiki/katex/empty.html text/plain \ || exit 1 -# /plugins/tiddlywiki/tahoelafs/index.html Demo wiki with Tahoe-LAFS plugin -# /plugins/tiddlywiki/tahoelafs/empty.html Empty wiki with Tahoe-LAFS plugin -node $TW5_BUILD_TIDDLYWIKI \ - ./editions/tahoelafs \ - --load $TW5_BUILD_OUTPUT/build.tid \ - --output $TW5_BUILD_OUTPUT \ - --rendertiddler $:/core/save/all plugins/tiddlywiki/tahoelafs/index.html text/plain \ - --rendertiddler $:/core/save/empty plugins/tiddlywiki/tahoelafs/empty.html text/plain \ - || exit 1 - -# /plugins/tiddlywiki/d3/index.html Demo wiki with D3 plugin -# /plugins/tiddlywiki/d3/empty.html Empty wiki with D3 plugin -node $TW5_BUILD_TIDDLYWIKI \ - ./editions/d3demo \ - --load $TW5_BUILD_OUTPUT/build.tid \ - --output $TW5_BUILD_OUTPUT \ - --rendertiddler $:/core/save/all plugins/tiddlywiki/d3/index.html text/plain \ - --rendertiddler $:/core/save/empty plugins/tiddlywiki/d3/empty.html text/plain \ - || exit 1 - # /plugins/tiddlywiki/codemirror/index.html Demo wiki with codemirror plugin # /plugins/tiddlywiki/codemirror/empty.html Empty wiki with codemirror plugin node $TW5_BUILD_TIDDLYWIKI \ diff --git a/boot/boot.js b/boot/boot.js index 9b88b98b1..6ac64c586 100644 --- a/boot/boot.js +++ b/boot/boot.js @@ -641,7 +641,7 @@ $tw.utils.evalGlobal = function(code,context,filename,sandbox,allowGlobals) { // Call the function and return the exports return fn.apply(null,contextValues); }; -$tw.utils.sandbox = !$tw.browser ? vm.createContext({}) : undefined; +$tw.utils.sandbox = !$tw.browser ? vm.createContext({}) : undefined; /* Run code in a sandbox with only the specified context variables in scope */ @@ -799,12 +799,13 @@ the password, and to encrypt/decrypt a block of text $tw.utils.Crypto = function() { var sjcl = $tw.node ? (global.sjcl || require("./sjcl.js")) : window.sjcl, currentPassword = null, - callSjcl = function(method,inputText,password) { + callSjcl = function(method,inputText,password,options) { + options = options || {}; password = password || currentPassword; var outputText; try { if(password) { - outputText = sjcl[method](password,inputText); + outputText = sjcl[method](password,inputText,options); } } catch(ex) { console.log("Crypto error:" + ex); @@ -830,7 +831,8 @@ $tw.utils.Crypto = function() { return !!currentPassword; } this.encrypt = function(text,password) { - return callSjcl("encrypt",text,password); + // set default ks:256 -- see: http://bitwiseshiftleft.github.io/sjcl/doc/convenience.js.html + return callSjcl("encrypt",text,password,{v:1,iter:10000,ks:256,ts:64,mode:"ccm",adata:"",cipher:"aes"}); }; this.decrypt = function(text,password) { return callSjcl("decrypt",text,password); @@ -1530,7 +1532,8 @@ Define all modules stored in ordinary tiddlers */ $tw.Wiki.prototype.defineTiddlerModules = function() { this.each(function(tiddler,title) { - if(tiddler.hasField("module-type")) { + // Modules in draft tiddlers are disabled + if(tiddler.hasField("module-type") && (!tiddler.hasField("draft.of"))) { switch(tiddler.fields.type) { case "application/javascript": // We only define modules that haven't already been defined, because in the browser modules in system tiddlers are defined in inline script @@ -1557,6 +1560,11 @@ $tw.Wiki.prototype.defineShadowModules = function() { this.eachShadow(function(tiddler,title) { // Don't define the module if it is overidden by an ordinary tiddler if(!self.tiddlerExists(title) && tiddler.hasField("module-type")) { + if(tiddler.hasField("draft.of")) { + // Report a fundamental problem + console.warn(`TiddlyWiki: Plugins should not contain tiddlers with a 'draft.of' field: ${tiddler.fields.title}`); + return; + } // Define the module $tw.modules.define(tiddler.fields.title,tiddler.fields["module-type"],tiddler.fields.text); } @@ -1905,7 +1913,7 @@ $tw.loadTiddlersFromFile = function(filepath,fields) { fileSize = fs.statSync(filepath).size, data; if(fileSize > $tw.config.maxEditFileSize) { - data = "File " + filepath + "not loaded because it is too large"; + data = "File " + filepath + " not loaded because it is too large"; console.log("Warning: " + data); ext = ".txt"; } else { @@ -1976,22 +1984,41 @@ filepath: pathname of the directory containing the specification file $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) { var tiddlers = []; // Read the specification - var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8")); + var filesInfo = $tw.utils.parseJSONSafe(fs.readFileSync(filepath + path.sep + "tiddlywiki.files","utf8"), function(e) { + console.log("Warning: tiddlywiki.files in " + filepath + " invalid: " + e.message); + return {}; + }); + // Helper to process a file var processFile = function(filename,isTiddlerFile,fields,isEditableFile,rootPath) { var extInfo = $tw.config.fileExtensionInfo[path.extname(filename)], type = (extInfo || {}).type || fields.type || "text/plain", typeInfo = $tw.config.contentTypeInfo[type] || {}, pathname = path.resolve(filepath,filename), - text = fs.readFileSync(pathname,typeInfo.encoding || "utf8"), metadata = $tw.loadMetadataForFile(pathname) || {}, - fileTiddlers; + fileTooLarge = false, + text, fileTiddlers; + + if("_canonical_uri" in fields) { + text = ""; + } else if(fs.statSync(pathname).size > $tw.config.maxEditFileSize) { + var msg = "File " + pathname + " not loaded because it is too large"; + console.log("Warning: " + msg); + fileTooLarge = true; + text = isTiddlerFile ? msg : ""; + } else { + text = fs.readFileSync(pathname,typeInfo.encoding || "utf8"); + } + if(isTiddlerFile) { - fileTiddlers = $tw.wiki.deserializeTiddlers(path.extname(pathname),text,metadata) || []; + fileTiddlers = $tw.wiki.deserializeTiddlers(fileTooLarge ? ".txt" : path.extname(pathname),text,metadata) || []; } else { fileTiddlers = [$tw.utils.extend({text: text},metadata)]; } var combinedFields = $tw.utils.extend({},fields,metadata); + if(fileTooLarge && isTiddlerFile) { + delete combinedFields.type; // type altered + } $tw.utils.each(fileTiddlers,function(tiddler) { $tw.utils.each(combinedFields,function(fieldInfo,name) { if(typeof fieldInfo === "string" || $tw.utils.isArray(fieldInfo)) { @@ -2066,6 +2093,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) { } else if(tidInfo.suffix) { tidInfo.fields.text = {suffix: tidInfo.suffix}; } + tidInfo.fields = tidInfo.fields || {}; processFile(tidInfo.file,tidInfo.isTiddlerFile,tidInfo.fields); }); // Process any listed directories @@ -2087,6 +2115,7 @@ $tw.loadTiddlersFromSpecification = function(filepath,excludeRegExp) { var thisPath = path.relative(filepath, files[t]), filename = path.basename(thisPath); if(filename !== "tiddlywiki.files" && !metaRegExp.test(filename) && fileRegExp.test(filename)) { + dirSpec.fields = dirSpec.fields || {}; processFile(thisPath,dirSpec.isTiddlerFile,dirSpec.fields,dirSpec.isEditableFile,dirSpec.path); } } @@ -2549,10 +2578,10 @@ $tw.boot.execStartup = function(options){ if($tw.safeMode) { $tw.wiki.processSafeMode(); } - // Register typed modules from the tiddlers we've just loaded - $tw.wiki.defineTiddlerModules(); - // And any modules within plugins + // Register typed modules from the tiddlers we've just loaded and any modules within plugins + // Tiddlers should appear last so that they may overwrite shadows during module registration $tw.wiki.defineShadowModules(); + $tw.wiki.defineTiddlerModules(); // Make sure the crypto state tiddler is up to date if($tw.crypto) { $tw.crypto.updateCryptoStateTiddler(); @@ -2621,11 +2650,13 @@ $tw.boot.executeNextStartupTask = function(callback) { $tw.boot.log(s.join(" ")); // Execute task if(!$tw.utils.hop(task,"synchronous") || task.synchronous) { - task.startup(); - if(task.name) { - $tw.boot.executedStartupModules[task.name] = true; + const thenable = task.startup(); + if(thenable && typeof thenable.then === "function"){ + thenable.then(asyncTaskCallback); + return true; + } else { + return asyncTaskCallback(); } - return $tw.boot.executeNextStartupTask(callback); } else { task.startup(asyncTaskCallback); return true; diff --git a/core-server/commander.js b/core-server/commander.js index b73e39b0f..a6cdc81c9 100644 --- a/core-server/commander.js +++ b/core-server/commander.js @@ -99,16 +99,18 @@ Commander.prototype.executeNextCommand = function() { } } if(command.info.synchronous) { - // Synchronous command + // Synchronous command (await thenables) c = new command.Command(params,this); err = c.execute(); - if(err) { + if(err && typeof err.then === "function") { + err.then(e => { e ? this.callback(e) : this.executeNextCommand(); }); + } else if(err) { this.callback(err); } else { this.executeNextCommand(); } } else { - // Asynchronous command + // Asynchronous command (await thenables) c = new command.Command(params,this,function(err) { if(err) { self.callback(err); @@ -117,7 +119,9 @@ Commander.prototype.executeNextCommand = function() { } }); err = c.execute(); - if(err) { + if(err && typeof err.then === "function") { + err.then(e => { if(e) this.callback(e); }); + } else if(err) { this.callback(err); } } diff --git a/core-server/server/routes/delete-tiddler.js b/core-server/server/routes/delete-tiddler.js index 33cb40d55..17db39848 100644 --- a/core-server/server/routes/delete-tiddler.js +++ b/core-server/server/routes/delete-tiddler.js @@ -8,10 +8,14 @@ DELETE /recipes/default/tiddlers/:title \*/ "use strict"; -exports.method = "DELETE"; +exports.methods = ["DELETE"]; exports.path = /^\/bags\/default\/tiddlers\/(.+)$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var title = $tw.utils.decodeURIComponentSafe(state.params[0]); state.wiki.deleteTiddler(title); diff --git a/core-server/server/routes/get-favicon.js b/core-server/server/routes/get-favicon.js index 8b757b2f7..ce4bc55ed 100644 --- a/core-server/server/routes/get-favicon.js +++ b/core-server/server/routes/get-favicon.js @@ -8,10 +8,14 @@ GET /favicon.ico \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/favicon.ico$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var buffer = state.wiki.getTiddlerText("$:/favicon.ico",""); state.sendResponse(200,{"Content-Type": "image/x-icon"},buffer,"base64"); diff --git a/core-server/server/routes/get-file.js b/core-server/server/routes/get-file.js index 39681de4c..ec928319c 100644 --- a/core-server/server/routes/get-file.js +++ b/core-server/server/routes/get-file.js @@ -8,35 +8,66 @@ GET /files/:filepath \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/files\/(.+)$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var path = require("path"), fs = require("fs"), - util = require("util"), suppliedFilename = $tw.utils.decodeURIComponentSafe(state.params[0]), baseFilename = path.resolve(state.boot.wikiPath,"files"), filename = path.resolve(baseFilename,suppliedFilename), extension = path.extname(filename); // Check that the filename is inside the wiki files folder - if(path.relative(baseFilename,filename).indexOf("..") !== 0) { - // Send the file - fs.readFile(filename,function(err,content) { - var status,content,type = "text/plain"; - if(err) { - console.log("Error accessing file " + filename + ": " + err.toString()); - status = 404; - content = "File '" + suppliedFilename + "' not found"; - } else { - status = 200; - content = content; - type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream"); - } - state.sendResponse(status,{"Content-Type": type},content); - }); - } else { - state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found"); + if(path.relative(baseFilename,filename).indexOf("..") === 0) { + return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found"); } + fs.stat(filename, function(err, stats) { + if(err) { + return state.sendResponse(404,{"Content-Type": "text/plain"},"File '" + suppliedFilename + "' not found"); + } else { + var type = ($tw.config.fileExtensionInfo[extension] ? $tw.config.fileExtensionInfo[extension].type : "application/octet-stream"), + responseHeaders = { + "Content-Type": type, + "Accept-Ranges": "bytes" + }; + var rangeHeader = request.headers.range, + stream; + if(rangeHeader) { + // Handle range requests + var parts = rangeHeader.replace(/bytes=/, "").split("-"), + start = parseInt(parts[0], 10), + end = parts[1] ? parseInt(parts[1], 10) : stats.size - 1; + // Validate start and end + if(isNaN(start) || isNaN(end) || start < 0 || end < start || end >= stats.size) { + responseHeaders["Content-Range"] = "bytes */" + stats.size; + return response.writeHead(416, responseHeaders).end(); + } + var chunksize = (end - start) + 1; + responseHeaders["Content-Range"] = "bytes " + start + "-" + end + "/" + stats.size; + responseHeaders["Content-Length"] = chunksize; + response.writeHead(206, responseHeaders); + stream = fs.createReadStream(filename, {start: start, end: end}); + } else { + responseHeaders["Content-Length"] = stats.size; + response.writeHead(200, responseHeaders); + stream = fs.createReadStream(filename); + } + // Common stream error handling + stream.on("error", function(err) { + if(!response.headersSent) { + response.writeHead(500, {"Content-Type": "text/plain"}); + response.end("Read error"); + } else { + response.destroy(); + } + }); + stream.pipe(response); + } + }); }; diff --git a/core-server/server/routes/get-index.js b/core-server/server/routes/get-index.js index 7ba52faf6..f856bd511 100644 --- a/core-server/server/routes/get-index.js +++ b/core-server/server/routes/get-index.js @@ -8,10 +8,14 @@ GET / \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var text = state.wiki.renderTiddler(state.server.get("root-render-type"),state.server.get("root-tiddler")), responseHeaders = { diff --git a/core-server/server/routes/get-login-basic.js b/core-server/server/routes/get-login-basic.js index bca3bd18d..e9a73c856 100644 --- a/core-server/server/routes/get-login-basic.js +++ b/core-server/server/routes/get-login-basic.js @@ -8,10 +8,14 @@ GET /login-basic -- force a Basic Authentication challenge \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/login-basic$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { if(!state.authenticatedUsername) { // Challenge if there's no username diff --git a/core-server/server/routes/get-status.js b/core-server/server/routes/get-status.js index d93c64037..ed2c52806 100644 --- a/core-server/server/routes/get-status.js +++ b/core-server/server/routes/get-status.js @@ -8,10 +8,14 @@ GET /status \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/status$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var text = JSON.stringify({ username: state.authenticatedUsername || state.server.get("anon-username") || "", diff --git a/core-server/server/routes/get-tiddler-html.js b/core-server/server/routes/get-tiddler-html.js index a891ea6e7..b7a8aa8f6 100644 --- a/core-server/server/routes/get-tiddler-html.js +++ b/core-server/server/routes/get-tiddler-html.js @@ -8,10 +8,14 @@ GET /:title \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/([^\/]+)$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var title = $tw.utils.decodeURIComponentSafe(state.params[0]), tiddler = state.wiki.getTiddler(title); diff --git a/core-server/server/routes/get-tiddler.js b/core-server/server/routes/get-tiddler.js index 58ecb8a43..b5c4e11a0 100644 --- a/core-server/server/routes/get-tiddler.js +++ b/core-server/server/routes/get-tiddler.js @@ -8,10 +8,14 @@ GET /recipes/default/tiddlers/:title \*/ "use strict"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var title = $tw.utils.decodeURIComponentSafe(state.params[0]), tiddler = state.wiki.getTiddler(title), diff --git a/core-server/server/routes/get-tiddlers-json.js b/core-server/server/routes/get-tiddlers-json.js index 89f91032e..8c78dad9f 100644 --- a/core-server/server/routes/get-tiddlers-json.js +++ b/core-server/server/routes/get-tiddlers-json.js @@ -10,10 +10,14 @@ GET /recipes/default/tiddlers.json?filter= var DEFAULT_FILTER = "[all[tiddlers]!is[system]sort[title]]"; -exports.method = "GET"; +exports.methods = ["GET"]; exports.path = /^\/recipes\/default\/tiddlers.json$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var filter = state.queryParameters.filter || DEFAULT_FILTER; if(state.wiki.getTiddlerText("$:/config/Server/AllowAllExternalFilters") !== "yes") { diff --git a/core-server/server/routes/put-tiddler.js b/core-server/server/routes/put-tiddler.js index 74327bd50..55479b570 100644 --- a/core-server/server/routes/put-tiddler.js +++ b/core-server/server/routes/put-tiddler.js @@ -8,10 +8,14 @@ PUT /recipes/default/tiddlers/:title \*/ "use strict"; -exports.method = "PUT"; +exports.methods = ["PUT"]; exports.path = /^\/recipes\/default\/tiddlers\/(.+)$/; +exports.info = { + priority: 100 +}; + exports.handler = function(request,response,state) { var title = $tw.utils.decodeURIComponentSafe(state.params[0]), fields = $tw.utils.parseJSONSafe(state.data); diff --git a/core-server/server/server.js b/core-server/server/server.js index 2118e9b50..e1741d08b 100644 --- a/core-server/server/server.js +++ b/core-server/server/server.js @@ -74,6 +74,11 @@ function Server(options) { // console.log("Loading server route " + title); self.addRoute(routeDefinition); }); + this.routes.sort((a, b) => { + const priorityA = a.info?.priority ?? 100, + priorityB = b.info?.priority ?? 100; + return priorityB - priorityA; + }); // Initialise the http vs https this.listenOptions = null; this.protocol = "http"; @@ -217,7 +222,7 @@ Server.prototype.findMatchingRoute = function(request,state) { } else { match = potentialRoute.path.exec(pathname); } - if(match && request.method === potentialRoute.method) { + if(match && (potentialRoute.methods?.includes(request.method) || potentialRoute.method === request.method)) { state.params = []; for(var p=1; p"); + this.iframeDoc.write(""); this.iframeDoc.close(); // Style the iframe this.iframeNode.className = this.dummyTextArea.className; diff --git a/core/modules/editor/factory.js b/core/modules/editor/factory.js index 984cc76ba..2fd323c0b 100644 --- a/core/modules/editor/factory.js +++ b/core/modules/editor/factory.js @@ -68,7 +68,7 @@ function editTextWidgetFactory(toolbarEngine,nonToolbarEngine) { // Fix height this.engine.fixHeight(); // Focus if required - if(this.editFocus === "true" || this.editFocus === "yes") { + if($tw.browser && (this.editFocus === "true" || this.editFocus === "yes") && !$tw.utils.hasClass(this.parentDomNode.ownerDocument.activeElement,"tc-keep-focus")) { this.engine.focus(); } // Add widget message listeners diff --git a/core/modules/filters/format/json.js b/core/modules/filters/format/json.js index 5db3658e7..98f85dd27 100644 --- a/core/modules/filters/format/json.js +++ b/core/modules/filters/format/json.js @@ -16,12 +16,8 @@ exports.json = function(source,operand,options) { spaces = /^\d+$/.test(operand) ? parseInt(operand,10) : operand; } source(function(tiddler,title) { - var data = $tw.utils.parseJSONSafe(title); - try { - data = JSON.parse(title); - } catch(e) { - data = undefined; - } + var data = $tw.utils.parseJSONSafe(title,function(){return undefined;}); + if(data !== undefined) { results.push(JSON.stringify(data,null,spaces)); } diff --git a/core/modules/filters/math.js b/core/modules/filters/math.js index 4c2a9168a..bdf2117eb 100644 --- a/core/modules/filters/math.js +++ b/core/modules/filters/math.js @@ -217,6 +217,10 @@ function makeNumericReducingOperator(fnCalc,initialValue,fnFinal) { source(function(tiddler,title) { result.push($tw.utils.parseNumber(title)); }); + // We return an empty array if there are no input titles + if(result.length === 0) { + return []; + } var value = result.reduce(function(accumulator,currentValue) { return fnCalc(accumulator,currentValue); },initialValue); diff --git a/core/modules/info/dimensions.js b/core/modules/info/dimensions.js new file mode 100644 index 000000000..48458dd0f --- /dev/null +++ b/core/modules/info/dimensions.js @@ -0,0 +1,86 @@ +/*\ +title: $:/core/modules/info/windowdimensions.js +type: application/javascript +module-type: info +\*/ + +exports.getInfoTiddlerFields = function(updateInfoTiddlersCallback) { + if(!$tw.browser) { + return []; + } + + class WindowDimensionsTracker { + constructor(updateCallback) { + this.updateCallback = updateCallback; + this.resizeHandlers = new Map(); + this.dimensionsInfo = [ + ["outer/width", win => win.outerWidth], + ["outer/height", win => win.outerHeight], + ["inner/width", win => win.innerWidth], + ["inner/height", win => win.innerHeight], + ["client/width", win => win.document.documentElement.clientWidth], + ["client/height", win => win.document.documentElement.clientHeight] + ]; + } + + buildTiddlers(win,windowId) { + const prefix = `$:/info/browser/window/${windowId}/`; + return this.dimensionsInfo.map(([suffix, getter]) => ({ + title: prefix + suffix, + text: String(getter(win)) + })); + } + + clearTiddlers(windowId) { + const prefix = `$:/info/browser/window/${windowId}/`, + deletions = this.dimensionsInfo.map(([suffix]) => prefix + suffix); + this.updateCallback([], deletions); + } + + getUpdateHandler(win,windowId) { + let scheduled = false; + return () => { + if(!scheduled) { + scheduled = true; + requestAnimationFrame(() => { + this.updateCallback(this.buildTiddlers(win,windowId), []); + scheduled = false; + }); + } + }; + } + + trackWindow(win,windowId) { + const handler = this.getUpdateHandler(win, windowId); + handler(); // initial update + win.addEventListener("resize",handler,{passive:true}); + this.resizeHandlers.set(windowId,{win, handler}); + } + + untrackWindow(windowId) { + const entry = this.resizeHandlers.get(windowId); + if(entry) { + entry.win.removeEventListener("resize", entry.handler); + this.resizeHandlers.delete(windowId); + } + this.clearTiddlers(windowId); + } + } + + const tracker = new WindowDimensionsTracker(updateInfoTiddlersCallback); + + // Track main window + tracker.trackWindow(window,"system/main"); + + // Hook into event bus for user windows + if($tw.eventBus) { + $tw.eventBus.on("window:opened", ({window: win, windowID}) => { + tracker.trackWindow(win, "user/" + windowID); + }); + $tw.eventBus.on("window:closed", ({windowID}) => { + tracker.untrackWindow("user/" + windowID); + }); + } + + return []; +}; diff --git a/core/modules/parsers/audioparser.js b/core/modules/parsers/audioparser.js index 601de058e..757703da4 100644 --- a/core/modules/parsers/audioparser.js +++ b/core/modules/parsers/audioparser.js @@ -7,23 +7,34 @@ The audio parser parses an audio tiddler into an embeddable HTML element \*/ +/*jslint node: true, browser: true */ +/*global $tw: false */ "use strict"; var AudioParser = function(type,text,options) { var element = { type: "element", - tag: "audio", + tag: "$audio", // Using $audio to enable widget interception attributes: { controls: {type: "string", value: "controls"}, style: {type: "string", value: "width: 100%; object-fit: contain"} } - }, - src; + }; + + // Pass through source information if(options._canonical_uri) { element.attributes.src = {type: "string", value: options._canonical_uri}; + element.attributes.type = {type: "string", value: type}; } else if(text) { element.attributes.src = {type: "string", value: "data:" + type + ";base64," + text}; + element.attributes.type = {type: "string", value: type}; } + + // Pass through tiddler title if available + if(options.title) { + element.attributes.tiddler = {type: "string", value: options.title}; + } + this.tree = [element]; this.source = text; this.type = type; @@ -33,3 +44,4 @@ exports["audio/ogg"] = AudioParser; exports["audio/mpeg"] = AudioParser; exports["audio/mp3"] = AudioParser; exports["audio/mp4"] = AudioParser; + \ No newline at end of file diff --git a/core/modules/parsers/parseutils.js b/core/modules/parsers/parseutils.js index 30bc39509..05a85e1ec 100644 --- a/core/modules/parsers/parseutils.js +++ b/core/modules/parsers/parseutils.js @@ -82,6 +82,7 @@ exports.parseTokenString = function(source,pos,token) { /* Look for a token matching a regex. Returns null if not found, otherwise returns {type: "regexp", match:, start:, end:,} +Use the "Y" (sticky) flag to avoid searching the entire rest of the string */ exports.parseTokenRegExp = function(source,pos,reToken) { var node = { @@ -172,7 +173,7 @@ exports.parseMacroParameter = function(source,pos) { start: pos }; // Define our regexp - var reMacroParameter = /(?:([A-Za-z0-9\-_]+)\s*:)?(?:\s*(?:"""([\s\S]*?)"""|"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|((?:(?:>(?!>))|[^\s>"'])+)))/g; + const reMacroParameter = /(?:([A-Za-z0-9\-_]+)\s*:)?(?:\s*(?:"""([\s\S]*?)"""|"([^"]*)"|'([^']*)'|\[\[([^\]]*)\]\]|((?:(?:>(?!>))|[^\s>"'])+)))/y; // Skip whitespace pos = $tw.utils.skipWhiteSpace(source,pos); // Look for the parameter @@ -240,7 +241,7 @@ exports.parseMacroInvocation = function(source,pos) { params: [] }; // Define our regexps - var reMacroName = /([^\s>"'=]+)/g; + const reMacroName = /([^\s>"'=]+)/y; // Skip whitespace pos = $tw.utils.skipWhiteSpace(source,pos); // Look for a double less than sign @@ -277,7 +278,7 @@ exports.parseFilterVariable = function(source) { params: [], }, pos = 0, - reName = /([^\s"']+)/g; + reName = /([^\s"']+)/y; // If there is no whitespace or it is an empty string then there are no macro parameters if(/^\S*$/.test(source)) { node.name = source; @@ -302,11 +303,11 @@ exports.parseAttribute = function(source,pos) { start: pos }; // Define our regexps - var reAttributeName = /([^\/\s>"'`=]+)/g, - reUnquotedAttribute = /([^\/\s<>"'`=]+)/g, - reFilteredValue = /\{\{\{([\S\s]+?)\}\}\}/g, - reIndirectValue = /\{\{([^\}]+)\}\}/g, - reSubstitutedValue = /(?:```([\s\S]*?)```|`([^`]|[\S\s]*?)`)/g; + const reAttributeName = /([^\/\s>"'`=]+)/y, + reUnquotedAttribute = /([^\/\s<>"'`=]+)/y, + reFilteredValue = /\{\{\{([\S\s]+?)\}\}\}/y, + reIndirectValue = /\{\{([^\}]+)\}\}/y, + reSubstitutedValue = /(?:```([\s\S]*?)```|`([^`]|[\S\s]*?)`)/y; // Skip whitespace pos = $tw.utils.skipWhiteSpace(source,pos); // Get the attribute name diff --git a/core/modules/parsers/wikiparser/rules/commentblock.js b/core/modules/parsers/wikiparser/rules/commentblock.js index 96d3deb3d..9e25587ba 100644 --- a/core/modules/parsers/wikiparser/rules/commentblock.js +++ b/core/modules/parsers/wikiparser/rules/commentblock.js @@ -22,7 +22,7 @@ Note that the syntax for comments is simplified to an opening " "backgroundColor" diff --git a/core/modules/utils/dom/modal.js b/core/modules/utils/dom/modal.js index 4e407499e..a7b538ec3 100644 --- a/core/modules/utils/dom/modal.js +++ b/core/modules/utils/dom/modal.js @@ -210,7 +210,7 @@ Modal.prototype.display = function(title,options) { bodyWidgetNode.addEventListener("tm-close-tiddler",closeHandler,false); footerWidgetNode.addEventListener("tm-close-tiddler",closeHandler,false); // Whether to close the modal dialog when the mask (area outside the modal) is clicked - if(tiddler.fields && (tiddler.fields["mask-closable"] === "yes" || tiddler.fields["mask-closable"] === "true")) { + if(tiddler.fields && (tiddler.fields["mask-closable"] === "yes" || tiddler.fields["mask-closable"] === "true" || tiddler.fields["mask-closable"] === "" || "mask-closable" in tiddler.fields === false)) { modalBackdrop.addEventListener("click",closeHandler,false); } // Set the initial styles for the message diff --git a/core/modules/utils/messaging.js b/core/modules/utils/messaging.js new file mode 100644 index 000000000..c7467f8b9 --- /dev/null +++ b/core/modules/utils/messaging.js @@ -0,0 +1,126 @@ +/*\ +title: $:/core/modules/utils/messaging.js +type: application/javascript +module-type: utils-browser + +Messaging utilities for use with window.postMessage() etc. + +This module intentionally has no dependencies so that it can be included in non-TiddlyWiki projects + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var RESPONSE_TIMEOUT = 2 * 1000; + +/* +Class to handle subscribing to publishers + +target: Target window (eg iframe.contentWindow) +type: String indicating type of item for which subscriptions are being provided (eg "SAVING") +onsubscribe: Function to be invoked with err parameter when the subscription is established, or there is a timeout +onmessage: Function to be invoked when a new message arrives, invoked with (data,callback). The callback is invoked with the argument (response) +*/ +function BrowserMessagingSubscriber(options) { + var self = this; + this.target = options.target; + this.type = options.type; + this.onsubscribe = options.onsubscribe || function() {}; + this.onmessage = options.onmessage; + this.hasConfirmed = false; + this.channel = new MessageChannel(); + this.channel.port1.addEventListener("message",function(event) { + if(this.timerID) { + clearTimeout(this.timerID); + this.timerID = null; + } + if(event.data) { + if(event.data.verb === "SUBSCRIBED") { + self.hasConfirmed = true; + self.onsubscribe(null); + } else if(event.data.verb === self.type) { + self.onmessage(event.data,function(response) { + // Send the response back on the supplied port, and then close it + event.ports[0].postMessage(response); + event.ports[0].close(); + }); + } + } + }); + // Set a timer so that if we don't hear from the iframe before a timeout we alert the user + this.timerID = setTimeout(function() { + if(!self.hasConfirmed) { + self.onsubscribe("NO_RESPONSE"); + } + },RESPONSE_TIMEOUT); + this.channel.port1.start(); + this.target.postMessage({verb: "SUBSCRIBE",to: self.type},"*",[this.channel.port2]); +} + +exports.BrowserMessagingSubscriber = BrowserMessagingSubscriber; + +/* +Class to handle publishing subscriptions + +type: String indicating type of item for which subscriptions are being provided (eg "SAVING") +onsubscribe: Function to be invoked when a subscription occurs +*/ +function BrowserMessagingPublisher(options) { + var self = this; + this.type = options.type; + this.hostIsListening = false; + this.port = null; + // Listen to connection requests from the host + window.addEventListener("message",function(event) { + if(event.data && event.data.verb === "SUBSCRIBE" && event.data.to === self.type) { + self.hostIsListening = true; + // Acknowledge + self.port = event.ports[0]; + self.port.postMessage({verb: "SUBSCRIBED", to: self.type}); + if(options.onsubscribe) { + options.onsubscribe(event.data); + } + } + }); +} + +BrowserMessagingPublisher.prototype.canSend = function() { + return !!this.hostIsListening && !!this.port; +}; + +BrowserMessagingPublisher.prototype.send = function(data,callback) { + var self = this; + callback = callback || function() {}; + // Check that we've been initialised by the host + if(!this.hostIsListening || !this.port) { + return false; + } + // Create a channel for the confirmation + var channel = new MessageChannel(); + channel.port1.addEventListener("message",function(event) { + if(event.data && event.data.verb === "OK") { + callback(null); + } else { + callback("BrowserMessagingPublisher for " + self.type + " error: " + (event.data || {}).verb); + } + channel.port1.close(); + }); + channel.port1.start(); + // Send the save request with the port for the response + this.port.postMessage(data,[channel.port2]); +}; + +BrowserMessagingPublisher.prototype.close = function() { + if(this.port) { + this.port.close(); + this.hostIsListening = false; + this.port = null; + } +}; + +exports.BrowserMessagingPublisher = BrowserMessagingPublisher; + +})(); diff --git a/core/modules/utils/parsetree.js b/core/modules/utils/parsetree.js index 410f92181..ba0e48b29 100644 --- a/core/modules/utils/parsetree.js +++ b/core/modules/utils/parsetree.js @@ -119,3 +119,19 @@ exports.getParseTreeText = function getParseTreeText(tree) { } return output.join(""); }; + +exports.getParser = function(type,options) { + options = options || {}; + // Select a parser + var Parser = $tw.Wiki.parsers[type]; + if(!Parser && $tw.utils.getFileExtensionInfo(type)) { + Parser = $tw.Wiki.parsers[$tw.utils.getFileExtensionInfo(type).type]; + } + if(!Parser) { + Parser = $tw.Wiki.parsers[options.defaultType || "text/vnd.tiddlywiki"]; + } + if(!Parser) { + return null; + } + return Parser; +}; diff --git a/core/modules/utils/utils.js b/core/modules/utils/utils.js index 621b3572e..951d2ccc8 100644 --- a/core/modules/utils/utils.js +++ b/core/modules/utils/utils.js @@ -9,8 +9,6 @@ Various static utility functions. "use strict"; -var base64utf8 = require("$:/core/modules/utils/base64-utf8/base64-utf8.module.js"); - /* Display a message, in colour if we're on a terminal */ @@ -854,22 +852,50 @@ if(typeof window !== 'undefined') { } } +exports.base64ToBytes = function(base64) { + const binString = exports.atob(base64); + return Uint8Array.from(binString, (m) => m.codePointAt(0)); +}; + +exports.bytesToBase64 = function(bytes) { + const binString = Array.from(bytes, (byte) => String.fromCodePoint(byte)).join(""); + return exports.btoa(binString); +}; + +exports.base64EncodeUtf8 = function(str) { + if ($tw.browser) { + return exports.bytesToBase64(new TextEncoder().encode(str)); + } else { + const buff = Buffer.from(str, "utf-8"); + return buff.toString("base64"); + } +}; + +exports.base64DecodeUtf8 = function(str) { + if ($tw.browser) { + return new TextDecoder().decode(exports.base64ToBytes(str)); + } else { + const buff = Buffer.from(str, "base64"); + return buff.toString("utf-8"); + } +}; + /* Decode a base64 string */ exports.base64Decode = function(string64,binary,urlsafe) { - var encoded = urlsafe ? string64.replace(/_/g,'/').replace(/-/g,'+') : string64; + const encoded = urlsafe ? string64.replace(/_/g,'/').replace(/-/g,'+') : string64; if(binary) return exports.atob(encoded) - else return base64utf8.base64.decode.call(base64utf8,encoded); + else return exports.base64DecodeUtf8(encoded); }; /* Encode a string to base64 */ exports.base64Encode = function(string64,binary,urlsafe) { - var encoded; + let encoded; if(binary) encoded = exports.btoa(string64); - else encoded = base64utf8.base64.encode.call(base64utf8,string64); + else encoded = exports.base64EncodeUtf8(string64); if(urlsafe) { encoded = encoded.replace(/\+/g,'-').replace(/\//g,'_'); } @@ -1035,7 +1061,7 @@ exports.makeCompareFunction = function(type,options) { return compare(dateA,dateB); }, "version": function(a,b) { - return $tw.utils.compareVersions(a,b); + return compare($tw.utils.compareVersions(a,b),0); }, "alphanumeric": function(a,b) { if(!isCaseSensitive) { diff --git a/core/modules/widgets/audio.js b/core/modules/widgets/audio.js new file mode 100644 index 000000000..7bd073c4f --- /dev/null +++ b/core/modules/widgets/audio.js @@ -0,0 +1,103 @@ +/*\ +title: $:/core/modules/widgets/audio.js +type: application/javascript +module-type: widget + +Basic Audio widget for displaying audio files. +This is a simple implementation that can be overridden by plugins +for more advanced functionality. + +\*/ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var AudioWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +AudioWidget.prototype = new Widget(); + +/* +Render this widget into the DOM +*/ +AudioWidget.prototype.render = function(parent,nextSibling) { + this.parentDomNode = parent; + this.computeAttributes(); + this.execute(); + + // Create audio element + var audioElement = this.document.createElement("audio"); + audioElement.setAttribute("controls", this.getAttribute("controls", "controls")); + audioElement.setAttribute("style", this.getAttribute("style", "width: 100%; object-fit: contain")); + audioElement.className = "tw-audio-element"; + + // Set source + if(this.audioSource) { + if (this.audioSource.indexOf("data:") === 0) { + audioElement.setAttribute("src", this.audioSource); + } else { + var sourceElement = this.document.createElement("source"); + sourceElement.setAttribute("src", this.audioSource); + if(this.audioType) { + sourceElement.setAttribute("type", this.audioType); + } + audioElement.appendChild(sourceElement); + } + } + + // Insert the audio into the DOM + parent.insertBefore(audioElement, nextSibling); + this.domNodes.push(audioElement); +}; + +/* +Compute the internal state of the widget +*/ +AudioWidget.prototype.execute = function() { + // Get the audio source and type + this.audioSource = this.getAttribute("src"); + this.audioType = this.getAttribute("type"); + this.audioControls = this.getAttribute("controls", "controls"); + + // Try to get from tiddler attribute + if(!this.audioSource && this.getAttribute("tiddler")) { + var tiddlerTitle = this.getAttribute("tiddler"); + var tiddler = this.wiki.getTiddler(tiddlerTitle); + if(tiddler) { + if(tiddler.fields._canonical_uri) { + this.audioSource = tiddler.fields._canonical_uri; + this.audioType = tiddler.fields.type; + } else if(tiddler.fields.text) { + this.audioSource = "data:" + tiddler.fields.type + ";base64," + tiddler.fields.text; + this.audioType = tiddler.fields.type; + } + } + } + + // Make sure we have a tiddler for saving timestamps + this.tiddlerTitle = this.getAttribute("tiddler"); +}; + +/* +Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering +*/ +AudioWidget.prototype.refresh = function(changedTiddlers) { + var changedAttributes = this.computeAttributes(); + if(changedAttributes.src || changedAttributes.type || changedAttributes.controls || changedAttributes.tiddler) { + this.refreshSelf(); + return true; + } else { + return false; + } +}; + +exports.audio = AudioWidget; + + diff --git a/core/modules/widgets/button.js b/core/modules/widgets/button.js index 68f2fcd11..8f6f14376 100644 --- a/core/modules/widgets/button.js +++ b/core/modules/widgets/button.js @@ -61,6 +61,10 @@ ButtonWidget.prototype.render = function(parent,nextSibling) { sourcePrefix: "data-", destPrefix: "data-" }); + this.assignAttributes(domNode,{ + sourcePrefix: "aria-", + destPrefix: "aria-" + }); // Assign other attributes if(this.style) { domNode.setAttribute("style",this.style); @@ -68,9 +72,6 @@ ButtonWidget.prototype.render = function(parent,nextSibling) { if(this.tooltip) { domNode.setAttribute("title",this.tooltip); } - if(this["aria-label"]) { - domNode.setAttribute("aria-label",this["aria-label"]); - } if (this.role) { domNode.setAttribute("role", this.role); } @@ -215,7 +216,6 @@ ButtonWidget.prototype.execute = function() { this.setTo = this.getAttribute("setTo"); this.popup = this.getAttribute("popup"); this.hover = this.getAttribute("hover"); - this["aria-label"] = this.getAttribute("aria-label"); this.role = this.getAttribute("role"); this.tooltip = this.getAttribute("tooltip"); this.style = this.getAttribute("style"); @@ -271,6 +271,10 @@ ButtonWidget.prototype.refresh = function(changedTiddlers) { sourcePrefix: "data-", destPrefix: "data-" }); + this.assignAttributes(this.domNodes[0],{ + sourcePrefix: "aria-", + destPrefix: "aria-" + }); } return this.refreshChildren(changedTiddlers); }; diff --git a/core/modules/widgets/element.js b/core/modules/widgets/element.js index 34fd3b9ff..8b0a88e86 100755 --- a/core/modules/widgets/element.js +++ b/core/modules/widgets/element.js @@ -74,6 +74,8 @@ ElementWidget.prototype.render = function(parent,nextSibling) { // Create the DOM node and render children var domNode = this.document.createElementNS(this.namespace,this.tag); this.assignAttributes(domNode,{excludeEventAttributes: true}); + // Allow hooks to manipulate the DOM node. Eg: Add debug info + $tw.hooks.invokeHook("th-dom-rendering-element", domNode, this); parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); this.domNodes.push(domNode); diff --git a/core/modules/widgets/eventcatcher.js b/core/modules/widgets/eventcatcher.js index 70dc99202..aac6519a4 100644 --- a/core/modules/widgets/eventcatcher.js +++ b/core/modules/widgets/eventcatcher.js @@ -44,7 +44,7 @@ EventWidget.prototype.render = function(parent,nextSibling) { domNode.addEventListener(type,function(event) { var selector = self.getAttribute("selector"), matchSelector = self.getAttribute("matchSelector"), - actions = self.getAttribute("$"+type) || self.getAttribute("actions-"+type), + actions = self.getAttribute("$"+type), stopPropagation = self.getAttribute("stopPropagation","onaction"), selectedNode = event.target, selectedNodeRect, @@ -122,9 +122,6 @@ EventWidget.prototype.execute = function() { self.types.push(key.slice(1)); } }); - if(!this.types.length) { - this.types = this.getAttribute("events","").split(" "); - } this.elementTag = this.getAttribute("tag"); // Make child widgets this.makeChildWidgets(); diff --git a/core/modules/widgets/importvariables.js b/core/modules/widgets/importvariables.js index befee4a90..597b5bc56 100644 --- a/core/modules/widgets/importvariables.js +++ b/core/modules/widgets/importvariables.js @@ -49,7 +49,8 @@ ImportVariablesWidget.prototype.execute = function(tiddlerList) { var parser = widgetPointer.wiki.parseTiddler(title,{parseAsInline:true, configTrimWhiteSpace:false}); if(parser) { var parseTreeNode = parser.tree[0]; - while(parseTreeNode && ["setvariable","set","parameters"].indexOf(parseTreeNode.type) !== -1) { + // process AST nodes generated by pragma rules. + while(parseTreeNode && ["setvariable","set","parameters","void"].indexOf(parseTreeNode.type) !== -1) { var node = { type: "set", attributes: parseTreeNode.attributes, @@ -82,7 +83,7 @@ ImportVariablesWidget.prototype.execute = function(tiddlerList) { // this widget. If it needs to refresh, // it'll do so along with the the whole // importvariable tree. - if (widgetPointer != this) { + if(widgetPointer != this) { widgetPointer.makeChildWidgets = function(){}; } widgetPointer = widgetPointer.children[0]; @@ -93,7 +94,7 @@ ImportVariablesWidget.prototype.execute = function(tiddlerList) { } }); - if (widgetPointer != this) { + if(widgetPointer != this) { widgetPointer.parseTreeNode.children = this.parseTreeNode.children; } else { widgetPointer.makeChildWidgets(); diff --git a/core/modules/widgets/link.js b/core/modules/widgets/link.js index c8b54818d..d2c599542 100755 --- a/core/modules/widgets/link.js +++ b/core/modules/widgets/link.js @@ -45,6 +45,10 @@ LinkWidget.prototype.render = function(parent,nextSibling) { sourcePrefix: "data-", destPrefix: "data-" }); + this.assignAttributes(domNode,{ + sourcePrefix: "aria-", + destPrefix: "aria-" + }); parent.insertBefore(domNode,nextSibling); this.renderChildren(domNode,null); this.domNodes.push(domNode); @@ -125,9 +129,13 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) { }); domNode.setAttribute("title",tooltipText); } - if(this["aria-label"]) { - domNode.setAttribute("aria-label",this["aria-label"]); + if(this.role) { + domNode.setAttribute("role",this.role); } + this.assignAttributes(domNode,{ + sourcePrefix: "aria-", + destPrefix: "aria-" + }) // Add a click event handler $tw.utils.addEventListeners(domNode,[ {name: "click", handlerObject: this, handlerMethod: "handleClickEvent"}, @@ -139,6 +147,8 @@ LinkWidget.prototype.renderLink = function(parent,nextSibling) { dragTiddlerFn: function() {return self.to;}, widget: this }); + } else if(this.draggable === "no") { + domNode.setAttribute("draggable","false"); } // Assign data- attributes this.assignAttributes(domNode,{ @@ -188,7 +198,7 @@ LinkWidget.prototype.execute = function() { // Pick up our attributes this.to = this.getAttribute("to",this.getVariable("currentTiddler")); this.tooltip = this.getAttribute("tooltip"); - this["aria-label"] = this.getAttribute("aria-label"); + this.role = this.getAttribute("role"); this.linkClasses = this.getAttribute("class"); this.overrideClasses = this.getAttribute("overrideClass"); this.tabIndex = this.getAttribute("tabindex"); diff --git a/core/modules/widgets/range.js b/core/modules/widgets/range.js index 8d039b1f1..7c2d60c45 100644 --- a/core/modules/widgets/range.js +++ b/core/modules/widgets/range.js @@ -94,8 +94,6 @@ RangeWidget.prototype.getActionVariables = function(options) { // actionsStart RangeWidget.prototype.handleMouseDownEvent = function(event) { - this.mouseDown = true; // TODO remove once IE is gone. - this.startValue = this.inputDomNode.value; // TODO remove this line once IE is gone! this.handleEvent(event); // Trigger actions if(this.actionsMouseDown) { @@ -106,26 +104,16 @@ RangeWidget.prototype.handleMouseDownEvent = function(event) { // actionsStop RangeWidget.prototype.handleMouseUpEvent = function(event) { - this.mouseDown = false; // TODO remove once IE is gone. this.handleEvent(event); // Trigger actions if(this.actionsMouseUp) { var variables = this.getActionVariables() this.invokeActionString(this.actionsMouseUp,this,event,variables); } - // TODO remove the following if() once IE is gone! - if ($tw.browser.isIE) { - if (this.startValue !== this.inputDomNode.value) { - this.handleChangeEvent(event); - this.startValue = this.inputDomNode.value; - } - } } RangeWidget.prototype.handleChangeEvent = function(event) { - if (this.mouseDown) { // TODO refactor this function once IE is gone. - this.handleInputEvent(event); - } + this.handleInputEvent(event); }; RangeWidget.prototype.handleInputEvent = function(event) { @@ -152,8 +140,6 @@ RangeWidget.prototype.handleEvent = function(event) { Compute the internal state of the widget */ RangeWidget.prototype.execute = function() { - // TODO remove the next 1 lines once IE is gone! - this.mouseUp = true; // Needed for IE10 // Get the parameters from the attributes this.tiddlerTitle = this.getAttribute("tiddler",this.getVariable("currentTiddler")); this.tiddlerField = this.getAttribute("field","text"); diff --git a/core/modules/widgets/void.js b/core/modules/widgets/void.js new file mode 100755 index 000000000..de009c506 --- /dev/null +++ b/core/modules/widgets/void.js @@ -0,0 +1,23 @@ +/*\ +title: $:/core/modules/widgets/void.js +type: application/javascript +module-type: widget + +Void widget that corresponds to pragma and comment AST nodes, etc. It does not render itself but renders all its children. + +\*/ + +"use strict"; + +var Widget = require("$:/core/modules/widgets/widget.js").widget; + +var VoidNodeWidget = function(parseTreeNode,options) { + this.initialise(parseTreeNode,options); +}; + +/* +Inherit from the base widget class +*/ +VoidNodeWidget.prototype = new Widget(); + +exports.void = VoidNodeWidget; diff --git a/core/modules/wiki.js b/core/modules/wiki.js index 28338602c..551595c8e 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1059,17 +1059,7 @@ Options include: exports.parseText = function(type,text,options) { text = text || ""; options = options || {}; - // Select a parser - var Parser = $tw.Wiki.parsers[type]; - if(!Parser && $tw.utils.getFileExtensionInfo(type)) { - Parser = $tw.Wiki.parsers[$tw.utils.getFileExtensionInfo(type).type]; - } - if(!Parser) { - Parser = $tw.Wiki.parsers[options.defaultType || "text/vnd.tiddlywiki"]; - } - if(!Parser) { - return null; - } + var Parser = $tw.utils.getParser(type,options) // Return the parser instance return new Parser(type,text,{ parseAsInline: options.parseAsInline, @@ -1083,7 +1073,7 @@ exports.parseText = function(type,text,options) { Parse a tiddler according to its MIME type */ exports.parseTiddler = function(title,options) { - options = $tw.utils.extend({},options); + options = options || {}; var cacheType = options.parseAsInline ? "inlineParseTree" : "blockParseTree", tiddler = this.getTiddler(title), self = this; @@ -1443,7 +1433,7 @@ exports.search = function(text,options) { // Don't search the text field if the content type is binary var fieldName = searchFields[fieldIndex]; if(fieldName === "text" && contentTypeInfo.encoding !== "utf8") { - break; + continue; } var str = tiddler.fields[fieldName], t; diff --git a/core/palettes/BrightMute.tid b/core/palettes/BrightMute.tid index e763150a7..bda0282b3 100644 --- a/core/palettes/BrightMute.tid +++ b/core/palettes/BrightMute.tid @@ -47,6 +47,7 @@ modal-footer-background: #f5f5f5 modal-footer-border: #dddddd modal-header-border: #eeeeee muted-foreground: #bbb +network-activity-foreground: <> notification-background: #ffffdd notification-border: #999999 page-background: #6f6f70 @@ -56,22 +57,26 @@ primary: #29a6ee select-tag-background: select-tag-foreground: sidebar-button-foreground: <> -sidebar-controls-foreground-hover: #000000 +sidebar-controls-foreground-hover: #222222 sidebar-controls-foreground: #c2c1c2 -sidebar-foreground-shadow: rgba(255,255,255,0) +sidebar-foreground-shadow: transparent sidebar-foreground: #d3d2d4 -sidebar-muted-foreground-hover: #444444 +sidebar-muted-foreground-hover: #333333 sidebar-muted-foreground: #c0c0c0 sidebar-tab-background-selected: #6f6f70 sidebar-tab-background: #666667 sidebar-tab-border-selected: #999 sidebar-tab-border: #515151 sidebar-tab-divider: #999 -sidebar-tab-foreground-selected: -sidebar-tab-foreground: #999 +sidebar-tab-foreground-selected: #bfbfbf +sidebar-tab-foreground: #b0b0b0 sidebar-tiddler-link-foreground-hover: #444444 -sidebar-tiddler-link-foreground: #d1d0d2 +sidebar-tiddler-link-foreground: #aaaaaa site-title-foreground: <> +stability-deprecated: #bf616a +stability-experimental: #d08770 +stability-legacy: #88c0d0 +stability-stable: #a3be8c static-alert-foreground: #aaaaaa tab-background-selected: #ffffff tab-background: #d8d8d8 diff --git a/core/templates/external-js/tiddlywiki5-external-js.html.tid b/core/templates/external-js/tiddlywiki5-external-js.html.tid index b161584d7..39ded2b5e 100644 --- a/core/templates/external-js/tiddlywiki5-external-js.html.tid +++ b/core/templates/external-js/tiddlywiki5-external-js.html.tid @@ -1,12 +1,13 @@ title: $:/core/templates/tiddlywiki5-external-js.html <$set name="saveTiddlerAndShadowsFilter" filter="[subfilter] [subfilterplugintiddlers[]]"> +<$set name="rawMarkupFilter" filter="[enlist] +[[$:/core]plugintiddlers[]]"> ` `{{$:/core/templates/MOTW.html}}` -`{{{ [enlisttag[$:/tags/RawMarkupWikified/TopHead]] ||$:/core/templates/raw-static-tiddler}}}` +`{{{ [enlisttag[$:/tags/RawMarkupWikified/TopHead]] ||$:/core/templates/raw-static-tiddler}}}` @@ -22,13 +23,13 @@ title: $:/core/templates/tiddlywiki5-external-js.html -`{{{ [enlisttag[$:/core/wiki/rawmarkup]] ||$:/core/templates/plain-text-tiddler}}}` -`{{{ [enlisttag[$:/tags/RawMarkup]] ||$:/core/templates/plain-text-tiddler}}}` -`{{{ [enlisttag[$:/tags/RawMarkupWikified]] ||$:/core/templates/raw-static-tiddler}}}` +`{{{ [enlisttag[$:/core/wiki/rawmarkup]] ||$:/core/templates/plain-text-tiddler}}}` +`{{{ [enlisttag[$:/tags/RawMarkup]] ||$:/core/templates/plain-text-tiddler}}}` +`{{{ [enlisttag[$:/tags/RawMarkupWikified]] ||$:/core/templates/raw-static-tiddler}}}` -`{{{ [enlisttag[$:/tags/RawMarkupWikified/TopBody]] ||$:/core/templates/raw-static-tiddler}}}` +`{{{ [enlisttag[$:/tags/RawMarkupWikified/TopBody]] ||$:/core/templates/raw-static-tiddler}}}`
`{{$:/boot/boot.css||$:/core/templates/css-tiddler}}` @@ -42,9 +43,10 @@ title: $:/core/templates/tiddlywiki5-external-js.html `{{$:/core/templates/store.area.template.html}}` -`{{{ [enlisttag[$:/tags/RawMarkupWikified/BottomBody]] ||$:/core/templates/raw-static-tiddler}}}` +`{{{ [enlisttag[$:/tags/RawMarkupWikified/BottomBody]] ||$:/core/templates/raw-static-tiddler}}}` ` + \ No newline at end of file diff --git a/core/ui/AdvancedSearch/Filter.tid b/core/ui/AdvancedSearch/Filter.tid index 7369e4c40..4005ebbdc 100644 --- a/core/ui/AdvancedSearch/Filter.tid +++ b/core/ui/AdvancedSearch/Filter.tid @@ -62,28 +62,34 @@ caption: {{$:/language/Search/Filter/Caption}} \end +\procedure input-actions() +<%if [match[((input-tab-right))]] %> +<> +<%elseif [match[((input-tab-left))]] %> +<> +<%endif%> +\end + \whitespace trim <>