From b7562f0c7b08b81a7965b37c57d496846e1660b5 Mon Sep 17 00:00:00 2001 From: Jeremy Ruston Date: Sat, 14 Oct 2023 09:41:21 +0100 Subject: [PATCH] Conditional Shortcut Syntax (#7710) * Initial Commit * Update docs * Add support for elseif blocks * Another test * WIP * Change from `{%if%}` to `<%if%>` See discussion here - https://talk.tiddlywiki.org/t/proposed-if-widget/7882/64 * Don't use the widget body as the template if a list-empty widget is present See discussion here - https://github.com/Jermolene/TiddlyWiki5/pull/7710#issuecomment-1717193296 * List widget should search recursively for list-template and list-empty * Allow block mode content within an if/then/else clause * Update docs * Add from-version tag to docs --- .../parsers/wikiparser/rules/conditional.js | 120 ++++++++++++++++++ core/modules/parsers/wikiparser/wikiparser.js | 37 ++++-- .../tests/data/conditionals/Basic.tid | 26 ++++ .../tests/data/conditionals/BlockMode.tid | 37 ++++++ .../tiddlers/tests/data/conditionals/Else.tid | 26 ++++ .../tests/data/conditionals/Elseif.tid | 32 +++++ .../tests/data/conditionals/MissingEndIf.tid | 26 ++++ .../data/conditionals/MultipleResults.tid | 12 ++ .../tests/data/conditionals/Nested.tid | 38 ++++++ .../tests/data/conditionals/NestedElseif.tid | 60 +++++++++ .../wikitext/Conditional Shortcut Syntax.tid | 61 +++++++++ 11 files changed, 467 insertions(+), 8 deletions(-) create mode 100644 core/modules/parsers/wikiparser/rules/conditional.js create mode 100644 editions/test/tiddlers/tests/data/conditionals/Basic.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/BlockMode.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/Else.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/Elseif.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/MissingEndIf.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/MultipleResults.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/Nested.tid create mode 100644 editions/test/tiddlers/tests/data/conditionals/NestedElseif.tid create mode 100644 editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid diff --git a/core/modules/parsers/wikiparser/rules/conditional.js b/core/modules/parsers/wikiparser/rules/conditional.js new file mode 100644 index 000000000..23940fd88 --- /dev/null +++ b/core/modules/parsers/wikiparser/rules/conditional.js @@ -0,0 +1,120 @@ +/*\ +title: $:/core/modules/parsers/wikiparser/rules/conditional.js +type: application/javascript +module-type: wikirule + +Conditional shortcut syntax + +``` +This is a <% if [{something}] %>Elephant<% elseif [{else}] %>Pelican<% else %>Crocodile<% endif %> +``` + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.name = "conditional"; +exports.types = {inline: true, block: true}; + +exports.init = function(parser) { + this.parser = parser; + // Regexp to match + this.matchRegExp = /\<\%\s*if\s+/mg; + this.terminateIfRegExp = /\%\>/mg; +}; + +exports.findNextMatch = function(startPos) { + // Look for the next <% if shortcut + this.matchRegExp.lastIndex = startPos; + this.match = this.matchRegExp.exec(this.parser.source); + // If not found then return no match + if(!this.match) { + return undefined; + } + // Check for the next %> + this.terminateIfRegExp.lastIndex = this.match.index; + this.terminateIfMatch = this.terminateIfRegExp.exec(this.parser.source); + // If not found then return no match + if(!this.terminateIfMatch) { + return undefined; + } + // Return the position at which the construction was found + return this.match.index; +}; + +/* +Parse the most recent match +*/ +exports.parse = function() { + // Get the filter condition + var filterCondition = this.parser.source.substring(this.match.index + this.match[0].length,this.terminateIfMatch.index); + // Advance the parser position to past the %> + this.parser.pos = this.terminateIfMatch.index + this.terminateIfMatch[0].length; + // Parse the if clause + return this.parseIfClause(filterCondition); +}; + +exports.parseIfClause = function(filterCondition) { + // Create the list widget + var listWidget = { + type: "list", + tag: "$list", + isBlock: this.is.block, + children: [ + { + type: "list-template", + tag: "$list-template" + }, + { + type: "list-empty", + tag: "$list-empty" + } + ] + }; + $tw.utils.addAttributeToParseTreeNode(listWidget,"filter",filterCondition); + $tw.utils.addAttributeToParseTreeNode(listWidget,"variable","condition"); + $tw.utils.addAttributeToParseTreeNode(listWidget,"limit","1"); + // Check for an immediately following double linebreak + var hasLineBreak = !!$tw.utils.parseTokenRegExp(this.parser.source,this.parser.pos,/([^\S\n\r]*\r?\n(?:[^\S\n\r]*\r?\n|$))/g); + // Parse the body looking for else or endif + var reEndString = "\\<\\%\\s*(endif)\\s*\\%\\>|\\<\\%\\s*(else)\\s*\\%\\>|\\<\\%\\s*(elseif)\\s+([\\s\\S]+?)\\%\\>", + ex; + if(hasLineBreak) { + ex = this.parser.parseBlocksTerminatedExtended(reEndString); + } else { + var reEnd = new RegExp(reEndString,"mg"); + ex = this.parser.parseInlineRunTerminatedExtended(reEnd,{eatTerminator: true}); + } + // Put the body into the list template + listWidget.children[0].children = ex.tree; + // Check for an else or elseif + if(ex.match) { + if(ex.match[1] === "endif") { + // Nothing to do if we just found an endif + } else if(ex.match[2] === "else") { + // Check for an immediately following double linebreak + hasLineBreak = !!$tw.utils.parseTokenRegExp(this.parser.source,this.parser.pos,/([^\S\n\r]*\r?\n(?:[^\S\n\r]*\r?\n|$))/g); + // If we found an else then we need to parse the body looking for the endif + var reEndString = "\\<\\%\\s*(endif)\\s*\\%\\>", + ex; + if(hasLineBreak) { + ex = this.parser.parseBlocksTerminatedExtended(reEndString); + } else { + var reEnd = new RegExp(reEndString,"mg"); + ex = this.parser.parseInlineRunTerminatedExtended(reEnd,{eatTerminator: true}); + } + // Put the parsed content inside the list empty template + listWidget.children[1].children = ex.tree; + } else if(ex.match[3] === "elseif") { + // Parse the elseif clause by reusing this parser, passing the new filter condition + listWidget.children[1].children = this.parseIfClause(ex.match[4]); + } + } + // Return the parse tree node + return [listWidget]; +}; + +})(); diff --git a/core/modules/parsers/wikiparser/wikiparser.js b/core/modules/parsers/wikiparser/wikiparser.js index bb457b205..293b7d3d3 100644 --- a/core/modules/parsers/wikiparser/wikiparser.js +++ b/core/modules/parsers/wikiparser/wikiparser.js @@ -223,7 +223,7 @@ Parse a block from the current position terminatorRegExpString: optional regular expression string that identifies the end of plain paragraphs. Must not include capturing parenthesis */ WikiParser.prototype.parseBlock = function(terminatorRegExpString) { - var terminatorRegExp = terminatorRegExpString ? new RegExp("(" + terminatorRegExpString + "|\\r?\\n\\r?\\n)","mg") : /(\r?\n\r?\n)/mg; + var terminatorRegExp = terminatorRegExpString ? new RegExp(terminatorRegExpString + "|\\r?\\n\\r?\\n","mg") : /(\r?\n\r?\n)/mg; this.skipWhitespace(); if(this.pos >= this.sourceLength) { return []; @@ -264,11 +264,21 @@ WikiParser.prototype.parseBlocksUnterminated = function() { }; /* -Parse blocks of text until a terminating regexp is encountered +Parse blocks of text until a terminating regexp is encountered. Wrapper for parseBlocksTerminatedExtended that just returns the parse tree */ WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) { - var terminatorRegExp = new RegExp("(" + terminatorRegExpString + ")","mg"), - tree = []; + var ex = this.parseBlocksTerminatedExtended(terminatorRegExpString); + return ex.tree; +}; + +/* +Parse blocks of text until a terminating regexp is encountered +*/ +WikiParser.prototype.parseBlocksTerminatedExtended = function(terminatorRegExpString) { + var terminatorRegExp = new RegExp(terminatorRegExpString,"mg"), + result = { + tree: [] + }; // Skip any whitespace this.skipWhitespace(); // Check if we've got the end marker @@ -277,7 +287,7 @@ WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) { // Parse the text into blocks while(this.pos < this.sourceLength && !(match && match.index === this.pos)) { var blocks = this.parseBlock(terminatorRegExpString); - tree.push.apply(tree,blocks); + result.tree.push.apply(result.tree,blocks); // Skip any whitespace this.skipWhitespace(); // Check if we've got the end marker @@ -286,8 +296,9 @@ WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) { } if(match && match.index === this.pos) { this.pos = match.index + match[0].length; + result.match = match; } - return tree; + return result; }; /* @@ -330,6 +341,11 @@ WikiParser.prototype.parseInlineRunUnterminated = function(options) { }; WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,options) { + var ex = this.parseInlineRunTerminatedExtended(terminatorRegExp,options); + return ex.tree; +}; + +WikiParser.prototype.parseInlineRunTerminatedExtended = function(terminatorRegExp,options) { options = options || {}; var tree = []; // Find the next occurrence of the terminator @@ -349,7 +365,10 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option if(options.eatTerminator) { this.pos += terminatorMatch[0].length; } - return tree; + return { + match: terminatorMatch, + tree: tree + }; } } // Process any inline rule, along with the text preceding it @@ -373,7 +392,9 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option this.pushTextWidget(tree,this.source.substr(this.pos),this.pos,this.sourceLength); } this.pos = this.sourceLength; - return tree; + return { + tree: tree + }; }; /* diff --git a/editions/test/tiddlers/tests/data/conditionals/Basic.tid b/editions/test/tiddlers/tests/data/conditionals/Basic.tid new file mode 100644 index 000000000..ff2d2df4d --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/Basic.tid @@ -0,0 +1,26 @@ +title: Conditionals/Basic +description: Basic conditional shortcut syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Text + +This is a <% if [match[one]] %>Elephant<% endif %>, I think. ++ +title: Output + +<$let something="one"> +{{Text}} + + +<$let something="two"> +{{Text}} + ++ +title: ExpectedResult + +

+This is a Elephant, I think. +

+This is a , I think. +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/conditionals/BlockMode.tid b/editions/test/tiddlers/tests/data/conditionals/BlockMode.tid new file mode 100644 index 000000000..45233baa4 --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/BlockMode.tid @@ -0,0 +1,37 @@ +title: Conditionals/BlockMode +description: Basic conditional shortcut syntax in block mode +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\procedure test(animal) +<% if [match[Elephant]] %> + +! It is an elephant + +<% else %> + +<% if [match[Giraffe]] %> + +! It is a giraffe + +<% else %> + +! It is completely unknown + +<% endif %> + +<% endif %> + +\end + +<> + +<> + +<> ++ +title: ExpectedResult + +

It is a giraffe

It is an elephant

It is completely unknown

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/conditionals/Else.tid b/editions/test/tiddlers/tests/data/conditionals/Else.tid new file mode 100644 index 000000000..7bc32b34e --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/Else.tid @@ -0,0 +1,26 @@ +title: Conditionals/Else +description: Else conditional shortcut syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Text + +This is a <% if [match[one]] %>Elephant<% else %>Crocodile<% endif %>, I think. ++ +title: Output + +<$let something="one"> +{{Text}} + + +<$let something="two"> +{{Text}} + ++ +title: ExpectedResult + +

+This is a Elephant, I think. +

+This is a Crocodile, I think. +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/conditionals/Elseif.tid b/editions/test/tiddlers/tests/data/conditionals/Elseif.tid new file mode 100644 index 000000000..d37f3380c --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/Elseif.tid @@ -0,0 +1,32 @@ +title: Conditionals/Elseif +description: Elseif conditional shortcut syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Text + +This is a <% if [match[one]] %>Elephant<% elseif [match[two]] %>Antelope<% else %>Crocodile<% endif %>, I think. ++ +title: Output + +<$let something="one"> +{{Text}} + + +<$let something="two"> +{{Text}} + + +<$let something="three"> +{{Text}} + ++ +title: ExpectedResult + +

+This is a Elephant, I think. +

+This is a Antelope, I think. +

+This is a Crocodile, I think. +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/conditionals/MissingEndIf.tid b/editions/test/tiddlers/tests/data/conditionals/MissingEndIf.tid new file mode 100644 index 000000000..cacaf9869 --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/MissingEndIf.tid @@ -0,0 +1,26 @@ +title: Conditionals/MissingEndif +description: Conditional shortcut syntax with missing endif +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Text + +This is a <% if [match[one]] %>Elephant ++ +title: Output + +<$let something="one"> +{{Text}} + + +<$let something="two"> +{{Text}} + ++ +title: ExpectedResult + +

+This is a Elephant +

+This is a +

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/conditionals/MultipleResults.tid b/editions/test/tiddlers/tests/data/conditionals/MultipleResults.tid new file mode 100644 index 000000000..baa966ed5 --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/MultipleResults.tid @@ -0,0 +1,12 @@ +title: Conditionals/MultipleResults +description: Check that multiple results from the filter are ignored +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +This is a <% if 1 2 3 4 5 6 %>Elephant<% endif %>, I think. ++ +title: ExpectedResult + +

This is a Elephant, I think.

\ No newline at end of file diff --git a/editions/test/tiddlers/tests/data/conditionals/Nested.tid b/editions/test/tiddlers/tests/data/conditionals/Nested.tid new file mode 100644 index 000000000..dffa791fc --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/Nested.tid @@ -0,0 +1,38 @@ +title: Conditionals/Nested +description: Nested conditional shortcut syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Output + +\procedure test(animal) +<% if [match[Elephant]] %> +It is an elephant +<% else %> +<% if [match[Giraffe]] %> +It is a giraffe +<% else %> +It is completely unknown +<% endif %> +<% endif %> +\end + +<> + +<> + +<> + ++ +title: ExpectedResult + + + +It is a giraffe + + +It is an elephant + + +It is completely unknown + diff --git a/editions/test/tiddlers/tests/data/conditionals/NestedElseif.tid b/editions/test/tiddlers/tests/data/conditionals/NestedElseif.tid new file mode 100644 index 000000000..6fba8cac8 --- /dev/null +++ b/editions/test/tiddlers/tests/data/conditionals/NestedElseif.tid @@ -0,0 +1,60 @@ +title: Conditionals/NestedElseif +description: Nested elseif conditional shortcut syntax +type: text/vnd.tiddlywiki-multiple +tags: [[$:/tags/wiki-test-spec]] + +title: Text + +\whitespace trim +This is a + <% if [match[one]] %> + <% if [match[one]] %> + Indian + <% elseif [match[two]] %> + African + <% else %> + Unknown + <% endif %> + Elephant + <% elseif [match[two]] %> + Antelope + <% else %> + Crocodile + <% endif %> +, I think. ++ +title: Output + +<$let something="one" another="one"> +{{Text}} + + +<$let something="one" another="two"> +{{Text}} + + +<$let something="one" another="three"> +{{Text}} + + +<$let something="two"> +{{Text}} + + +<$let something="three"> +{{Text}} + ++ +title: ExpectedResult + +

+This is a Indian Elephant, I think. +

+This is a African Elephant, I think. +

+This is a Unknown Elephant, I think. +

+This is a Antelope, I think. +

+This is a Crocodile, I think. +

\ No newline at end of file diff --git a/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid b/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid new file mode 100644 index 000000000..6cdfb1517 --- /dev/null +++ b/editions/tw5.com/tiddlers/wikitext/Conditional Shortcut Syntax.tid @@ -0,0 +1,61 @@ +created: 20230901122740573 +modified: 20230901123102263 +tags: WikiText +title: Conditional Shortcut Syntax +type: text/vnd.tiddlywiki + +<<.from-version "5.3.2">> The conditional shortcut syntax provides a convenient way to express if-then-else logic within WikiText. It evaluates a filter and considers the condition to be true if there is at least one result (regardless of the value of that result). + +A simple example: + +<$macrocall $name='wikitext-example-without-html' +src='<% if [{$:/$:/info/url/protocol}match[file:]] %> + Loaded from a file URI +<% else %> + Not loaded from a file URI +<% endif %> +'/> + +One or more `<% elseif %>` clauses may be included before the `<% else %>` clause: + +<$macrocall $name='wikitext-example-without-html' +src='<% if [{$:/$:/info/url/protocol}match[file:]] %> + Loaded from a file URI +<% elseif [{$:/$:/info/url/protocol}match[https:]] %> + Loaded from an HTTPS URI +<% elseif [{$:/$:/info/url/protocol}match[http:]] %> + Loaded from an HTTP URI +<% else %> + Loaded from an unknown protocol +<% endif %> +'/> + +The conditional shortcut syntax can be nested: + +<$macrocall $name='wikitext-example-without-html' +src='\procedure test(animal) +<% if [match[Elephant]] %> + It is an elephant +<% else %> + <% if [match[Giraffe]] %> + It is a giraffe + <% else %> + It is completely unknown + <% endif %> +<% endif %> +\end + +<> + +<> + +<> +'/> + +Notes: + +* Clauses are parsed in inline mode by default. Force block mode parsing by following the opening `<% if %>`, `<% elseif %>` or `<% else %>` with two line breaks +* Within an "if" or "elseif" clause, the variable `condition` contains the value of the first result of evaluating the filter condition +* Widgets and HTML elements must be within a single conditional clause; it is not possible to start an element in one conditional clause and end it in another +* The conditional shortcut syntax cannot contain pragmas such as procedure definitions +