feat(conditional-content): {show}/{hide} tag processing with conditions engine
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 47s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 59s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Blocked by required conditions
Generic: Project CI / Tests (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (push) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (push) Blocked by required conditions
Platform: moko-platform CI / CI Summary (push) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Blocked by required conditions
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Blocked by required conditions
Joomla: Extension CI / PHPStan Analysis (pull_request) Blocked by required conditions
Joomla: Extension CI / Build RC Pre-Release (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 4: Governance (pull_request) Blocked by required conditions
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Blocked by required conditions
Platform: moko-platform CI / CI Summary (pull_request) Blocked by required conditions
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 9s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Universal: Auto Version Bump / Version Bump (push) Successful in 19s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 14s
Universal: PR Check / Branch Policy (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Generic: Repo Health / Access control (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 38s
Generic: Project CI / Lint & Validate (pull_request) Successful in 51s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 52s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 47s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 59s
- processConditionalTags() handles {show}, {hide}, {else} blocks
- Supports condition="alias_or_id" for saved condition sets
- Supports inline rules: access_level, user_group, menu_item, home_page, date, day, url
- Multiple inline rules per tag use AND logic
- Nested tag support (processes innermost first, up to 10 iterations)
- Runs in onContentPrepare (articles) and onAfterRender (final HTML)
- ConditionsHelper: added passByAlias(), resolveAlias(), evaluateInlineRule()
- Implements #164
This commit is contained in:
@@ -420,6 +420,74 @@ class ConditionsHelper
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a condition by its alias string.
|
||||
*
|
||||
* @param string $alias The condition alias.
|
||||
*
|
||||
* @return bool True when the condition passes.
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
public static function passByAlias(string $alias): bool
|
||||
{
|
||||
$id = self::resolveAlias($alias);
|
||||
|
||||
if ($id === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::pass($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a condition reference that may be an integer ID or an alias string.
|
||||
*
|
||||
* @param string $ref The reference (numeric ID or alias).
|
||||
*
|
||||
* @return int|null The condition ID, or null if not found.
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
public static function resolveAlias(string $ref): ?int
|
||||
{
|
||||
if (is_numeric($ref)) {
|
||||
return (int) $ref;
|
||||
}
|
||||
|
||||
$db = Factory::getContainer()->get('DatabaseDriver');
|
||||
|
||||
$query = $db->getQuery(true)
|
||||
->select($db->quoteName('id'))
|
||||
->from($db->quoteName('#__mokosuiteclient_conditions'))
|
||||
->where($db->quoteName('alias') . ' = :alias')
|
||||
->bind(':alias', $ref);
|
||||
|
||||
$id = $db->setQuery($query)->loadResult();
|
||||
|
||||
return $id !== null ? (int) $id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a single inline rule (public wrapper around passRule).
|
||||
*
|
||||
* @param string $type The rule type (e.g. 'visitor__access_level').
|
||||
* @param object $params The rule params object.
|
||||
*
|
||||
* @return bool
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
public static function evaluateInlineRule(string $type, object $params): bool
|
||||
{
|
||||
$rule = (object) [
|
||||
'type' => $type,
|
||||
'params' => $params,
|
||||
];
|
||||
|
||||
return self::passRule($rule);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the evaluation cache (useful between requests in testing).
|
||||
*
|
||||
|
||||
@@ -2968,4 +2968,274 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
|
||||
|
||||
$modules = $filtered;
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
// Conditional Content Tags ({show}/{hide})
|
||||
// ------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Process conditional content tags in article/content body.
|
||||
*
|
||||
* @param string $context The context of the content being passed.
|
||||
* @param object $article The article object.
|
||||
* @param object $params The article params.
|
||||
* @param int $page The page number.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
public function onContentPrepare($context, &$article, &$params, $page = 0): void
|
||||
{
|
||||
// Skip admin, search indexer contexts, and empty text.
|
||||
if (!$this->getApplication()->isClient('site'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ($context === 'com_finder.indexer')
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$text = $article->text ?? ($article->introtext ?? '');
|
||||
|
||||
if ($text === '' || (strpos($text, '{show') === false && strpos($text, '{hide') === false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processConditionalTags($text);
|
||||
|
||||
// Write back to whichever property was populated.
|
||||
if (isset($article->text))
|
||||
{
|
||||
$article->text = $text;
|
||||
}
|
||||
else
|
||||
{
|
||||
$article->introtext = $text;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process conditional content tags in the final rendered HTML body.
|
||||
*
|
||||
* Catches tags in modules, template overrides, and other places that
|
||||
* onContentPrepare does not reach.
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
public function onAfterRender(): void
|
||||
{
|
||||
if (!$this->getApplication()->isClient('site'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $this->getApplication()->getBody();
|
||||
|
||||
if ($body === '' || (strpos($body, '{show') === false && strpos($body, '{hide') === false))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$this->processConditionalTags($body);
|
||||
$this->getApplication()->setBody($body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process {show} and {hide} conditional content tags.
|
||||
*
|
||||
* Supported syntax:
|
||||
* {show condition="alias_or_id"}content{/show}
|
||||
* {hide condition="alias_or_id"}content{/hide}
|
||||
* {show condition="alias"}shown{else}hidden{/show}
|
||||
* {show access_level="1,2"}content{/show}
|
||||
* {show user_group="8"}content{/show}
|
||||
* {show menu_item="101,102"}content{/show}
|
||||
* {show home_page="1"}content{/show}
|
||||
*
|
||||
* @param string &$text The text to process (modified in place).
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function processConditionalTags(string &$text): void
|
||||
{
|
||||
// Process innermost tags first (handles nesting by repeating).
|
||||
$maxIterations = 10;
|
||||
$iteration = 0;
|
||||
|
||||
while ($iteration < $maxIterations
|
||||
&& (strpos($text, '{show') !== false || strpos($text, '{hide') !== false))
|
||||
{
|
||||
$pattern = '#\{(show|hide)\s+([^}]*)\}(.*?)(?:\{else\}(.*?))?\{/\1\}#si';
|
||||
$self = $this;
|
||||
|
||||
$newText = preg_replace_callback($pattern, function ($matches) use ($self) {
|
||||
$tag = strtolower($matches[1]); // 'show' or 'hide'
|
||||
$attributes = $matches[2];
|
||||
$content = $matches[3];
|
||||
$elseBlock = $matches[4] ?? '';
|
||||
|
||||
$passes = $self->evaluateTagCondition($attributes);
|
||||
|
||||
// For {hide}, invert the logic.
|
||||
if ($tag === 'hide')
|
||||
{
|
||||
$passes = !$passes;
|
||||
}
|
||||
|
||||
return $passes ? $content : $elseBlock;
|
||||
}, $text);
|
||||
|
||||
// If nothing changed, stop iterating.
|
||||
if ($newText === $text)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
$text = $newText;
|
||||
$iteration++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the condition specified by tag attributes.
|
||||
*
|
||||
* @param string $attributes The raw attribute string from inside the tag.
|
||||
*
|
||||
* @return bool True if the condition passes.
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function evaluateTagCondition(string $attributes): bool
|
||||
{
|
||||
$helper = \Moko\Component\MokoSuiteClient\Administrator\Helper\ConditionsHelper::class;
|
||||
|
||||
// Parse all key="value" pairs from the attribute string.
|
||||
$attrs = [];
|
||||
|
||||
if (preg_match_all('/(\w+)\s*=\s*"([^"]*)"/', $attributes, $attrMatches, PREG_SET_ORDER))
|
||||
{
|
||||
foreach ($attrMatches as $m)
|
||||
{
|
||||
$attrs[strtolower($m[1])] = $m[2];
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Saved condition reference: condition="alias_or_id"
|
||||
if (!empty($attrs['condition']))
|
||||
{
|
||||
$ref = trim($attrs['condition']);
|
||||
|
||||
return $helper::passByAlias($ref);
|
||||
}
|
||||
|
||||
// 2. Inline rules — map attribute names to rule types.
|
||||
$inlineMap = [
|
||||
'access_level' => 'visitor__access_level',
|
||||
'user_group' => 'visitor__user_group',
|
||||
'menu_item' => 'menu__menu_item',
|
||||
'home_page' => 'menu__home_page',
|
||||
'date' => 'date__date',
|
||||
'day' => 'date__day',
|
||||
'url' => 'other__url',
|
||||
];
|
||||
|
||||
// Evaluate all inline attributes; ALL must pass (AND logic).
|
||||
$hasInline = false;
|
||||
|
||||
foreach ($inlineMap as $attrName => $ruleType)
|
||||
{
|
||||
if (!isset($attrs[$attrName]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasInline = true;
|
||||
$rawValue = $attrs[$attrName];
|
||||
|
||||
$params = $this->buildInlineRuleParams($ruleType, $rawValue);
|
||||
|
||||
if (!$helper::evaluateInlineRule($ruleType, $params))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If we processed at least one inline rule and none failed, pass.
|
||||
if ($hasInline)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// No recognised attributes — default to not showing.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a params object for an inline rule from the raw attribute value.
|
||||
*
|
||||
* @param string $ruleType The rule type identifier.
|
||||
* @param string $rawValue The raw comma-separated value from the tag attribute.
|
||||
*
|
||||
* @return object A params object suitable for ConditionsHelper::evaluateInlineRule().
|
||||
*
|
||||
* @since 02.48.00
|
||||
*/
|
||||
private function buildInlineRuleParams(string $ruleType, string $rawValue): object
|
||||
{
|
||||
$params = new \stdClass();
|
||||
|
||||
switch ($ruleType)
|
||||
{
|
||||
case 'visitor__access_level':
|
||||
case 'visitor__user_group':
|
||||
case 'menu__menu_item':
|
||||
case 'date__day':
|
||||
// Comma-separated list of IDs.
|
||||
$params->selection = array_map('trim', explode(',', $rawValue));
|
||||
$params->comparison = 'any';
|
||||
break;
|
||||
|
||||
case 'menu__home_page':
|
||||
// Single boolean-like value: "1" or "0".
|
||||
$params->selection = [trim($rawValue)];
|
||||
break;
|
||||
|
||||
case 'date__date':
|
||||
// Supports "after:2026-01-01", "before:2026-12-31",
|
||||
// "between:2026-01-01,2026-12-31", or plain date (defaults to 'after').
|
||||
$parts = explode(':', $rawValue, 2);
|
||||
|
||||
if (\count($parts) === 2 && \in_array($parts[0], ['before', 'after', 'between'], true))
|
||||
{
|
||||
$params->comparison = $parts[0];
|
||||
$params->selection = array_map('trim', explode(',', $parts[1]));
|
||||
}
|
||||
else
|
||||
{
|
||||
$params->comparison = 'after';
|
||||
$params->selection = array_map('trim', explode(',', $rawValue));
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 'other__url':
|
||||
// Comma-separated regex patterns.
|
||||
$params->selection = array_map('trim', explode(',', $rawValue));
|
||||
break;
|
||||
|
||||
default:
|
||||
$params->selection = array_map('trim', explode(',', $rawValue));
|
||||
break;
|
||||
}
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user