1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-29 12:59:56 +00:00

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
This commit is contained in:
Jeremy Ruston 2023-10-14 09:41:21 +01:00 committed by GitHub
parent 4c9c85aec5
commit b7562f0c7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 467 additions and 8 deletions

View File

@ -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];
};
})();

View File

@ -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 terminatorRegExpString: optional regular expression string that identifies the end of plain paragraphs. Must not include capturing parenthesis
*/ */
WikiParser.prototype.parseBlock = function(terminatorRegExpString) { 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(); this.skipWhitespace();
if(this.pos >= this.sourceLength) { if(this.pos >= this.sourceLength) {
return []; 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) { WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) {
var terminatorRegExp = new RegExp("(" + terminatorRegExpString + ")","mg"), var ex = this.parseBlocksTerminatedExtended(terminatorRegExpString);
tree = []; 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 // Skip any whitespace
this.skipWhitespace(); this.skipWhitespace();
// Check if we've got the end marker // Check if we've got the end marker
@ -277,7 +287,7 @@ WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) {
// Parse the text into blocks // Parse the text into blocks
while(this.pos < this.sourceLength && !(match && match.index === this.pos)) { while(this.pos < this.sourceLength && !(match && match.index === this.pos)) {
var blocks = this.parseBlock(terminatorRegExpString); var blocks = this.parseBlock(terminatorRegExpString);
tree.push.apply(tree,blocks); result.tree.push.apply(result.tree,blocks);
// Skip any whitespace // Skip any whitespace
this.skipWhitespace(); this.skipWhitespace();
// Check if we've got the end marker // Check if we've got the end marker
@ -286,8 +296,9 @@ WikiParser.prototype.parseBlocksTerminated = function(terminatorRegExpString) {
} }
if(match && match.index === this.pos) { if(match && match.index === this.pos) {
this.pos = match.index + match[0].length; 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) { WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,options) {
var ex = this.parseInlineRunTerminatedExtended(terminatorRegExp,options);
return ex.tree;
};
WikiParser.prototype.parseInlineRunTerminatedExtended = function(terminatorRegExp,options) {
options = options || {}; options = options || {};
var tree = []; var tree = [];
// Find the next occurrence of the terminator // Find the next occurrence of the terminator
@ -349,7 +365,10 @@ WikiParser.prototype.parseInlineRunTerminated = function(terminatorRegExp,option
if(options.eatTerminator) { if(options.eatTerminator) {
this.pos += terminatorMatch[0].length; this.pos += terminatorMatch[0].length;
} }
return tree; return {
match: terminatorMatch,
tree: tree
};
} }
} }
// Process any inline rule, along with the text preceding it // 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.pushTextWidget(tree,this.source.substr(this.pos),this.pos,this.sourceLength);
} }
this.pos = this.sourceLength; this.pos = this.sourceLength;
return tree; return {
tree: tree
};
}; };
/* /*

View File

@ -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 [<something>match[one]] %>Elephant<% endif %>, I think.
+
title: Output
<$let something="one">
{{Text}}
</$let>
<$let something="two">
{{Text}}
</$let>
+
title: ExpectedResult
<p>
This is a Elephant, I think.
</p><p>
This is a , I think.
</p>

View File

@ -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 [<animal>match[Elephant]] %>
! It is an elephant
<% else %>
<% if [<animal>match[Giraffe]] %>
! It is a giraffe
<% else %>
! It is completely unknown
<% endif %>
<% endif %>
\end
<<test "Giraffe">>
<<test "Elephant">>
<<test "Antelope">>
+
title: ExpectedResult
<h1 class="">It is a giraffe</h1><h1 class="">It is an elephant</h1><h1 class="">It is completely unknown</h1>

View File

@ -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 [<something>match[one]] %>Elephant<% else %>Crocodile<% endif %>, I think.
+
title: Output
<$let something="one">
{{Text}}
</$let>
<$let something="two">
{{Text}}
</$let>
+
title: ExpectedResult
<p>
This is a Elephant, I think.
</p><p>
This is a Crocodile, I think.
</p>

View File

@ -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 [<something>match[one]] %>Elephant<% elseif [<something>match[two]] %>Antelope<% else %>Crocodile<% endif %>, I think.
+
title: Output
<$let something="one">
{{Text}}
</$let>
<$let something="two">
{{Text}}
</$let>
<$let something="three">
{{Text}}
</$let>
+
title: ExpectedResult
<p>
This is a Elephant, I think.
</p><p>
This is a Antelope, I think.
</p><p>
This is a Crocodile, I think.
</p>

View File

@ -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 [<something>match[one]] %>Elephant
+
title: Output
<$let something="one">
{{Text}}
</$let>
<$let something="two">
{{Text}}
</$let>
+
title: ExpectedResult
<p>
This is a Elephant
</p><p>
This is a
</p>

View File

@ -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
<p>This is a Elephant, I think.</p>

View File

@ -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 [<animal>match[Elephant]] %>
It is an elephant
<% else %>
<% if [<animal>match[Giraffe]] %>
It is a giraffe
<% else %>
It is completely unknown
<% endif %>
<% endif %>
\end
<<test "Giraffe">>
<<test "Elephant">>
<<test "Antelope">>
+
title: ExpectedResult
It is a giraffe
It is an elephant
It is completely unknown

View File

@ -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&#32;
<% if [<something>match[one]] %>
<% if [<another>match[one]] %>
Indian
<% elseif [<another>match[two]] %>
African
<% else %>
Unknown
<% endif %>
&#32;Elephant
<% elseif [<something>match[two]] %>
Antelope
<% else %>
Crocodile
<% endif %>
, I think.
+
title: Output
<$let something="one" another="one">
{{Text}}
</$let>
<$let something="one" another="two">
{{Text}}
</$let>
<$let something="one" another="three">
{{Text}}
</$let>
<$let something="two">
{{Text}}
</$let>
<$let something="three">
{{Text}}
</$let>
+
title: ExpectedResult
<p>
This is a Indian Elephant, I think.
</p><p>
This is a African Elephant, I think.
</p><p>
This is a Unknown Elephant, I think.
</p><p>
This is a Antelope, I think.
</p><p>
This is a Crocodile, I think.
</p>

View File

@ -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 [<animal>match[Elephant]] %>
It is an elephant
<% else %>
<% if [<animal>match[Giraffe]] %>
It is a giraffe
<% else %>
It is completely unknown
<% endif %>
<% endif %>
\end
<<test "Giraffe">>
<<test "Elephant">>
<<test "Antelope">>
'/>
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