diff --git a/core/modules/commands/save.js b/core/modules/commands/save.js index 9769cec69..3cb7ef08c 100644 --- a/core/modules/commands/save.js +++ b/core/modules/commands/save.js @@ -43,7 +43,9 @@ Saves individual tiddlers in their raw text or binary format to the specified fi directory: path.resolve(self.commander.outputPath), pathFilters: [filenameFilter], wiki: wiki, - fileInfo: {} + fileInfo: { + overwrite: true + } }); if(self.commander.verbose) { console.log("Saving \"" + title + "\" to \"" + fileInfo.filepath + "\""); diff --git a/core/modules/server/server.js b/core/modules/server/server.js index ce40bb3ae..a700fb146 100644 --- a/core/modules/server/server.js +++ b/core/modules/server/server.js @@ -140,6 +140,11 @@ function sendResponse(request,response,statusCode,headers,data,encoding) { return; } } + } else { + // RFC 7231, 6.1. Overview of Status Codes: + // Browser clients may cache 200, 203, 204, 206, 300, 301, + // 404, 405, 410, 414, and 501 unless given explicit cache controls + headers["Cache-Control"] = headers["Cache-Control"] || "no-store"; } /* If the gzip=yes is set, check if the user agent permits compression. If so, diff --git a/core/modules/utils/filesystem.js b/core/modules/utils/filesystem.js index 1ba34323e..5319e0481 100644 --- a/core/modules/utils/filesystem.js +++ b/core/modules/utils/filesystem.js @@ -316,11 +316,13 @@ Options include: pathFilters: optional array of filters to be used to generate the base path wiki: optional wiki for evaluating the pathFilters fileInfo: an existing fileInfo object to check against + fileInfo.overwrite: if true, turns off filename clash numbers (defaults to false) */ exports.generateTiddlerFilepath = function(title,options) { var directory = options.directory || "", extension = options.extension || "", originalpath = (options.fileInfo && options.fileInfo.originalpath) ? options.fileInfo.originalpath : "", + overwrite = options.fileInfo && options.fileInfo.overwrite || false, filepath; // Check if any of the pathFilters applies if(options.pathFilters && options.wiki) { @@ -381,19 +383,20 @@ exports.generateTiddlerFilepath = function(title,options) { filepath += char.charCodeAt(0).toString(); }); } - // Add a uniquifier if the file already exists - var fullPath, oldPath = (options.fileInfo) ? options.fileInfo.filepath : undefined, + // Add a uniquifier if the file already exists (default) + var fullPath = path.resolve(directory, filepath + extension); + if (!overwrite) { + var oldPath = (options.fileInfo) ? options.fileInfo.filepath : undefined, count = 0; - do { - fullPath = path.resolve(directory,filepath + (count ? "_" + count : "") + extension); - if(oldPath && oldPath == fullPath) { - break; - } - count++; - } while(fs.existsSync(fullPath)); + do { + fullPath = path.resolve(directory,filepath + (count ? "_" + count : "") + extension); + if(oldPath && oldPath == fullPath) break; + count++; + } while(fs.existsSync(fullPath)); + } // If the last write failed with an error, or if path does not start with: // the resolved options.directory, the resolved wikiPath directory, the wikiTiddlersPath directory, - // or the 'originalpath' directory, then $tw.utils.encodeURIComponentExtended() and resolve to tiddler directory. + // or the 'originalpath' directory, then $tw.utils.encodeURIComponentExtended() and resolve to options.directory. var writePath = $tw.hooks.invokeHook("th-make-tiddler-path",fullPath,fullPath), encode = (options.fileInfo || {writeError: false}).writeError == true; if(!encode) { diff --git a/core/modules/widgets/action-deletefield.js b/core/modules/widgets/action-deletefield.js index 54068471e..00f06562d 100644 --- a/core/modules/widgets/action-deletefield.js +++ b/core/modules/widgets/action-deletefield.js @@ -37,6 +37,7 @@ Compute the internal state of the widget DeleteFieldWidget.prototype.execute = function() { this.actionTiddler = this.getAttribute("$tiddler",this.getVariable("currentTiddler")); this.actionField = this.getAttribute("$field",null); + this.actionTimestamp = this.getAttribute("$timestamp","yes") === "yes"; }; /* @@ -69,11 +70,15 @@ DeleteFieldWidget.prototype.invokeAction = function(triggeringWidget,event) { $tw.utils.each(this.attributes,function(attribute,name) { if(name.charAt(0) !== "$" && name !== "title") { removeFields[name] = undefined; - hasChanged = true; + if(name in tiddler.fields) { + hasChanged = true; + } } }); if(hasChanged) { - this.wiki.addTiddler(new $tw.Tiddler(this.wiki.getCreationFields(),tiddler,removeFields,this.wiki.getModificationFields())); + var creationFields = this.actionTimestamp ? this.wiki.getCreationFields() : {}; + var modificationFields = this.actionTimestamp ? this.wiki.getModificationFields() : {}; + this.wiki.addTiddler(new $tw.Tiddler(creationFields,tiddler,removeFields,modificationFields)); } } return true; // Action was invoked diff --git a/core/templates/external-js/save-all-external-js.tid b/core/templates/external-js/save-all-external-js.tid index ff5bbc851..1f4908878 100644 --- a/core/templates/external-js/save-all-external-js.tid +++ b/core/templates/external-js/save-all-external-js.tid @@ -3,7 +3,7 @@ title: $:/core/save/all-external-js \whitespace trim \import [subfilter{$:/core/config/GlobalImportFilter}] \define saveTiddlerFilter() -[is[tiddler]] -[prefix[$:/state/popup/]] -[prefix[$:/temp/]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[[$:/core]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] $(publishFilter)$ +[is[tiddler]] -[prefix[$:/state/popup/]] -[prefix[$:/temp/]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[[$:/core]] -[[$:/boot/boot.css]] -[is[system]type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] $(publishFilter)$ \end diff --git a/core/templates/external-js/save-offline-external-js.tid b/core/templates/external-js/save-offline-external-js.tid index 564a34948..70cb8bbc0 100644 --- a/core/templates/external-js/save-offline-external-js.tid +++ b/core/templates/external-js/save-offline-external-js.tid @@ -3,7 +3,7 @@ title: $:/core/save/offline-external-js \whitespace trim \import [subfilter{$:/core/config/GlobalImportFilter}] \define saveTiddlerFilter() -[is[tiddler]] -[prefix[$:/state/popup/]] -[prefix[$:/temp/]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[[$:/core]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/tiddlywiki/tiddlyweb]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] $(publishFilter)$ +[is[tiddler]] -[prefix[$:/state/popup/]] -[prefix[$:/temp/]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[[$:/core]] -[[$:/plugins/tiddlywiki/filesystem]] -[[$:/plugins/tiddlywiki/tiddlyweb]] -[[$:/boot/boot.css]] -[is[system]type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] $(publishFilter)$ \end \define defaultCoreURL() tiddlywikicore-$(version)$.js <$let coreURL={{{ [[coreURL]is[variable]thenelse] }}}> diff --git a/core/templates/save-all.tid b/core/templates/save-all.tid index d7473ba5b..a316d1954 100644 --- a/core/templates/save-all.tid +++ b/core/templates/save-all.tid @@ -2,6 +2,6 @@ title: $:/core/save/all \import [subfilter{$:/core/config/GlobalImportFilter}] \define saveTiddlerFilter() -[is[tiddler]] -[prefix[$:/state/popup/]] -[prefix[$:/temp/]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] $(publishFilter)$ +[is[tiddler]] -[prefix[$:/state/popup/]] -[prefix[$:/temp/]] -[prefix[$:/HistoryList]] -[status[pending]plugin-type[import]] -[[$:/boot/boot.css]] -[is[system]type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] $(publishFilter)$ \end {{$:/core/templates/tiddlywiki5.html}} diff --git a/core/templates/save-empty.tid b/core/templates/save-empty.tid index 6f0da4822..0b1c33b59 100644 --- a/core/templates/save-empty.tid +++ b/core/templates/save-empty.tid @@ -1,6 +1,6 @@ title: $:/core/save/empty \define saveTiddlerFilter() -[is[system]] -[prefix[$:/state/popup/]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] +[is[system]] -[prefix[$:/state/popup/]] -[[$:/boot/boot.css]] -[is[system]type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] +[sort[title]] \end {{$:/core/templates/tiddlywiki5.html}} diff --git a/core/templates/save-lazy-all.tid b/core/templates/save-lazy-all.tid index a4b5cd6e9..da4353fba 100644 --- a/core/templates/save-lazy-all.tid +++ b/core/templates/save-lazy-all.tid @@ -1,7 +1,7 @@ title: $:/core/save/lazy-all \define saveTiddlerFilter() -[is[system]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] [is[tiddler]type[application/javascript]] +[sort[title]] +[is[system]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[is[system]type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] [is[tiddler]type[application/javascript]] +[sort[title]] \end \define skinnySaveTiddlerFilter() [!is[system]] -[type[application/javascript]] diff --git a/core/templates/save-lazy-images.tid b/core/templates/save-lazy-images.tid index 0a4a84295..b23b348f0 100644 --- a/core/templates/save-lazy-images.tid +++ b/core/templates/save-lazy-images.tid @@ -1,7 +1,7 @@ title: $:/core/save/lazy-images \define saveTiddlerFilter() -[is[tiddler]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[!is[system]is[image]] +[sort[title]] +[is[tiddler]] -[prefix[$:/state/popup/]] -[[$:/HistoryList]] -[[$:/boot/boot.css]] -[is[system]type[application/javascript]library[yes]] -[[$:/boot/boot.js]] -[[$:/boot/bootprefix.js]] -[!is[system]is[image]] +[sort[title]] \end \define skinnySaveTiddlerFilter() [!is[system]is[image]] diff --git a/editions/test/tiddlers/tests/test-action-deletefield.js b/editions/test/tiddlers/tests/test-action-deletefield.js new file mode 100644 index 000000000..876f44d8e --- /dev/null +++ b/editions/test/tiddlers/tests/test-action-deletefield.js @@ -0,0 +1,176 @@ +/*\ +title: test-action-deletefield.js +type: application/javascript +tags: [[$:/tags/test-spec]] + +Tests <$action-deletefield />. + +\*/ +(function(){ + +/* jslint node: true, browser: true */ +/* eslint-env node, browser, jasmine */ +/* eslint no-mixed-spaces-and-tabs: ["error", "smart-tabs"]*/ +/* global $tw, require */ +"use strict"; + +describe("<$action-deletefield /> tests", function() { + +const TEST_TIDDLER_TITLE = "TargetTiddler"; +const TEST_TIDDLER_MODIFIED = "20240313114828368"; + +function setupWiki(condition, targetField, wikiOptions) { + // Create a wiki + var wiki = new $tw.Wiki({}); + var tiddlers = [{ + title: "Root", + text: "Some dummy content" + }]; + var tiddler; + if(condition.targetTiddlerExists) { + var fields = { + title: TEST_TIDDLER_TITLE, + }; + if(condition.modifiedFieldExists) { + fields.modified = TEST_TIDDLER_MODIFIED; + } + if(condition.targetFieldExists) { + fields[targetField] = "some text"; + } + var tiddler = new $tw.Tiddler(fields); + tiddlers.push(tiddler); + } + wiki.addTiddlers(tiddlers); + wiki.addIndexersToWiki(); + var widgetNode = wiki.makeTranscludeWidget("Root",{document: $tw.fakeDocument, parseAsInline: true}); + var container = $tw.fakeDocument.createElement("div"); + widgetNode.render(container,null); + return { + wiki: wiki, + widgetNode: widgetNode, + contaienr: container, + tiddler: tiddler, + }; +} + +function generateTestConditions() { + var conditions = []; + + $tw.utils.each([true, false], function(tiddlerArgumentIsPresent) { + $tw.utils.each([true, false], function(targetTiddlerExists) { + $tw.utils.each([true, false], function(targetFieldExists) { + $tw.utils.each([true, false], function(fieldArgumentIsUsed) { + $tw.utils.each([true, false], function(modifiedFieldExists) { + $tw.utils.each(["", "yes", "no"], function(timestampArgument) { + conditions.push({ + tiddlerArgumentIsPresent: tiddlerArgumentIsPresent, + targetTiddlerExists: targetTiddlerExists, + targetFieldExists: targetFieldExists, + fieldArgumentIsUsed: fieldArgumentIsUsed, + modifiedFieldExists: modifiedFieldExists, + timestampArgument: timestampArgument, + }); + }); + }); + }); + }); + }); + }); + + return conditions; +} + +function generateActionWikitext(condition, targetField) { + var actionPieces = [ + "<$action-deletefield", + (condition.tiddlerArgumentIsPresent ? "$tiddler='" + TEST_TIDDLER_TITLE + "'" : ""), + (condition.fieldArgumentIsUsed ? "$field='" + targetField + "'" : targetField), + (condition.timestampArgument !== "" ? "$timestamp='" + condition.timestampArgument + "'" : ""), + "/>", + ]; + + return actionPieces.join(" "); +} + +function generateTestContext(action, tiddler) { + var expectationContext = "action: " + action + "\ntiddler:\n\n"; + if(tiddler) { + expectationContext += tiddler.getFieldStringBlock({exclude: ["text"]}); + if(tiddler.text) { + expectationContext += "\n\n" + tiddler.text; + } + expectationContext += "\n\n"; + } else { + expectationContext += "null"; + } + + return expectationContext; +} + +it("should correctly delete fields", function() { + var fields = ['caption', 'description', 'text']; + + var conditions = generateTestConditions(); + + $tw.utils.each(conditions, function(condition) { + $tw.utils.each(fields, function(field) { + var info = setupWiki(condition, field); + var originalTiddler = info.tiddler; + + var invokeActions = function(actions) { + info.widgetNode.invokeActionString(actions,info.widgetNode,null,{ + currentTiddler: TEST_TIDDLER_TITLE, + }); + }; + + var action = generateActionWikitext(condition,field); + + invokeActions(action); + + var testContext = generateTestContext(action,originalTiddler); + + var tiddler = info.wiki.getTiddler(TEST_TIDDLER_TITLE); + if(originalTiddler) { + // assert that the tiddler doesn't have the target field anymore + expect(tiddler.hasField(field)).withContext(testContext).toBeFalsy(); + + var targetFieldWasPresent = originalTiddler.hasField(field); + var updateTimestamps = condition.timestampArgument !== "no"; + + // "created" should exist if it did beforehand, or if the tiddler changed and we asked the widget to update timestamps + var createdFieldShouldExist = originalTiddler.hasField("created") || (targetFieldWasPresent && updateTimestamps); + + // "created" should change only if it didn't exist beforehand and the tiddler changed and we asked the widget to update timestamps + var createdFieldShouldChange = !originalTiddler.hasField("created") && (targetFieldWasPresent && updateTimestamps); + + // "modified" should exist if it did beforehand, or if the tiddler changed and we asked the widget to update timestamps + var modifiedFieldShouldExist = originalTiddler.hasField("modified") || (targetFieldWasPresent && updateTimestamps); + + // "modified" should change if the tiddler changed and we asked the widget to update timestamps + var modifiedFieldShouldChange = targetFieldWasPresent && updateTimestamps; + + expect(tiddler.hasField("created")).withContext(testContext).toBe(createdFieldShouldExist); + expect(tiddler.hasField("modified")).withContext(testContext).toBe(modifiedFieldShouldExist); + + if(createdFieldShouldChange) { + expect(tiddler.fields.created).withContext(testContext).not.toEqual(originalTiddler.fields.created); + } else { + expect(tiddler.fields.created).withContext(testContext).toEqual(originalTiddler.fields.created); + } + + if(modifiedFieldShouldChange) { + expect(tiddler.fields.modified).withContext(testContext).not.toEqual(originalTiddler.fields.modified); + } else { + expect(tiddler.fields.modified).withContext(testContext).toEqual(originalTiddler.fields.modified); + } + } else { + // assert that the tiddler didn't get created if it didn't exist already + expect(tiddler).withContext(testContext).toBeUndefined(); + } + }); + }); +}); + +}); + +})(); diff --git a/editions/tw5.com/tiddlers/widgets/ActionDeleteFieldWidget.tid b/editions/tw5.com/tiddlers/widgets/ActionDeleteFieldWidget.tid index 2855804fd..5b797232b 100644 --- a/editions/tw5.com/tiddlers/widgets/ActionDeleteFieldWidget.tid +++ b/editions/tw5.com/tiddlers/widgets/ActionDeleteFieldWidget.tid @@ -16,6 +16,7 @@ The ''action-deletefield'' widget is invisible. Any content within it is ignored |!Attribute |!Description | |$tiddler |The title of the tiddler whose fields are to be modified (if not provided defaults to the [[current tiddler|Current Tiddler]]) | |$field |Optional name of a field to delete | +|$timestamp |<<.from-version "5.3.4">> Specifies whether the timestamp(s) of the target tiddler will be updated (''modified'' and ''modifier'', plus ''created'' and ''creator'' for newly created tiddlers). Can be "yes" (the default) or "no" | |//{any attributes not starting with $}// |Each attribute name specifies a field to be deleted. The attribute value is ignored and need not be specified | ! Examples