TangibleInc / template-system

A template system for WordPress with content type loops and conditions
https://docs.loopsandlogic.com/reference/template-system/
6 stars 2 forks source link

Logic tag to build and evaluate conditional rules #115

Closed eliot-akira closed 3 months ago

eliot-akira commented 3 months ago

From Tangible Blocks: Visibility conditions - Syntax change:

same data structure for conditional rules on the frontend and server side, with compatible evaluators written in JavaScript and PHP

Logic tag, where users can build rules using RuleGroup and Rule tags. These rules can be passed to the If and Loop tag for server-side evaluation; or, they can be passed to the Control tag to define visibility conditions.

Build logic

<Logic name=weekend_webinar>
  <Rule taxonomy=event_type term=webinar />
  <Or>
    <Rule field=event_date value=Saturday />
    <Rule field=event_date value=Sunday />
  </Or>
</Logic>

Use with Loop

<Loop post_type=event logic=weekend_webinar>
  Weekend seminar: <Field title />
</Loop>

Use with If

<Loop post_type=event>
  <If logic=weekend_webinar>
    Weekend seminar: <Field title />
  <Else />
    Weekday seminar: <Field title />
  </If>
</Loop>

Use with Control

<Logic name=example>
  <Rule control=text_1 value="some value" />
</Logic>

<Control type="text_2" name="text_2" label="Text value" logic=example />
eliot-akira commented 3 months ago

This is turning out to be an interesting feature. I found a library called JSON Logic, a cross-language specification for building complex rules that can be serialized as JSON and evaluated in the browser or server side.

It was the perfect starting point - I rewrote their implementation in TypeScript and PHP to fully customize for our needs. To confirm they're correct and compatible with each other, there's a suite of test cases written in JSON (tests.json).

Logic rules

The main concept is a "logic rule", which is always in this shape.

type Rule = {
  [key: Operator]: Value | Value[]
}

type Operator = 'and' | 'or' | 'not' | ...
type Value = Rule | any

It's a plain object (associative array) with a single key, which is an operator. It has one or more values, which themselves can be a logic rule. Here's the list of supported operators, like and, or, not, == (equal), != (not equal), in.

Example of logic rules

{
  '==': [1, 2]
}

..is equivalent to the condition, 1 == 2.

{
  and: [
    { '>': [1, 2] },
    { '<': [3, 4] }
  ]
}

..is (1 > 2) && (3 < 4).

Logic tag

The new Logic tag is a way to build conditional rules using Rule, And, Or, and Not tags.

<Logic name=example action=hide>
  <Rule control=text_1 value="some value" />
</Logic>

The above creates an object like:

{
  name: 'example',
  action: 'hide',
  logic: {
    and: [
      { rule: { control: 'text_1', value: 'some value' } }
    ]
  }
}

Logic tag attributes

Example of complex rules

<Logic name=weekend_webinar>
  <Rule taxonomy=event_type term=webinar />
  <Or>
    <Rule field=event_date value=Saturday />
    <Rule field=event_date value=Sunday />
  </Or>
</Logic>

That creates JSON logic like:

{
  name: 'weekend_webinar',
  logic: {
    and: [
      { rule: { taxonomy: 'event_type', term: 'webinar' } },
      {
        or: [
          { rule: { field: 'event_date', value: 'Saturday' } },
          { rule: { field: 'event_date', value: 'Sunday' } }
        ]
      }
    ]
  }
}

Rule tag

The Rule tag is generic. It accepts any attributes, and simply passes them to the rule it creates.

Internally, it uses a special operator rule, which calls a given dynamic rule evaluator. For example, it can be evaluated using If tag on the server side. Or a custom evaluator on the frontend to determine visibility conditions based on block control values.

And, Or, Not tags

These tags combine rules in different ways.

All rules must be true

<And>
  <Rule ... />
  <Rule ... />
</And>

At least one rule must be true

<Or>
  <Rule ... />
  <Rule ... />
</Or>

Rule must be false

<Not>
  <Rule ... />
</Not>

At least one rule must be false

<Not>
  <Rule ... />
  <Rule ... />
</Not>

All rules must be false

<Not>
  <Or>
    <Rule ... />
    <Rule ... />
  </Or>
</Not>

Pass logic to other tags

The If and Loop tags now support a logic attribute, which applies the logic defined with that name, using the If tag to dynamically evaluate rules.

<If logic=weekend_webinar>
  Weekend seminar: <Field title />
<Else />
  Weekday seminar: <Field title />
</If>

It's a better version of logic variables. For backward compatibility, the If tag falls back to any logic variable defined with <Set logic>.

Using <Loop logic> will filter items that match the given logic.

<Loop post_type=event logic=weekend_webinar>
  Weekend seminar: <Field title />
</Loop>

It's a shortcut that's equivalent to:

<Loop post_type=event>
  <If logic=weekend_webinar>
    Weekend seminar: <Field title />
  </If>
</Loop>

This is less efficient than using query parameters, but useful for post-processing the query result.

Functions

PHP

Here's where the Logic tag is defined, language/logic/tag.php. It includes utility functions like:

use tangible\template_system;

$logic = template_system\get_logic_by_name( $name );

if ($logic !== false) {
  $result = template_system\evaluate_logic( $logic, $evaluator );
}

TypeScript

The core logic functions are currently in logic/index.ts.

During development, we can maybe alias @tangible/logic to ./vendor/tangible/template-system/logic. I plan to publish it as an NPM package when its interface is stable.

import { evaluate } from '@tangible/logic'

const result = evaluate( logic, evaluator )

The rule evaluator takes a rule and returns boolean (true or false). For example:

const logic = {
  rule: { control: 'example', value: '123' }
}

const data = {
  example: '456'
}

function ruleEvaluator(rule) {
  const { control, value } = rule
  return data[ control ] === value
}

const result = evaluate( logic, ruleEvaluator )

@nicolas-jaussaud Hopefully that's a good foundation and enough info to get you started on converting the visibility conditions syntax for block controls (TangibleInc/blocks#1).

@BenTangible @juliacanzani It would be lovely if you could kick the tires and take it for a test drive. The above description can probably serve as a rough draft for a documentation page.

eliot-akira commented 3 months ago

The function evaluate can also be used to compare block control values with different operators.

function ruleEvaluator(rule) {
  const {
    control,
    value,
    compare = '==' // Default: equal
  } = rule
  const actual = data[ control ]
  return evaluate({
    [compare]: [ actual, value ]
  })
}

This might be useful for converting the current rule evaluator in the Fields module, assets/src/visibility/evaluate.js.

For now, the compare operators are in the original JSON Logic syntax, like ==, !=, >, <=. I plan to add human-readable aliases, like equal, not_equal, greater_than, and less_than_or_equal.

nicolas-jaussaud commented 3 months ago

Hi @eliot-akira, thank you for all the examples!

I would have some questions to be sure I understood everything correctly before starting making the changes in fields and blocks

This might be useful for converting the current rule evaluator in the Fields module

Does that mean we are harmonizing the syntax used in fields to be inline with what we do in L&L/blocks, and switching from this syntax:

$fields->render_fields('field-name', [
  // ...
  'condition' => [
    'action'    => 'show',
    'condition' => [
      '_and' => [
        'another-field' => [ '_eq'  => 'something' ],
        'and-another'   => [ '_neq' => 'something else' ],
      ]
    ],
  ],
]);

To this:

$fields->render_fields('field-name', [
  // ...
  'condition' => [
    'action'    => 'show',
    'logic' => [
      'and' => [
        'another-field' => [ 'equal'     => 'something' ],
        'and-another'   => [ 'not_equal' => 'something else' ],
      ],
    ],
  ],
]);

Both are very similar so if that's the case it shouldn't be too hard to keep backward compatibility

I plan to publish it as an NPM package when its interface is stable.

We also have a rule evaluation on the PHP side in fields (here) that uses the same syntax

I imagine we also want to update that part to template_system\evaluate_logic() to stay consistent, but I'm not sure we want to add template_system as a composer dependency of fields

Is the logic folder also going to be a its own composer module/github repository like for the framework? (Or maybe part of the framework?)

I'm a little worried that it will still be confusing for users to know which kind or rules can be used together. For example I will probably assume that something like this will work:

<Logic name="logic_name" action=hide>
  <Rule control=text_2 value="something" />
  <!-- I'm not sure it's a real rule but let's pretend it's a rule that exists in L&L -->
  <Rule user=current role="admin" />
</Logic>

<Control type="text" name="text_1" label="Text 1" logic="logic_name" />
<Control type="text" name="text_2" label="Text 2" />

Maybe we should try to evaluate both back-end and front-end conditions in blocks

I'm not sure what would be the best way to do it, but I'm thinking something like this could work (sorry the code is not correct but I hope it's clear enough to understand):

/**
 * PHP array for:
 * <Control type="text" name="text_1" label="Text 1" logic="logic_name" />
 */
$control;

if( ! empty($control['logic']) && is_string($control['logic']) ) {

  /**
   * Returned value according to <Logic name="logic_name" /> tag:
   * [
   *   'name'   => 'logic_name'
   *   'action' => 'hide',
   *   'logic'  => [
   *     'and' => [
   *       [ 'rule' => [ 'control' => 'text_2', 'value' => 'something' ],
   *       [ 'rule' => [ 'user' => 'current', 'role' => 'admin' ] 
   *     ]
   *   ]
   * ]
   */
  $item['logic'] = template_system\get_logic_by_name( $item['logic'] );

  foreach( $item['logic']['logic'] as $rule_group ) {
    foreach( $rule_group as $rule ) {

      /**
       * It's a control rule, so we know we will evaluate this one on the frontend side
       */
      if( ! empty($rule['control']) ) continue;

      $rule = [
        'server_side_evaluated' => true,
        'result' => logic\evaluate_rule( $rule ) // We will need a way to access the evaluation callback from template_system
      ];
    }
  }
}

And then we could do something like this on the client-side to combine both client-side and server-side rules:

import { evaluate } from '@tangible/logic'

const result = evaluate( control.logic, data => {

  if( data.server_side_evaluated ) return data.result

  return /** evaluate control value dynamically */
})
eliot-akira commented 3 months ago

Does that mean we are harmonizing the syntax used in fields to be inline with what we do in L&L/blocks..? Both are very similar so if that's the case it shouldn't be too hard to keep backward compatibility

Or, if it's easier, maybe the two syntaxes to co-exist with <If control> being deprecated in favor of <Logic>. That way we can avoid having to convert between two similar data structures.

In your first example, the condition in the new syntax would look like this.

[
  'action'    => 'show',
  'logic' => [
    'and' => [
      [
        'rule' => [
          'control' => 'another-field',
          'value' => 'something',
          'compare' => 'equal'
        ]
      ],
      [
        'rule' => [
          'control' => 'and-another',
          'value' => 'something else',
          'compare' => 'not_equal'
        ]
      ],
    ],
  ],
]

It's longer because the rule syntax is more generic, and rule is itself one of many operators.

You're right, this syntax is very similar in structure to what already exists in the Fields module. That's one of the things I liked about the JSON Logic specs.

Well, it's up to you if you want to convert the existing functions, or have them co-exist somehow with the new ones. I suppose whichever is better for long-term maintenance and further development of the feature.


We also have a rule evaluation on the PHP side in fields (here) that uses the same syntax. I imagine we also want to update that part to template_system\evaluate_logic() to stay consistent, but I'm not sure we want to add template_system as a composer dependency of fields

Oh that's a good point. It's best to publish it as its own module so the Fields module can include only the Logic module instead of the whole Framework. OK, so the Logic module will be an NPM package (JS) and an independent Composer package (PHP).

Then the interface would be:

use tangible\logic;

$result = logic\evaluate( $condition, $evaluator );

That aligns well with the JS side:

import * as logic from '@tangible/logic'

const result = logic.evaluate( condition, evaluator )

About supporting a mixture of server and browser-side logic rules..

Can we replace all server-side rules with true and false values by evaluating them when the Logic tag is called?

Then the transformed rules can be passed to the frontend, with only the control value comparisons. The same reduced set of rules can be evaluated every time any control value changes, which I imagine can be frequent.


OK, I'll let you know when this is done:

There's actually an existing Logic module (v1) that's entwined with the internals of the template system. This part I always felt needed a clearer design. The new version based on JSON Logic is more elegant conceptually, and convenient how it has compatible evaluators on frontend and server side.

It brings me back to the Logic UI concept, I can picture a visual user interface for building logic rules for various purposes: location rules for templates, or visibility conditions for templates, blocks, block controls. It would be great to have a standard schema of logic rules for all these situations, and even UI components.

BenTangible commented 3 months ago

I was hoping to take this for a more thorough test drive, but I don't think I fully understand the proposed solution yet and how it's going to affect the rest of the language. It seems the main thing being discussed here is a replacement to logic variables. How does L&L determine whether the logic should be evaluated by the server or browser? Is there going to be a unique set of browser-side compatible subjects or will there be some overlap?

In any case, here are a few comments on the discussion in this thread. Overall, I like the ideas in this thread and will try to contribute something more solution-oriented once this has percolated a bit more.


The Not tag is confusing

I know that not is a common logical operator, but I find it confusing and redundant. In L&L, not is a comparison (sort of, it flips the logic and is "read" as being part of the comparison) and I think it's simplest if we left it only as a comparison, not a logical operator. For example, if I wanted to express "All rules must be false" as you mentioned above, I would intuitively approach it like this:

<And>
  <Rule A is not B />
  <Rule C is not D />
</And>

Because in my head, "all" is associated with "and" and "any" is associated with "or", making your nested Not > Or approach a bit confusing. Personally, I'd just omit the Not tag since it forces a more human-readable/intuitive logic structure even if it is a tiny bit more verbose.


action=hide appears to link logic with action unnecessarily

In our current approach, logic is defined inside the opening If tag and any action that arises from the logic being true is defined separately between the opening and closing tags. This seems necessary if we want L&L to be a flexible language. However, in this thread and the previous one I'm seeing action=hide within the Logic tag itself as if the Logic tag should also define what happens when it returns true. Is there a reason we'd want to define a show/hide action within the logic variable itself? My gut feeling is that we wouldn't want to hardcode any action or associate that with the Logic tag.


I don't think we should pass logic to the Loop tag

<Loop post_type=event logic=weekend_webinar> is a neat idea, but I think it could get us in trouble. If we're aiming for a consolidated syntax between server/browser logic processing, we'll prosumably want to support most if not all the current subjects defined in the If tag, many of which don't make sense passed to a loop. The only ones that really make sense are field and maybe variable. On top of potential conflicts, this now gives users four ways to filter their loops (logic, attributes, query parameters, and nested If tags) so if there's a performance concern, that adds yet another thing for new users to wrap their heads around. I'd just omit the Loop logic idea.

eliot-akira commented 3 months ago

Thanks for the feedback!

About not linking logic with action, the Logic tag does not do anything with action, it simply passes all attributes to the condition object it creates.

There is no link between logic and action, it's up to the evaluator what to do with condition properties that are passed to it. The example for action=hide is only relevant when using visibility logic for block controls, where the action is passed to the frontend evaluator to decide what to do when the given condition is true.


About the Not tag, I agree the example with <Not><Or> is confusing. We can think of a more intuitive way to express it.

Otherwise, I feel the Not tag is necessary for the expressiveness of combining rules, together with And and Or ("logical operators"). I write a lot of if statements, and not is a fundamental operator I use very often.

You're right the Rule tag can accept a not attribute when it's passed to the If tag. That provides a way to express "not this rule". But we also need a way to express "not this complex set of rules", where the Not tag is useful. So it depends on what is more natural to use in that particular context.


..That reminds me, I was wondering whether it's possible to use a logic variable inside another one:

<Logic name=weekend compare=or>
  <Rule field=day value=saturday />
  <Rule field=day value=sunday />
</Logic>

<Logic name=weekday>
  <Rule not logic=weekend />
</Logic>

Theoretically that should work, when the rules are evaluated by the If tag.

However, for the frontend evaluator being planned for block control visibility, so far we've only considered rules like <Rule control=x value=y />. If we want it to support <Rule logic> or <Rule not control>, we'll need to implement it.


I agree with not passing logic to the Loop tag directly. Most attributes for the Loop tag are for building up the database query, and they will not work as expected when mixed with the logic variable. For example:

<Loop type=event logic=weekend_webinar count=3>

That looks like it should return 3 weekend seminars, but instead will get 3 events then filter them based on the logic. If we don't give users the ability to apply filtering logic directly on the loop, they will have to use the If tag inside.

<Loop type=event count=3>
  <If logic=weekend_webinar>

That's clear that there will be 3 or less events that match.

It would be great, however, to consider how we can let users achieve the "natural" course of action, like: "Loop through 3 events that match this logic." Currently, I don't think there's a simple way to do it.

eliot-akira commented 3 months ago

@nicolas-jaussaud The Logic module is now published as its own Git repository.

https://github.com/TangibleInc/logic

It's a subrepo of the template system.

npm run subrepo init logic -r git@github.com:tangibleinc/logic -b main
npm run subrepo push logic

Its documentation includes a section on how to use from WordPress plugin or module. To summarize:

{
  "repositories": [
    {
      "type": "vcs",
      "url": "git@github.com:tangibleinc/logic"
    }
  ],
  "require": {
    "tangible/logic": "dev-main"
  },
  "minimum-stability": "dev"
}
require_once __DIR__ . '/vendor/tangible/logic/module.php';
import * as logic from './vendor/tangible/logic/index.ts'

Instead of importing it as an NPM package, I think it's better if the Fields module uses the same version of the Logic module on both frontend/backend, by loading from the vendor folder.

BenTangible commented 3 months ago

Thanks for the responses!

The example for action=hide is only relevant when using visibility logic for block controls, where the action is passed to the frontend evaluator to decide what to do when the given condition is true.

Wouldn't it only get passed to the frontend evaluator when it gets used, such as Control type=something logic=my_logic_variable? I would have assumed that when the visibility logic is true, the control is displayed, and when false, it's hidden. If the logic needs to be inverted, that can simply be done with the logic rules, not with an action attribute.


One other bit of feedback: in the first post you mention that logic will be built up with Rule tags, optionally grouped with the RuleGroup tag. Based on the examples, it seems we've pivoted to using And and Or tags, which I think is more intuitive and less verbose than RuleGroup compare=or. However, I'm not clear on the benefit of using the Rule tag. Ultimately, these rules are just conditions and use the same syntax (it seems) as the If tag. Well, other than the <Rule taxonomy=event_type term=webinar /> example which seems to use query parameters but I imagine that was a simple oversight. In any case, could we instead use closed If tags here rather than introducing a new tag? My question about server/browser subjects wasn't answered, but I assume it's going to be necessary to define specific subjects that are used to evaluate conditions in the browser since many of the existing subjects (field, loop, query, user, etc.) couldn't work with browser-side logic. Given the fact that down the road we'll likely need to create documentation for "frontend If" subjects/syntax anyway, it might be simpler to account for that now and not create a new Rule tag that essentially does the same thing as our If tag. Reading through this thread, the scope of overlap between If and Rule is worryingly confusing to me.

Sorry if I'm derailing the conversation, I just want to make sure we're thinking a few steps ahead here.

eliot-akira commented 3 months ago

If the logic needs to be inverted, that can simply be done with the logic rules, not with an action attribute.

True. The action attribute is used in some examples only because that property exists in the current implementation of block control visibility logic.

An advantage of having an "action" is that it's extensible, if we want to support any other frontend (or backend) actions in the future. But for our current purpose, I agree it's unnecessary - the default action can be show implicitly.


could we instead use closed If tags here rather than introducing a new tag?

The purpose of the Rule tag is to build a rule object with the given attributes. It's independent of what is used to evaluate it - so it would be confusing to combine it with the If tag, behaving differently when inside or outside the Logic tag. It might complicate any autocomplete feature in the code editor that we may add in the future.

It would also make it impossible to use the If tag to build rules dynamically, like:

<If something>
  <Rule some rule />
<Else />
  <Rule another rule />
</If>

How does L&L determine whether the logic should be evaluated by the server or browser?

Currently, that's determined by the user by passing the built rules to the If tag, which is server-side only; or the Control tag, only available within the Block Control template.

As Nicolas mentioned, for this latter use case it seems we need to support a mixture of server-side rules. Theoretically, I think it can be achieved by pre-evaluating such rules and replacing them with true/false, before passing the transformed rules to the frontend. But I'm not too comfortable with the idea - it will complicate the implementation as well as explaining to the user how it works.

Is there going to be a unique set of browser-side compatible subjects or will there be some overlap?

This is an open question to discuss and explore. So far, the only browser-side subject is control, which exists only when the logic is passed the Control tag.

With this example of a proposed way to mix frontend/backend rules:

<Logic name=example>
  <Rule control=text_1 value=something />
  <Rule user_role=admin />
</Logic>

<Control name=control logic=example />

The rule for user role is evaluated once on the server side. But the rule for control value is supposed to be evaluated dynamically when it changes on the frontend.

The difference in behavior is not clear from the syntax. Perhaps it should be made more explicit, like

<Rule dynamic control=text_1 value=something />

That would allow controlling where the rule gets evaluated. In this case, the following without dynamic would be evaluated once on the server side, like all other rules.

<Rule control=text_1 value=something />

Apart from this context of visibility conditions, for browser-side logic in general, I'm considering a special HTML attribute with a different function and syntax than the If tag, so that they're clearly separated. Something like x-if and x-show in Alpine.js.

eliot-akira commented 3 months ago

About logical operators to combine rules, it may be more intuitive to have All and Any tags, instead of (or as aliases to) And and Or. It might improve readability in some situations.

eliot-akira commented 3 months ago

About applying logic to loops, I think it's actually possible to achieve this:

<Loop type=event logic=weekend_webinar count=3>

There are only a few attributes (count and paged) that would need to be treated differently when logic is present, so that they're applied after the filtering instead of being part of the query.

Then the above code will work as expected, "Loop through 3 events that match this logic."

Otherwise, to get the same result, the user will have to figure out a trick to count posts inside the If tag and break out of the loop when a desired number is reached.

eliot-akira commented 3 months ago

About browser-side conditions, it would be great if we can leverage the Interactivity API that is now built into WordPress core. Here's the reference page with a list of supported HTML directives.

I'm having difficulty picturing how I would use any of the features though. Maybe we can come up with some concrete examples of browser-side conditions that would be useful for our purposes.

When we have an idea of specific use cases, we can explore if/how logic rules will fit into such dynamic frontend behavior. How they could be used together with If tags. And how well they would work in the context of block controls, which are inside the constrained environment of a page builder.

BenTangible commented 3 months ago

Perfect, thanks for the clarifications, I was mainly tripping up over visualizing how front-end and back-end conditions would be mixed, but I now see that this approach doesn't preclude that.


for browser-side logic in general, I'm considering a special HTML attribute with a different function and syntax than the If tag

If the Rule tag is already sharing its syntax with the If tag for conditions that can be evaluated on both the front-end and back-end (such as <Rule control=text_1 value=something />), I would assume our eventual front-end conditional logic evaluator tag would have to share the same syntax (or very similar with different attributes).


it may be more intuitive to have All and Any tags

That's a good point. I was thinking earlier about legibility and realized that the word "and" is generally placed between items in a list, which is what's done in most logic UIs: the and operator will be placed multiple times between the different rules. On the other hand, in L&L we're wrapping a bunch of rules in a tag, so the word "all" works better to group rules.

This also presents a less confusing alternative to the Not tag: we could allow true and false to be added as attributes to the Any and All tags. true would be the default so we might not need to mention it. So instead of:

<Not>
  <Or>
    <Rule ... />
    <Rule ... />
  </Or>
</Not>

It could instead be:

<All false>
  <Rule ... />
  <Rule ... />
</All>
eliot-akira commented 3 months ago

As an aside, I learned that "readability" is a real word.

Readability: the ease with which a reader can understand a written text; the concept exists in both natural language and programming languages

Legibility: the ease with which a reader can decode symbols, particularly in written language

The latter is about being able to recognize letters, for example the style of handwriting or printed font that's more or less legible. And the former is about how the use of words or flow of sentences can affect the reader's understanding.

The word was fresh in my mind because I was recently looking at a popular JavaScript library called readability that is used for Firefox Reader View, which takes an HTML document and extracts only the title and text.

Apparently, even the word "writability" exists, meaning "the ease of writing a program, given the problem it must solve".


I think <All false> is a bit ambiguous, it sounds like it means "all rules must be false" - but if it's meant to be a negation of all/and, it would behave as "any rule must be false".

All means all rules are true. In other words, if any rule is false, the whole thing is false. For example:

<All>
  <Rule check=1 value=1 />
  <Rule check=2 value=3 />
  <Rule check=3 value=3 />
</All>

The above results in false. Internally, when the rules are evaluated, as soon as the second rule fails, it returns false without even evaluating the rest of the rules.

When All is negated:

<Not>
  <Rule check=1 value=1 />
  <Rule check=2 value=3 />
  <Rule check=3 value=3 />
</Not>

This results in true, because not all rules are true. (Second rule is false.) The code above works without wrapping the rules in All because that's the default.

That's different from all rules are false.

<All false>
  <Rule check=1 value=1 />
  <Rule check=2 value=3 />
  <Rule check=3 value=3 />
</All>

This should result in false because not all rules are false. (First rule is true.) Internally, as soon as any rule is true, it returns false and skips the rest of the rules. Otherwise, it will check every rule and confirm that it's false.

Looking at the JSON Logic evaluator, I see there's a logical operator called none. It behaves as described above, as you can see from the comments: "First truthy, short circuit" (return false) and at the end, "None were truthy" (return true).


So I will implement:

These cover what are called Boolean operators (and, or, not).

<All false> can be translated internally to the logical operator none, to mean "all rules must be false".

eliot-akira commented 3 months ago

I was mainly tripping up over visualizing how front-end and back-end conditions would be mixed, but I now see that this approach doesn't preclude that.

Well.. I'm still wondering how we can cleanly achieve mixing frontend and backend logic.

JSON Logic is a good start because its data structure is cross-platform, with evaluators in multiple languages. But it was not designed for partially evaluating some rules on the backend, then the rest of the rules on the frontend - that sounds complicated.

It reminds me of a famous talk called "Simple Made Easy" by the creator of the Clojure language, where he explains the meaning of the word "complect" and how it relates to conceptual simplicity in software. When more than one thing is complected (braided/folded/twisted) together, complexity arises.


Interestingly, this question of mixing client/server logic is very relevant these days in the world of React, where they released a feature for server-side components (blog announcement and docs).

Traditionally, all React components were client side, rendered in the browser; or, rendered on the server then "hydrated" (rendered again) in the client to make them dynamic. With the new feature, developers can make some components render entirely on the server, shipping only the resulting HTML to the browser. That's much more efficient in terms of JS bundle size and performance.

Newer frameworks that evolved after (or on top of) React usually include such a feature already, such as server components in Next.js, client/server modules in Remix, or client directives in Astro.

With L&L, we're working in the opposite direction. By default all templates are rendered on the server, and gradually we've been introducing more client-side behavior like pagination, where parts of the template are dynamically rendered.


There's a new internal project called Elandel.

TypeScript library with an extensible HTML engine based on Unified and hast (Hypertext Abstract Syntax Tree format); and CSS engine based on PostCSS

It can parse a template into a syntax tree; beautify its formatting; render with loops, logic, and dynamic content.

Originally the HTML and CSS engines were created to solve a need in the template code editor, to better integrate with the extended syntax we're using. (The new HTML formatter is better designed and more flexible to customize than the old one based on Prettier/Angular's HTML formatter. I remember you pointed out a small but very significant detail regarding the spaces between tags, so if you're up for it, I'd love to work together on refining the details.)

These features were organized into an independent module (separate from WordPress and PHP-specific code) because I realized it has the potential to become the foundation of a cross-platform template language that can run entirely in the browser, or on server-side JavaScript runtimes such as Node and Bun.

As it evolves, it could use headless WordPress (REST API) as a data source, among others like SQLite, SQLocal (SQLite WASM), or in-memory objects for testing. The language will be compatible with L&L, so we can use it for instant live preview in the editor, or runnable code examples in the docs.

It will be developed to first prioritize practical benefits for L&L and Tangible Blocks. But the long-term vision gives me as a fresh perspective, particularly in looking at all the frontend features that exist in the Template System, and considering if/how they can be organized into general-purpose functions that are useful for a template engine that runs on the frontend. For example, the TypeScript evaluator for JSON Logic will be a suitable basis for If and Logic tags in Elandel.


How that relates to mixing frontend/backend logic, or the possible use of HTML directives.. I think we'll need to explore/experiment, discuss and develop it according to our practical needs.

So far, it seems that whatever solution we create for block control visibility has a specific context that might not apply to a general solution for all frontend logic. We don't have enough concrete use cases for the latter, so the solution is not clear yet. In contrast, there is an existing implementation for visibility logic, so it's clear what needs to be achieved as the end result.

Looking at the available HTML directives in the Interactivity API, there is no x-if or x-show like in Alpine.

Even if it did, we probably won't be able to use it to show/hide block controls, because of how controls are rendered in the page builder settings form. Even if that's possible, every control would need to be wrapped in a div with special HTML attributes to determine its visibility condition. So it might not be ergonomic in terms of writability.

eliot-akira commented 3 months ago

The All and Any tags have been added in commit https://github.com/TangibleInc/template-system/commit/ced49b711ba7d68c1cd4a276421236d80bd9b880. They combine rules with the and and or operators respectively.

They accept the attribute false to mean:

I've also updated the Logic tag to accept attributes compare=any and compare=all.


To rewrite the examples from my comment above (which I plan to use as rough draft for a docs page about the Logic tag)..

All rules must be true

<All>
  <Rule ... />
  <Rule ... />
</All>

All rules must be false

<All false>
  <Rule ... />
  <Rule ... />
</All>

At least one rule must be true

<Any>
  <Rule ... />
  <Rule ... />
</Any>

At least one rule must be false

<Any false>
  <Rule ... />
  <Rule ... />
</Not>

Not all rules are true

<Not>
  <Rule ... />
  <Rule ... />
</Not>

This is the negation of All, which is equivalent to <Any false>.

I find this more intuitive, but that's subjective and can depend on what kind of condition is being expressed. For example, given a complex condition like the "weekend webinar":

<All>
  <Rule taxonomy=event_type term=webinar />
  <Any>
    <Rule field=event_date value=Saturday />
    <Rule field=event_date value=Sunday />
  </Any>
</All>

It's easier to get the opposite logic, "weekday seminar", by replacing All with Not.

<Not>
  <Rule taxonomy=event_type term=webinar />
  <Any>
    <Rule field=event_date value=Saturday />
    <Rule field=event_date value=Sunday />
  </Any>
</Not>

Compared to using "any false", even though the result is the same.

<Any false>
  <Rule taxonomy=event_type term=webinar />
  <Any>
    <Rule field=event_date value=Saturday />
    <Rule field=event_date value=Sunday />
  </Any>
</Any>

Of course in this case it's better to use the Logic tag's attribute compare.

<Logic name=weekday_seminar compare=not>
  ...
</Logic>

Or use a previously defined logic as a rule and negate it.

<Logic name=weekday_seminar>
  <Rule not logic=weekend_seminar />
</Logic>

This last example reminds me.. When passing a logic variable to the frontend, any other logic variable used inside must also be passed so that the evaluator can refer to it.

..OK, so I'll close this issue as done, since the foundation of the Logic tag is now prepared.

We can discuss further about specific aspects: