diff --git a/core/modules/filters/duplicateslugs.js b/core/modules/filters/duplicateslugs.js new file mode 100644 index 000000000..dbd2f23e7 --- /dev/null +++ b/core/modules/filters/duplicateslugs.js @@ -0,0 +1,36 @@ +/*\ +title: $:/core/modules/filters/duplicateslugs.js +type: application/javascript +module-type: filteroperator + +Filter function for [duplicateslugs[]] + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +/* +Export our filter function +*/ +exports.duplicateslugs = function(source,operator,options) { + var slugs = Object.create(null), // Hashmap by slug of title, replaced with "true" if the duplicate title has already been output + results = []; + source(function(tiddler,title) { + var slug = options.wiki.slugify(title); + if(slug in slugs) { + if(slugs[slug] !== true) { + results.push(slugs[slug]); + slugs[slug] = true; + } + results.push(title); + } else { + slugs[slug] = title; + } + }); + return results; +}; + +})(); diff --git a/core/modules/filters/slugify.js b/core/modules/filters/slugify.js new file mode 100644 index 000000000..da091506b --- /dev/null +++ b/core/modules/filters/slugify.js @@ -0,0 +1,23 @@ +/*\ +title: $:/plugins/tiddlywiki/static/filters/slugify.js +type: application/javascript +module-type: filteroperator + +Filter operator for slugifying a tiddler title + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.slugify = function(source,operator,options) { + var results = []; + source(function(tiddler,title) { + results.push(options.wiki.slugify(title)); + }); + return results; +}; + +})(); diff --git a/core/modules/wiki.js b/core/modules/wiki.js index ff8bc6287..80189f7a9 100755 --- a/core/modules/wiki.js +++ b/core/modules/wiki.js @@ -1503,5 +1503,30 @@ exports.doesPluginInfoRequireReload = function(pluginInfo) { } }; +exports.slugify = function(title,options) { + var tiddler = this.getTiddler(title), + slug; + if(tiddler && tiddler.fields.slug) { + slug = tiddler.fields.slug; + } else { + slug = $tw.utils.transliterate(title.toString().toLowerCase()) // Replace diacritics with basic lowercase ASCII + .replace(/\s+/g,"-") // Replace spaces with - + .replace(/[^\w\-\.]+/g,"") // Remove all non-word chars except dash and dot + .replace(/\-\-+/g,"-") // Replace multiple - with single - + .replace(/^-+/,"") // Trim - from start of text + .replace(/-+$/,""); // Trim - from end of text + } + // If the resulting slug is blank (eg because the title is just punctuation characters) + if(!slug) { + // ...then just use the character codes of the title + var result = []; + $tw.utils.each(title.split(""),function(char) { + result.push(char.charCodeAt(0).toString()); + }); + slug = result.join("-"); + } + return slug; +}; + })(); diff --git a/editions/test/tiddlers/tests/test-filters.js b/editions/test/tiddlers/tests/test-filters.js index 61e229540..9b6ed25d6 100644 --- a/editions/test/tiddlers/tests/test-filters.js +++ b/editions/test/tiddlers/tests/test-filters.js @@ -94,6 +94,7 @@ function setupWiki(wikiOptions) { tags: ["one"], cost: "123", value: "120", + slug: "tiddler-one", authors: "Joe Bloggs", modifier: "JoeBloggs", modified: "201304152222"}); @@ -103,6 +104,7 @@ function setupWiki(wikiOptions) { tags: ["two"], cost: "42", value: "190", + slug: "tiddler-two", authors: "[[John Doe]]", modifier: "John", modified: "201304152211"}); @@ -669,6 +671,14 @@ function runTests(wiki) { expect(wiki.filterTiddlers("b a b c +[sortby[b a c b]]").join(",")).toBe("b,a,c"); }); + it("should handle the slugify operator", function() { + expect(wiki.filterTiddlers("[[Joe Bloggs]slugify[]]").join(",")).toBe("joe-bloggs"); + expect(wiki.filterTiddlers("[[Joe Bloggs2]slugify[]]").join(",")).toBe("joe-bloggs2"); + expect(wiki.filterTiddlers("[[@£$%^&*((]slugify[]]").join(",")).toBe("64-163-36-37-94-38-42-40-40"); + expect(wiki.filterTiddlers("One one ONE O!N!E +[slugify[]]").join(",")).toBe("one,one,one,one"); + expect(wiki.filterTiddlers("TiddlerOne $:/TiddlerTwo +[slugify[]]").join(",")).toBe("tiddler-one,tiddler-two"); + }); + it("should handle the sortsub operator", function() { var widget = require("$:/core/modules/widgets/widget.js"); var rootWidget = new widget.widget({ type:"widget", children:[ {type:"widget", children:[]} ] }, diff --git a/editions/tw5.com/tiddlers/filters/duplicateslugs Operator.tid b/editions/tw5.com/tiddlers/filters/duplicateslugs Operator.tid new file mode 100644 index 000000000..43c89a02d --- /dev/null +++ b/editions/tw5.com/tiddlers/filters/duplicateslugs Operator.tid @@ -0,0 +1,22 @@ +caption: duplicateslugs +created: 20200509141702846 +modified: 20200509141702846 +op-input: a [[selection of titles|Title Selection]] +op-output: the input titles transformed so that they only contain lower case letters, numbers, periods, dashes and underscores +op-purpose: returns each item in the list in a human-readable form for use in URLs or filenames +tags: [[Filter Operators]] +title: duplicateslugs Operator +type: text/vnd.tiddlywiki + +<<.from-version "5.1.23">> The <<.olink slugify>> can be used to transform arbitrary tiddler titles into human readable strings suitable for use in URLs or filenames. However, itis possible for multiple different titles to slugify to the same string. The <<.olink duplicateslugs>> operator can be used to display a warning. For example: + +<$macrocall $name='wikitext-example-without-html' +src='<$list filter="[!is[system]duplicateslugs[]limit[1]]" emptyMessage="There are no duplicate slugs"> +The following tiddlers have duplicate slugs: + +