I’ve started playing around with a highlighter for filter syntax. It looks like this:
There are many different hover behaviors, which I won’t try to capture here. You can just take a look at it at http://scott.sauyet.com/Tiddlywiki/WIP/FilterFormatDemo.html. If you want to play with styles, the stylesheet can be found in the sidebar. So is the JS macro I used. You can try them in your own wiki using: FilterFormatDemo.json (50.8 KB)
This is all throw-away, proof-of-concept code. I wouldn’t expect to keep a line of it for a production implementation. I’ll discuss it more below, but the main question is, would something like this be useful for the docs site, to be used wherever we demonstrate filters?
Implementation Details
I built this atop a Parser Expression Grammar (PEG) that I wrote custom for this, using the dingus from Peggy.js. This allows us to write declarative descriptions of expressions in our grammar alongside JS code to generate a result, in this case an Abstract Syntax Tree (AST). Tiddlywiki already has a function to do this, $tw.util.parseExpression
, but the tree it generated seemed to me to lack details I wanted to have. I would definitely revisit this choice. But even better would be to incorporate the Highlight Plugin, and add a mode for our filters. I don’t know precisely how to do this, but if we go this route, that would seem most likely.
The PEG grammar I used looks like this:
Filter
= _ head:Run tail:(_ Run)* _
{return [head, ...tail.map(e => e[1])]}
Run
= PrefixRun
/ StaticRun
PrefixRun
= prefix:Prefix run:FilterRun
{return {prefix, run}}
/ run: FilterRun
{return {prefix: '', run}}
StaticRun
= "\"" chars: [^"]* "\""
{ return {prefix: '', run: {steps: [{text: chars.join('').trim(), quotes: '"'}]}}; }
/ "'" chars: [^']* "'"
{ return {prefix: '', run: {steps: [{text: chars.join('').trim(), quotes:"'"}]}}; }
/ chars: [^'"\[]+
{ return {prefix: '', run: {steps: [{text: chars.join('').trim(), quotes: ''}]}}; }
Prefix
= NamedPrefix
/ ShortcutPrefix
NamedPrefix
= ":all" / ":and" / ":cascade" / ":else" / ":except" / ":filter"
/ ":intersection" / ":map" / ":or" / ":reduce" / ":sort" / ":then"
ShortcutPrefix
= "+" / "-" / "~" / "="
FilterRun
= "[" steps:FilterSteps "]"
{return {steps}}
FilterSteps
= head:FilterStep tail:(_ FilterStep)*
{return [head, ...tail.map(e => e[1])]}
FilterStep
= negated: ("!")? op:Operator suffixes:Suffixes? params:Params?
{return {negated: !!negated, op, suffixes: suffixes || [], params: params || []}}
/ negated: ("!")? params:Params
{return {negated: !!negated, op: '', params: params || []}}
Suffixes
= head:Suffix tail:(Suffix)*
{return [head, ...tail]}
Suffix
= ":" name:[^:\[\{\< ]+
{return {parts: name.join('').split(',')}}
/ ":"
{return {parts: [""]}}
Params
= head:Param tail:("," Param)*
{return [head, ...tail.map(e => e[1])]}
Param
= "[" hard:[^\]]+ "]"
{return {type: 'hard', text: hard.join('')}}
/ "{" textRef:[^!}]+ "}"
{return {type: 'textRef', tiddler: textRef.join('')}}
/ "{" textRef:[^!}]+ "!!" field:[^}]+ "}"
{return {type: 'textRef', tiddler: textRef.join(''), field: field.join('')}}
/ "{" "!!" field:[^}]+ "}"
{return {type: 'textRef', field: field.join('')}}
/ "<" varRef:[^<>]+ ">"
{return {type: 'varRef', text: varRef.join('')}}
Operator
= op: [^\[\]\:\<\{ ]+
{return op.join('')}
_ "whitespace"
= [ \t\n\r]*
This gets turned into JS parsing code, which is included in that macro.
For the filter [range[5],<max-val>,{!!step}] -[[20]] +[[9999]]
, this generates the tree:
[
{
prefix: '',
run: {
steps: [
{
negated: false,
op: 'range',
suffixes: [],
params: [
{
type: 'hard',
text: '5'
},
{
type: 'varRef',
text: 'max-val'
},
{
type: 'textRef',
field: 'step'
}
]
}
]
}
},
{
prefix: '-',
run: {
steps: [
{
negated: false,
op: '',
params: [
{
type: 'hard',
text: '20'
}
]
}
]
}
},
{
prefix: '+',
run: {
steps: [
{
negated: false,
op: '',
params: [
{
type: 'hard',
text: '9999'
}
]
}
]
}
}
]
That is then passed to some custom code, which converts that to the following HTML (without all the indentation and other white space):
<tt class="filter">
<span class="complex run">
<span class="punc sq-bracket">[</span>
<span class="step">
<span class="operator">range</span>
<span class="param">
<span class="punc sq-bracket">[</span>
5
<span class="punc sq-bracket">]</span>
</span>
<span class="punc comma">,</span>
<span class="param">
<span class="punc ang-bracket"><</span>
max-val
<span class="punc ang-bracket">></span>
</span>
<span class="punc comma">,</span>
<span class="param">
<span class="punc curly-bracket">{</span>
<span class="punc bangs">!!</span>
<span class="field">step</span>
<span class="punc curly-bracket">}</span>
</span>
</span>
<span class="punc sq-bracket">]</span>
</span>
<span class="prefix">-</span>
<span class="complex run">
<span class="punc sq-bracket">[</span>
<span class="step">
<span class="operator"></span>
<span class="param">
<span class="punc sq-bracket">[</span>
20
<span class="punc sq-bracket">]</span>
</span>
</span>
<span class="punc sq-bracket">]</span>
</span>
<span class="plain run"><span class="step">+</span>
</span>
<span class="complex run">
<span class="punc sq-bracket">[</span>
<span class="step">
<span class="operator"></span>
<span class="param">
<span class="punc sq-bracket">[</span>
9999
<span class="punc sq-bracket">]</span>
</span>
</span>
<span class="punc sq-bracket">]</span>
</span>
</tt>
Creating a module from this is just a matter of properly combining the parser generated by PEG alongside the AST->HTML code, and wrapping it in at HTML module,
Again, note that this is not production-ready, not even close. It’s a quickly hacked-together tool meant only to show the idea. But please play with it.