release 2023.03.16 - settings playground

master
Jean-Christian Paul Denis 2023-03-16 21:00:45 +01:00
parent d0090bf9a1
commit b2f3ff7021
Signed by: JcDenis
GPG Key ID: 1B5B8C5B90B6C951
13 changed files with 199 additions and 155 deletions

View File

@ -3,6 +3,11 @@ dev
- [ ] source translation
- [ ] fix third-party API (temp removed)
2023.03.16
- require php 8.1
- tyring something diferent for settings
- fix previously introduced bug
2023.03.15
- update to Dotclear 2.26-dev
- use namespace

View File

@ -18,7 +18,7 @@ $this->registerModule(
'Translater',
'Translate your Dotclear plugins and themes',
'Jean-Christian Denis & contributors',
'2023.03.15',
'2023.03.16',
[
'requires' => [['core', '2.26']],
'permissions' => null,

View File

@ -2,10 +2,10 @@
<modules xmlns:da="http://dotaddict.org/da/">
<module id="translater">
<name>Translater</name>
<version>2023.03.15</version>
<version>2023.03.16</version>
<author>Jean-Christian Denis &amp; contributors</author>
<desc>Translate your Dotclear plugins and themes</desc>
<file>https://github.com/JcDenis/translater/releases/download/v2023.03.15/plugin-translater.zip</file>
<file>https://github.com/JcDenis/translater/releases/download/v2023.03.16/plugin-translater.zip</file>
<da:dcmin>2.26</da:dcmin>
<da:details>https://plugins.dotaddict.org/dc2/details/translater</da:details>
<da:support>http://forum.dotclear.org/viewtopic.php?id=39220</da:support>

View File

@ -25,7 +25,7 @@ class Backend extends dcNsProcess
public static function init(): bool
{
if (defined('DC_CONTEXT_ADMIN')) {
self::$init = dcCore::app()->auth->isSuperAdmin();
self::$init = dcCore::app()->auth->isSuperAdmin() && version_compare(phpversion(), My::PHP_MIN, '>=');
}
return self::$init;

View File

@ -16,7 +16,7 @@ namespace Dotclear\Plugin\translater;
use adminModulesList;
use dcCore;
use Dotclear\Helper\Html\Form\Input;
use Dotclear\Helper\Html\Form\Submit;
use html;
class BackendBehaviors
@ -50,18 +50,18 @@ class BackendBehaviors
public static function adminModulesGetActions(adminModulesList $list, string $id, array $prop): ?string
{
if ($list->getList() != $prop['type'] . '-activate'
|| !self::translater()->get($prop['type'] . '_menu')
|| !self::translater()->getSetting($prop['type'] . '_menu')
|| !dcCore::app()->auth->isSuperAdmin()
) {
return null;
}
if (self::translater()->get('hide_default')
if (self::translater()->hide_default
&& in_array($id, My::defaultDistribModules($prop['type']))
) {
return null;
}
return (new Input(['translater[' . html::escapeHTML($id) . ']', null]))->value(__('Translate'));
return (new Submit(['translater[' . html::escapeHTML($id) . ']', null]))->value(__('Translate'))->render();
}
/**

View File

@ -35,7 +35,13 @@ class Config extends dcNsProcess
{
public static function init(): bool
{
self::$init = defined('DC_CONTEXT_ADMIN');
if (defined('DC_CONTEXT_ADMIN')) {
if (version_compare(phpversion(), My::PHP_MIN, '>=')) {
self::$init = true;
} else {
dcCore::app()->error->add(sprintf(__('Translater required php >= %s'), My::PHP_MIN));
}
}
return self::$init;
}
@ -51,13 +57,12 @@ class Config extends dcNsProcess
return true;
}
$translater = new Translater();
$s = new Settings();
try {
foreach (My::defaultSettings() as $key => $value) {
$translater->set($key, $_POST[$key] ?? '');
foreach ($s->listSettings() as $key) {
$s->writeSetting($key, $_POST[$key] ?? '');
}
$translater->writeSettings();
dcPage::addSuccessNotice(
__('Configuration successfully updated.')
@ -85,38 +90,38 @@ class Config extends dcNsProcess
(new Fieldset())->class('fieldset')->legend((new Legend(__('Translation'))))->fields([
// write_langphp
(new Para())->items([
(new Checkbox('write_langphp', $translater->get('write_langphp')))->value(1),
(new Checkbox('write_langphp', $translater->write_langphp))->value(1),
(new Label(__('Write .lang.php files'), Label::OUTSIDE_LABEL_AFTER))->for('write_langphp')->class('classic'),
]),
// scan_tpl
(new Para())->items([
(new Checkbox('scan_tpl', $translater->get('scan_tpl')))->value(1),
(new Checkbox('scan_tpl', $translater->scan_tpl))->value(1),
(new Label(__('Translate also strings of template files'), Label::OUTSIDE_LABEL_AFTER))->for('scan_tpl')->class('classic'),
]),
// parse_nodc
(new Para())->items([
(new Checkbox('parse_nodc', $translater->get('parse_nodc')))->value(1),
(new Checkbox('parse_nodc', $translater->parse_nodc))->value(1),
(new Label(__('Translate only unknow strings'), Label::OUTSIDE_LABEL_AFTER))->for('parse_nodc')->class('classic'),
]),
// hide_default
(new Para())->items([
(new Checkbox('hide_default', $translater->get('hide_default')))->value(1),
(new Checkbox('hide_default', $translater->hide_default))->value(1),
(new Label(__('Hide default modules of Dotclear'), Label::OUTSIDE_LABEL_AFTER))->for('hide_default')->class('classic'),
]),
// parse_comment
(new Para())->items([
(new Checkbox('parse_comment', $translater->get('parse_comment')))->value(1),
(new Checkbox('parse_comment', $translater->parse_comment))->value(1),
(new Label(__('Write comments in files'), Label::OUTSIDE_LABEL_AFTER))->for('parse_comment')->class('classic'),
]),
// parse_user
(new Para())->items([
(new Checkbox('parse_user', $translater->get('parse_user')))->value(1),
(new Checkbox('parse_user', $translater->parse_user))->value(1),
(new Label(__('Write informations about author in files'), Label::OUTSIDE_LABEL_AFTER))->for('parse_user')->class('classic'),
]),
// parse_userinfo
(new Para())->items([
(new Label(__('User info:')))->for('parse_userinfo'),
(new Input('parse_userinfo'))->size(65)->maxlenght(255)->value($translater->get('parse_userinfo')),
(new Input('parse_userinfo'))->size(65)->maxlenght(255)->value($translater->parse_userinfo),
]),
(new Note())->text(sprintf(
__('Following informations can be used: %s'),
@ -126,47 +131,47 @@ class Config extends dcNsProcess
(new Fieldset())->class('fieldset')->legend((new Legend(__('Import/Export'))))->fields([
// import_overwrite
(new Para())->items([
(new Checkbox('import_overwrite', $translater->get('import_overwrite')))->value(1),
(new Checkbox('import_overwrite', $translater->import_overwrite))->value(1),
(new Label(__('Overwrite existing languages'), Label::OUTSIDE_LABEL_AFTER))->for('import_overwrite')->class('classic'),
]),
// export_filename
(new Para())->items([
(new Label(__('Name of exported package:')))->for('export_filename'),
(new Input('export_filename'))->size(65)->maxlenght(255)->value($translater->get('export_filename')),
(new Input('export_filename'))->size(65)->maxlenght(255)->value($translater->export_filename),
]),
]),
(new Fieldset())->class('fieldset')->legend((new Legend(__('Backups'))))->fields([
// backup_auto
(new Para())->items([
(new Checkbox('backup_auto', $translater->get('backup_auto')))->value(1),
(new Checkbox('backup_auto', $translater->backup_auto))->value(1),
(new Label(__('Make backups when changes are made'), Label::OUTSIDE_LABEL_AFTER))->for('backup_auto')->class('classic'),
]),
// backup_limit
(new Para())->items([
(new Label(__('Limit backups per module to:')))->for('backup_limit')->class('classic'),
(new Number('backup_limit'))->min(0)->max(50)->value($translater->get('backup_limit')),
(new Number('backup_limit'))->min(0)->max(50)->value($translater->backup_limit),
]),
(new Note())->text(__('Set to 0 for no limit.'))->class('form-note'),
// backup_folder
(new Para())->items([
(new Label(__('Store backups in:')))->for('backup_folder'),
(new Select('backup_folder'))->default($translater->get('backup_folder'))->items(My::backupFoldersCombo()),
(new Select('backup_folder'))->default($translater->backup_folder)->items(My::backupFoldersCombo()),
]),
]),
(new Fieldset())->class('fieldset')->legend((new Legend(__('Behaviors'))))->fields([
// start_page
(new Para())->items([
(new Label(__('Default start menu:')))->for('start_page'),
(new Select('start_page'))->default($translater->get('start_page'))->items(My::startPageCombo()),
(new Select('start_page'))->default($translater->start_page)->items(My::startPageCombo()),
]),
// plugin_menu
(new Para())->items([
(new Checkbox('plugin_menu', $translater->get('plugin_menu')))->value(1),
(new Checkbox('plugin_menu', $translater->plugin_menu))->value(1),
(new Label(__('Enable menu on plugins page'), Label::OUTSIDE_LABEL_AFTER))->for('plugin_menu')->class('classic'),
]),
// theme_menu
(new Para())->items([
(new Checkbox('theme_menu', $translater->get('theme_menu')))->value(1),
(new Checkbox('theme_menu', $translater->theme_menu))->value(1),
(new Label(__('Enable menu on themes page'), Label::OUTSIDE_LABEL_AFTER))->for('theme_menu')->class('classic'),
]),

View File

@ -22,7 +22,9 @@ class Install extends dcNsProcess
{
public static function init(): bool
{
self::$init = defined('DC_CONTEXT_ADMIN') && dcCore::app()->newVersion(My::id(), dcCore::app()->plugins->moduleInfo(My::id(), 'version'));
self::$init = defined('DC_CONTEXT_ADMIN')
&& version_compare(phpversion(), My::PHP_MIN, '>=')
&& dcCore::app()->newVersion(My::id(), dcCore::app()->plugins->moduleInfo(My::id(), 'version'));
return self::$init;
}
@ -36,11 +38,6 @@ class Install extends dcNsProcess
try {
self::growUp();
foreach (My::defaultSettings() as $key => $value) {
dcCore::app()->blog->settings->get(My::id())->drop($key);
dcCore::app()->blog->settings->get(My::id())->put($key, $value, gettype($value), '', false, true);
}
return true;
} catch (Exception $e) {
dcCore::app()->error->add($e->getMessage());

View File

@ -27,7 +27,7 @@ class Manage extends dcNsProcess
public static function init(): bool
{
if (defined('DC_CONTEXT_ADMIN')) {
self::$init = dcCore::app()->auth->isSuperAdmin();
self::$init = dcCore::app()->auth->isSuperAdmin() && version_compare(phpversion(), My::PHP_MIN, '>=');
}
return self::$init;
@ -41,7 +41,7 @@ class Manage extends dcNsProcess
// set vars used in process and render methods
dcCore::app()->admin->translater = new Translater();
dcCore::app()->admin->type = $_REQUEST['type'] ?? dcCore::app()->admin->translater->get('start_page') ?: '';
dcCore::app()->admin->type = $_REQUEST['type'] ?? dcCore::app()->admin->translater->start_page ?: '';
dcCore::app()->admin->module = $_REQUEST['module'] ?? '';
dcCore::app()->admin->lang = $_REQUEST['lang'] ?? '';
$action = $_POST['action'] ?? '';
@ -223,7 +223,7 @@ class Manage extends dcNsProcess
$modules = dcCore::app()->admin->translater->getModules(dcCore::app()->admin->type);
ksort($modules);
foreach ($modules as $module) {
if (dcCore::app()->admin->translater->get('hide_default') && in_array($module->id, My::defaultDistribModules(dcCore::app()->admin->type))) {
if (dcCore::app()->admin->translater->hide_default && in_array($module->id, My::defaultDistribModules(dcCore::app()->admin->type))) {
continue;
}
if ($module->root_writable) {
@ -390,7 +390,7 @@ class Manage extends dcNsProcess
$backup_code['name'],
$backup_code['code'],
dt::str(
dcCore::app()->blog->settings->get('system')->get('date_format') . ' ' . dcCore::app()->blog->get('settings')->get('system->time_format'),
dcCore::app()->blog->settings->get('system')->get('date_format') . ' ' . dcCore::app()->blog->settings->get('system->time_format'),
(int) $backup_code['time'],
dcCore::app()->blog->settings->get('system')->get('blog_timezone')
),
@ -491,7 +491,7 @@ class Manage extends dcNsProcess
$i = 1;
foreach ($lines as $msgid => $rs) {
$in_dc = ($rs['in_dc'] && dcCore::app()->admin->translater->get('parse_nodc'));
$in_dc = ($rs['in_dc'] && dcCore::app()->admin->translater->parse_nodc);
$t_msgstr = $t_files = $strin = [];
foreach ($rs['o_msgstrs'] as $o_msgstr) {

View File

@ -21,6 +21,9 @@ use dcCore;
*/
class My
{
// required php version
public const PHP_MIN = '8.1';
/**
* This module id
*/
@ -97,44 +100,4 @@ class My
return $types[$type] ?? [];
}
public static function defaultSettings(): array
{
return [
// Show tranlsater button on plugins list
'plugin_menu' => false,
// Show tranlsater button on themes list
'theme_menu' => false,
// Create language backup on save
'backup_auto' => false,
// Backups number limit
'backup_limit' => 20,
// Backup main folder
'backup_folder' => 'module',
// Default ui start page
'start_page' => '-',
// Write .lang.php file (deprecated)
'write_langphp' => false,
// Scan also template files for translations
'scan_tpl' => true,
// Disable translation of know dotclear strings
'parse_nodc' => true,
// Hide official modules
'hide_default' => true,
// Add comment to translations files
'parse_comment' => false,
// Parse user info to translations files
'parse_user' => false,
// User infos to parse
'parse_userinfo' => 'displayname, email',
// Overwrite existing languages on import
'import_overwrite' => false,
// Filename of exported lang
'export_filename' => 'type-module-l10n-timestamp',
// Default service for external proposal tool
'proposal_tool' => 'google',
// Default lang for external proposal tool
'proposal_lang' => 'en',
];
}
}

132
src/Settings.php 100644
View File

@ -0,0 +1,132 @@
<?php
/**
* @brief translater, a plugin for Dotclear 2
*
* @package Dotclear
* @subpackage Plugin
*
* @author Jean-Christian Denis & contributors
*
* @copyright Jean-Christian Denis
* @copyright GPL-2.0 https://www.gnu.org/licenses/gpl-2.0.html
*/
declare(strict_types=1);
namespace Dotclear\Plugin\translater;
use dcCore;
class Settings
{
// Show tranlsater button on plugins list
public readonly bool $plugin_menu;
// Show tranlsater button on themes list
public readonly bool $theme_menu;
// Create language backup on save
public readonly bool $backup_auto;
// Backups number limit
public readonly int $backup_limit;
// Backup main folder
public readonly string $backup_folder;
// Default ui start page
public readonly string $start_page;
// Write .lang.php file (deprecated)
public readonly bool $write_langphp;
// Scan also template files for translations
public readonly bool $scan_tpl;
// Disable translation of know dotclear strings
public readonly bool $parse_nodc;
// Hide official modules
public readonly bool $hide_default;
// Add comment to translations files
public readonly bool $parse_comment;
// Parse user info to translations files
public readonly bool $parse_user;
// User infos to parse
public readonly string $parse_userinfo;
// Overwrite existing languages on import
public readonly bool $import_overwrite;
// Filename of exported lang
public readonly string $export_filename;
// Default service for external proposal tool
public readonly bool $proposal_tool;
// Default lang for external proposal tool
public readonly bool $proposal_lang;
/**
* Constructor set up plugin settings
*/
public function __construct()
{
$s = dcCore::app()->blog->settings->get(My::id());
$this->plugin_menu = (bool) ($s->get('plugin_menu') ?? false);
$this->theme_menu = (bool) ($s->get('theme_menu') ?? false);
$this->backup_auto = (bool) ($s->get('backup_auto') ?? false);
$this->backup_limit = (int) ($s->get('backup_limit') ?? 20);
$this->backup_folder = (string) ($s->get('backup_folder') ?? 'module');
$this->start_page = (string) ($s->get('start_page') ?? '-');
$this->write_langphp = (bool) ($s->get('write_langphp') ?? false);
$this->scan_tpl = (bool) ($s->get('scan_tpl') ?? true);
$this->parse_nodc = (bool) ($s->get('parse_nodc') ?? true);
$this->hide_default = (bool) ($s->get('hide_default') ?? true);
$this->parse_comment = (bool) ($s->get('parse_comment') ?? false);
$this->parse_user = (bool) ($s->get('parse_user') ?? false);
$this->parse_userinfo = (string) ($s->get('parse_userinfo') ?? 'displayname, email');
$this->import_overwrite = (bool) ($s->get('import_overwrite') ?? false);
$this->export_filename = (string) ($s->get('export_filename') ?? 'type-module-l10n-timestamp');
$this->proposal_tool = (bool) ($s->get('proposal_tool') ?? 'google');
$this->proposal_lang = (bool) ($s->get('proposal_lang') ?? 'en');
}
public function getSetting(string $key): mixed
{
return $this->{$key} ?? null;
}
/**
* Overwrite a plugin settings (in db)
*
* @param string $key The setting ID
* @param mixed $value The setting value
*
* @return bool True on success
*/
public function writeSetting(string $key, mixed $value): bool
{
if (property_exists($this, $key) && settype($value, gettype($this->{$key})) === true) {
dcCore::app()->blog->settings->get(My::id())->drop($key);
dcCore::app()->blog->settings->get(My::id())->put($key, $value, gettype($this->{$key}), '', true, true);
return true;
}
return false;
}
/**
* List defined settings keys
*
* @return array The settings keys
*/
public function listSettings(): array
{
return array_keys(get_class_vars(Settings::class));
}
}

View File

@ -24,10 +24,8 @@ use text;
/**
* Translater tools.
*/
class Translater
class Translater extends Settings
{
/** @var array $settings Translater settings */
private $settings = [];
/** @var array $modules List of modules we could work on */
private $modules = [];
@ -38,69 +36,13 @@ class Translater
*/
public function __construct(bool $full = true)
{
$this->loadSettings();
parent::__construct();
if ($full) {
$this->loadModules();
}
}
/// @name settings methods
//@{
/**
* Load settings from db
*/
public function loadSettings(): void
{
foreach (My::defaultSettings() as $key => $value) {
$this->settings[$key] = $value;
$this->set($key, dcCore::app()->blog->settings->get(My::id())->get($key));
}
}
/**
* Write settings to db
*
* @param boolean $overwrite Overwrite existing settings
*/
public function writeSettings($overwrite = true): void
{
foreach (My::defaultSettings() as $key => $value) {
dcCore::app()->blog->settings->get(My::id())->drop($key);
dcCore::app()->blog->settings->get(My::id())->put($key, $this->settings[$key], gettype($value), '', true, true);
}
}
/**
* Read a setting
*
* @param string $key The setting id
*
* @return mixed The setting value
*/
public function get(string $key): mixed
{
return $this->settings[$key] ?? null;
}
/**
* Write (temporary) a setting
*
* @param string $key The setting id
* @param mixed $value The setting value
*/
public function set(string $key, mixed $value): void
{
if (isset($this->settings[$key])) {
try {
settype($value, gettype($this->settings[$key]));
$this->settings[$key] = $value;
} catch (Exception $e) {
}
}
}
//@}
/// @name modules methods
//@{
/**

View File

@ -130,7 +130,7 @@ class TranslaterLang
{
$res = [];
$scan_ext = ['php'];
if ($this->translater->get('scan_tpl')) {
if ($this->translater->scan_tpl) {
$scan_ext[] = 'html';
}

View File

@ -78,7 +78,7 @@ class TranslaterModule
public function getBackupRoot(bool $throw = false)
{
$dir = false;
switch ($this->translater->get('backup_folder')) {
switch ($this->translater->backup_folder) {
case 'module':
if ($this->prop['root_writable']) {
$dir = $this->prop['locales'];
@ -199,7 +199,7 @@ class TranslaterModule
}
if (!empty($res)) {
Translater::isBackupLimit($this->prop['id'], $backup, $this->translater->get('backup_limit'), true);
Translater::isBackupLimit($this->prop['id'], $backup, $this->translater->backup_limit, true);
@set_time_limit(300);
$fp = fopen($backup . '/l10n-' . $this->prop['id'] . '-' . $lang . '-' . time() . '.bck.zip', 'wb');
@ -300,7 +300,7 @@ class TranslaterModule
foreach ($files as $file) {
$f = $this->parseZipFilename($file, true);
if (!$this->translater->get('import_overwrite')
if (!$this->translater->import_overwrite
&& file_exists($this->prop['locales'] . '/' . $f['lang'] . '/' . $f['group'] . $f['ext'])
) {
$not_overwrited[] = implode('-', [$f['lang'], $f['group'], $f['ext']]);
@ -354,7 +354,7 @@ class TranslaterModule
);
}
$filename = files::tidyFileName($this->translater->get('export_filename'));
$filename = files::tidyFileName($this->translater->export_filename);
if (empty($filename)) {
throw new Exception(
__('Export mask is not set in plugin configuration')
@ -396,7 +396,7 @@ class TranslaterModule
$filename = files::tidyFileName(dt::str(str_replace(
['timestamp', 'module', 'type', 'version'],
[time(), $this->prop['id'], $this->prop['type'], $this->prop['version']],
$this->translater->get('export_filename')
$this->translater->export_filename
)));
header('Content-Disposition: attachment;filename=' . $filename . '.zip');
@ -577,7 +577,7 @@ class TranslaterModule
));
}
if ($this->translater->get('backup_auto')) {
if ($this->translater->backup_auto) {
$this->createBackup($lang);
}
@ -672,18 +672,18 @@ class TranslaterModule
$lang = new TranslaterLang($this, $lang);
$content = '';
if ($this->translater->get('parse_comment')) {
if ($this->translater->parse_comment) {
$content .= '# Language: ' . $lang->get('name') . "\n" .
'# Module: ' . $this->get('id') . ' - ' . $this->get('version') . "\n" .
'# Date: ' . dt::str('%Y-%m-%d %H:%M:%S') . "\n";
if ($this->translater->get('parse_user') && $this->translater->get('parse_userinfo') != '') {
if ($this->translater->parse_user && $this->translater->parse_userinfo != '') {
$search = My::defaultUserInformations();
$replace = [];
foreach ($search as $n) {
$replace[] = dcCore::app()->auth->getInfo('user_' . $n);
}
$info = trim(str_replace($search, $replace, $this->translater->get('parse_userinfo')));
$info = trim(str_replace($search, $replace, $this->translater->parse_userinfo));
if (!empty($info)) {
$content .= '# Author: ' . html::escapeHTML($info) . "\n";
}
@ -703,7 +703,7 @@ class TranslaterModule
'"Plural-Forms: nplurals=2; plural=(n > 1);\n"' . "\n\n";
$comments = [];
if ($this->translater->get('parse_comment')) {
if ($this->translater->parse_comment) {
$msgids = $lang->getMsgids();
foreach ($msgids as $msg) {
$comments[$msg['msgid']] = ($comments[$msg['msgid']] ?? '') .
@ -715,7 +715,7 @@ class TranslaterModule
if (empty($msg['msgstr'][0])) {
continue;
}
if ($this->translater->get('parse_comment') && isset($comments[$msg['msgid']])) {
if ($this->translater->parse_comment && isset($comments[$msg['msgid']])) {
$content .= $comments[$msg['msgid']];
}
$content .= 'msgid "' . Translater::poString($msg['msgid'], true) . '"' . "\n";
@ -757,25 +757,25 @@ class TranslaterModule
*/
private function setLangphpContent(string $lang, string $group, array $msgs): void
{
if (!$this->translater->get('write_langphp')) {
if (!$this->translater->write_langphp) {
return;
}
$lang = new TranslaterLang($this, $lang);
$content = '';
if ($this->translater->get('parse_comment')) {
if ($this->translater->parse_comment) {
$content .= '// Language: ' . $lang->get('name') . "\n" .
'// Module: ' . $this->get('id') . ' - ' . $this->get('verison') . "\n" .
'// Date: ' . dt::str('%Y-%m-%d %H:%M:%S') . "\n";
if ($this->translater->get('parse_user') && !empty($this->translater->get('parse_userinfo'))) {
if ($this->translater->parse_user && !empty($this->translater->parse_userinfo)) {
$search = My::defaultUserInformations();
$replace = [];
foreach ($search as $n) {
$replace[] = dcCore::app()->auth->getInfo('user_' . $n);
}
$info = trim(str_replace($search, $replace, $this->translater->get('parse_userinfo')));
$info = trim(str_replace($search, $replace, $this->translater->parse_userinfo));
if (!empty($info)) {
$content .= '// Author: ' . html::escapeHTML($info) . "\n";
}