feat: Articles Field, Content Templater — complete Regular Labs suite
Universal: Auto Version Bump / Version Bump (push) Successful in 24s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 30s
Generic: Project CI / Lint & Validate (pull_request) Successful in 16s
Platform: moko-platform CI / Gate 1: Code Quality (push) Failing after 59s
Joomla: Extension CI / Release Readiness Check (pull_request) Failing after 8s
Joomla: Extension CI / Lint & Validate (pull_request) Failing after 56s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Universal: Secret Scanning / Gitleaks Secret Scan (pull_request) Successful in 7s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Platform: moko-platform CI / Gate 1: Code Quality (pull_request) Failing after 51s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 3s
Joomla: Metadata Validation / Validate Joomla Metadata (pull_request) Successful in 55s
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (push) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (push) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (push) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (push) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (push) Has been cancelled
Platform: moko-platform CI / CI Summary (push) Has been cancelled
Generic: Project CI / Tests (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.2) (pull_request) Has been cancelled
Joomla: Extension CI / Tests (PHP 8.3) (pull_request) Has been cancelled
Joomla: Extension CI / PHPStan Analysis (pull_request) Has been cancelled
Joomla: Extension CI / Build RC Pre-Release (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.1) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.2) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 2: Unit Tests (8.3) (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 3: Self-Health Check (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 4: Governance (pull_request) Has been cancelled
Platform: moko-platform CI / Gate 5: Template Integrity (pull_request) Has been cancelled
Platform: moko-platform CI / CI Summary (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Generic: Repo Health / Scripts governance (pull_request) Has been cancelled
Generic: Repo Health / Repository health (pull_request) Has been cancelled
Generic: Repo Health / Report Issues (pull_request) Has been cancelled

- Articles Field (#162): custom ListField type for article selection
  in any Joomla form, shows "Title [Category]" options
- Content Templater (#165): {template alias="x"} tags load pre-defined
  article templates from DB, returns introtext+fulltext from JSON
- content_templates table added to install.mysql.sql
- All Regular Labs replacements now implemented:
  #160 conditions + AMM, #161 articles anywhere, #162 articles field,
  #164 conditional content, #165 content templater, #167 email protector,
  #175 rereplacer, #176 snippets, #177 sourcerer, #180 users anywhere,
  #181 cache cleaner
This commit is contained in:
Jonathan Miller
2026-06-23 13:01:29 -05:00
parent 2702aea14a
commit 60a541fec1
4 changed files with 206 additions and 0 deletions
@@ -326,3 +326,27 @@ CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_replacements` (
PRIMARY KEY (`id`),
KEY `idx_published` (`published`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
-- Content Templates
--
CREATE TABLE IF NOT EXISTS `#__mokosuiteclient_content_templates` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`alias` VARCHAR(100) NOT NULL DEFAULT '',
`name` VARCHAR(255) NOT NULL DEFAULT '',
`description` TEXT NOT NULL,
`category` VARCHAR(50) NOT NULL DEFAULT '',
`color` VARCHAR(8) DEFAULT NULL,
`template_data` MEDIUMTEXT NOT NULL,
`joomla_category_id` INT NOT NULL DEFAULT 0,
`access` INT UNSIGNED NOT NULL DEFAULT 1,
`published` TINYINT(1) NOT NULL DEFAULT 1,
`ordering` INT NOT NULL DEFAULT 0,
`checked_out` INT UNSIGNED DEFAULT NULL,
`checked_out_time` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_published` (`published`),
KEY `idx_alias` (`alias`),
KEY `idx_category` (`joomla_category_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
@@ -3023,6 +3023,14 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
$modified = true;
}
if (stripos($text, '{template') !== false
&& (int) $this->params->get('content_templates_enabled', 0) === 1
&& $this->contentTemplatesTableExists())
{
$this->processTemplateTags($text);
$modified = true;
}
if (stripos($text, '{article') !== false && (int) $this->params->get('articles_anywhere_enabled', 0) === 1)
{
$this->processArticleTags($text);
@@ -3119,6 +3127,14 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
$changed = true;
}
if (stripos($body, '{template') !== false
&& (int) $this->params->get('content_templates_enabled', 0) === 1
&& $this->contentTemplatesTableExists())
{
$this->processTemplateTags($body);
$changed = true;
}
if (stripos($body, '{article') !== false
&& (int) $this->params->get('articles_anywhere_enabled', 0) === 1)
{
@@ -3939,4 +3955,95 @@ class MokoSuiteClient extends CMSPlugin implements BootableExtensionInterface
};
}, $text);
}
/**
* Process {template alias="..."} content tags.
*
* Loads a content template from the database, decodes the JSON
* `template_data` column, and returns the concatenation of its
* `introtext` and `fulltext` fields.
*
* @param string &$text The text to process (modified in place).
*
* @return void
*
* @since 02.48.00
*/
private function processTemplateTags(string &$text): void
{
if (!$this->params->get('content_templates_enabled', 0))
{
return;
}
$pattern = '#\{template\s+([^}]+)\}#i';
$text = preg_replace_callback($pattern, function ($match) {
$attrs = $this->parseTagAttributes($match[1]);
$alias = $attrs['alias'] ?? $attrs['id'] ?? '';
if (empty($alias))
{
return $match[0];
}
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$query = $db->getQuery(true)
->select($db->quoteName('template_data'))
->from($db->quoteName('#__mokosuiteclient_content_templates'))
->where($db->quoteName('published') . ' = 1');
if (is_numeric($alias))
{
$query->where($db->quoteName('id') . ' = ' . (int) $alias);
}
else
{
$query->where($db->quoteName('alias') . ' = ' . $db->quote($alias));
}
$db->setQuery($query);
$data = json_decode($db->loadResult() ?? '{}');
return ($data->introtext ?? '') . ($data->fulltext ?? '');
}
catch (\Throwable $e)
{
return '';
}
}, $text);
}
/**
* Check whether the content_templates DB table exists.
*
* @return bool
*
* @since 02.48.00
*/
private function contentTemplatesTableExists(): bool
{
static $exists = null;
if ($exists !== null)
{
return $exists;
}
try
{
$db = Factory::getContainer()->get(\Joomla\Database\DatabaseInterface::class);
$tables = $db->getTableList();
$prefix = $db->getPrefix();
$exists = \in_array($prefix . 'mokosuiteclient_content_templates', $tables, true);
}
catch (\Exception $e)
{
$exists = false;
}
return $exists;
}
}
@@ -0,0 +1,67 @@
<?php
/**
* @package MokoSuiteClient
* @subpackage plg_system_mokosuiteclient
* @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
* @license GNU General Public License version 3 or later; see LICENSE
*
* FILE INFORMATION
* DEFGROUP: Joomla.Plugin
* INGROUP: MokoSuiteClient
* VERSION: 02.47.62
* PATH: /src/Field/ArticlesField.php
* BRIEF: List field that populates with published Joomla articles
*/
namespace Moko\Plugin\System\MokoSuiteClient\Field;
defined('_JEXEC') or die;
use Joomla\CMS\Form\Field\ListField;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Factory;
use Joomla\Database\DatabaseInterface;
/**
* Form field that renders a dropdown (or multi-select) of all published
* articles, grouped by category name.
*
* Usage in XML:
* <field name="related_article" type="Articles" label="Related Article" multiple="true" />
*
* @since 02.47.62
*/
class ArticlesField extends ListField
{
protected $type = 'Articles';
protected function getOptions(): array
{
$options = parent::getOptions();
$db = Factory::getContainer()->get(DatabaseInterface::class);
$query = $db->getQuery(true)
->select([
$db->quoteName('a.id', 'value'),
$db->quoteName('a.title', 'text'),
$db->quoteName('c.title', 'category'),
])
->from($db->quoteName('#__content', 'a'))
->leftJoin($db->quoteName('#__categories', 'c') . ' ON c.id = a.catid')
->where($db->quoteName('a.state') . ' = 1')
->order($db->quoteName('a.title') . ' ASC');
$db->setQuery($query);
$articles = $db->loadObjectList() ?: [];
foreach ($articles as $article) {
$label = $article->text;
if (!empty($article->category)) {
$label .= ' [' . $article->category . ']';
}
$options[] = HTMLHelper::_('select.option', $article->value, $label);
}
return $options;
}
}
@@ -123,6 +123,14 @@
<option value="0">JNO</option>
</field>
<field name="content_templates_enabled" type="radio" default="0"
label="Content Templates"
description="Enable {template alias=&quot;name&quot;} content tags. Loads structured template data from the content_templates table and renders introtext + fulltext."
class="btn-group btn-group-yesno">
<option value="1">JYES</option>
<option value="0">JNO</option>
</field>
<field name="articles_anywhere_enabled" type="radio" default="0"
label="Articles Anywhere"
description="Enable {article id=&quot;42&quot;}[title]{/article} content tags. Insert article data anywhere using template placeholders for title, introtext, author, category, dates, images, and more."