Skip to content

Commit

Permalink
Conditional Shortcut Syntax (#7710)
Browse files Browse the repository at this point in the history
* 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 - #7710 (comment)

* 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
  • Loading branch information
Jermolene committed Oct 14, 2023
1 parent 4c9c85a commit b7562f0
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 8 deletions.
120 changes: 120 additions & 0 deletions 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];
};

})();
37 changes: 29 additions & 8 deletions core/modules/parsers/wikiparser/wikiparser.js
Expand Up @@ -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 [];
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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;
};

/*
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
};
};

/*
Expand Down
26 changes: 26 additions & 0 deletions 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 [<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>
37 changes: 37 additions & 0 deletions 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 [<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>
26 changes: 26 additions & 0 deletions 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 [<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>
32 changes: 32 additions & 0 deletions 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 [<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>
26 changes: 26 additions & 0 deletions 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 [<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>
12 changes: 12 additions & 0 deletions 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

<p>This is a Elephant, I think.</p>

1 comment on commit b7562f0

@vercel
Copy link

@vercel vercel bot commented on b7562f0 Oct 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

tiddlywiki5 – ./

tiddlywiki5-git-master-jermolene.vercel.app
tiddlywiki5.vercel.app
tiddlywiki5-jermolene.vercel.app

Please sign in to comment.