Add bbcode management 2

This commit is contained in:
Visman 2020-09-30 22:21:44 +07:00
parent 586e7c8197
commit 8a445d6180
6 changed files with 699 additions and 123 deletions

View file

@ -0,0 +1,33 @@
<?php
namespace ForkBB\Models\BBCodeList;
use ForkBB\Models\Method;
use ForkBB\Models\BBCodeList\Structure;
use RuntimeException;
class Insert extends Method
{
/**
* Добавляет bb-код в базу
*/
public function insert(Structure $structure): int
{
if (null !== $structure->getError()) {
throw new RuntimeException('BBCode structure has error');
}
$this->model->reset(); // ????
$vars = [
':tag' => $structure->tag,
':structure' => $structure->toString(),
];
$query = 'INSERT INTO ::bbcode (bb_tag, bb_edit, bb_delete, bb_structure)
VALUES (?s:tag, 1, 1, ?s:structure)';
$this->c->DB->exec($query, $vars);
return (int) $this->c->DB->lastInsertId();
}
}

View file

@ -5,9 +5,15 @@ namespace ForkBB\Models\BBCodeList;
use ForkBB\Core\Container;
use ForkBB\Models\Model as ParentModel;
use RuntimeException;
use Throwable;
class Structure extends ParentModel
{
const TAG_PATTERN = '%^(?:ROOT|[a-z\*][a-z\d-]{0,10})$%D';
const ATTR_PATTERN = '%^[a-z-]{2,15}$%D';
const JSON_OPTIONS = \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE | \JSON_THROW_ON_ERROR;
public function __construct(Container $container)
{
parent::__construct($container);
@ -17,7 +23,7 @@ class Structure extends ParentModel
'text only' => ['text_only'],
'tags only' => ['tags_only'],
'self nesting' => ['self_nesting'],
'attrs' => ['no_attr', 'def_attr'],
'attrs' => ['no_attr', 'def_attr', 'other_attrs'],
];
}
@ -28,7 +34,71 @@ class Structure extends ParentModel
public function toString(): string
{
$a = [
'tag' => $this->tag,
'type' => $this->type,
'parents' => $this->parents,
];
if (! empty($this->handler) && \is_string($this->handler)) {
$a['handler'] = $this->handler;
}
if (! empty($this->text_handler) && \is_string($this->text_handler)) {
$a['text handler'] = $this->text_handler;
}
if (null !== $this->auto) {
$a['auto'] = (bool) $this->auto;
}
if (null !== $this->self_nesting) {
$a['self nesting'] = (int) $this->self_nesting > 0 ? (int) $this->self_nesting : false;
}
if (null !== $this->recursive) {
$a['recursive'] = true;
}
if (null !== $this->text_only) {
$a['text only'] = true;
}
if (null !== $this->tags_only) {
$a['tags only'] = true;
}
if (null !== $this->single) {
$a['single'] = true;
}
if (null !== $this->pre) {
$a['pre'] = true;
}
if (
\is_array($this->new_attr)
&& ! empty($this->new_attr['allowed'])
&& ! empty($this->new_attr['name'])
) {
$this->setBBAttr($this->new_attr['name'], $this->new_attr, ['required', 'format', 'body format', 'text only']);
}
$a['attrs'] = $this->other_attrs;
if (null !== $this->no_attr) {
$a['attrs']['no attr'] = $this->no_attr;
}
if (null !== $this->def_attr) {
$a['attrs']['Def'] = $this->def_attr;
}
if (empty($a['attrs'])) {
unset($a['attrs']);
}
return \json_encode($a, self::JSON_OPTIONS);
}
protected function gettype(): string
@ -131,8 +201,14 @@ class Structure extends ParentModel
$this->setAttr('self nesting', $value);
}
protected function getBBAttr(/* mixed */ $data, array $fields) /* : mixed */
protected function getBBAttr(string $name, array $fields) /* : mixed */
{
if (empty($this->attrs[$name])) {
return null;
}
$data = $this->attrs[$name];
if (true === $data) {
return true;
} elseif (! \is_array($data)) {
@ -148,7 +224,7 @@ class Structure extends ParentModel
break;
case 'required':
case 'text only':
$value = isset($data[$field]) && true === $data[$field] ? true : null;
$value = isset($data[$field]) ? true : null;
break;
default:
throw new RuntimeException('Unknown attribute property');
@ -162,74 +238,291 @@ class Structure extends ParentModel
}
}
protected function setBBAttr(/* mixed */ $data, array $fields) /* : mixed */
protected function setBBAttr(string $name, /* mixed */ $data, array $fields): void
{
$attrs = $this->getAttr('attrs');
if (
empty($data['allowed'])
|| $data['allowed'] < 1
) {
return null;
}
unset($attrs[$name]);
} else {
$result = [];
foreach ($fields as $field) {
$key = \str_replace(' ', '_', $field);
$result = [];
foreach ($fields as $field) {
switch ($field) {
case 'format':
case 'body format':
$value = isset($data[$field]) && \is_string($data[$field]) ? $data[$field] : null;
break;
case 'required':
case 'text only':
$value = isset($data[$field]) && true === $data[$field] ? true : null;
break;
default:
throw new RuntimeException('Unknown attribute property');
switch ($field) {
case 'format':
case 'body format':
$value = ! empty($data[$key]) && \is_string($data[$key]) ? $data[$key] : null;
break;
case 'required':
case 'text only':
$value = ! empty($data[$key]) ? true : null;
break;
default:
throw new RuntimeException('Unknown attribute property');
}
if (isset($value)) {
$result[$field] = $value;
}
}
if (isset($value)) {
$key = \str_replace(' ', '_', $field);
$result[$key] = $value;
}
$attrs[$name] = empty($result) ? true : $result;
}
return empty($result) ? true : $result;
$this->setAttr('attrs', $attrs);
}
protected function getno_attr() /* mixed */
protected function getno_attr() /* : mixed */
{
return $this->getBBAttr($this->attrs['no attr'] ?? null, ['body format', 'text only']);
return $this->getBBAttr('no attr', ['body format', 'text only']);
}
protected function setno_attr(array $value): void
{
$value = $this->getBBAttr($value, ['body format', 'text only']);
$attrs = $this->getAttr('attrs');
if (null === $value) {
unset($attrs['no attr']);
} else {
$attrs['no attr'] = $value;
}
$this->setAttr('attrs', $attrs);
$this->setBBAttr('no attr', $value, ['body format', 'text only']);
}
protected function getdef_attr() /* mixed */
protected function getdef_attr() /* : mixed */
{
return $this->getBBAttr($this->attrs['Def'] ?? null, ['required', 'format', 'body format', 'text only']);
return $this->getBBAttr('Def', ['required', 'format', 'body format', 'text only']);
}
protected function setdef_attr(array $value): void
{
$value = $this->getBBAttr($value, ['required', 'format', 'body format', 'text only']);
$this->setBBAttr('Def', $value, ['required', 'format', 'body format', 'text only']);
}
protected function getother_attrs(): array
{
$attrs = $this->getAttr('attrs');
if (null === $value) {
unset($attrs['Def']);
} else {
$attrs['Def'] = $value;
if (! \is_array($attrs)) {
return [];
}
$this->setAttr('attrs', $attrs);
unset($attrs['no attr'], $attrs['Def'], $attrs['New']);
$result = [];
foreach ($attrs as $name => $attr) {
$value = $this->getBBAttr($name, ['required', 'format', 'body format', 'text only']);
if (null === $value) {
continue;
}
$result[$name] = $value;
}
return $result;
}
protected function setother_attrs(array $attrs): void
{
unset($attrs['no attr'], $attrs['Def']);
foreach ($attrs as $name => $attr) {
$this->setBBAttr($name, $attr, ['required', 'format', 'body format', 'text only']);
}
}
/**
* Ищет ошибку в структуре bb-кода
*/
public function getError(): ?array
{
if (
! \is_string($this->tag)
|| ! \preg_match(self::TAG_PATTERN, $this->tag)
) {
return ['Tag name not specified'];
}
$result = $this->testPHP($this->handler);
if (null !== $result) {
return ['PHP code error in Handler: %s', $result];
}
$result = $this->testPHP($this->text_handler);
if (null !== $result ) {
return ['PHP code error in Text handler: %s', $result];
}
if (
null !== $this->recursive
&& null !== $this->tags_only
) {
return ['Recursive and Tags only are enabled at the same time'];
}
if (
null !== $this->recursive
&& null !== $this->single
) {
return ['Recursive and Single are enabled at the same time'];
}
if (
null !== $this->text_only
&& null !== $this->tags_only
) {
return ['Text only and Tags only are enabled at the same time'];
}
if (\is_array($this->attrs)) {
foreach ($this->attrs as $name => $attr) {
if (
'no attr' !== $name
&& 'Def' !== $name
&& ! preg_match(self::ATTR_PATTERN, $name)
) {
return ['Attribute name %s is not valid', $name];
}
if (isset($attr['format'])) {
if (
! \is_string($attr['format'])
|| false === @\preg_match($attr['format'], 'abcdef')
) {
return ['Attribute %1$s, %2$s - regular expression error', $name, 'Format'];
}
}
if (isset($attr['body format'])) {
if (
! \is_string($attr['body format'])
|| false === @\preg_match($attr['body format'], 'abcdef')
) {
return ['Attribute %1$s, %2$s - regular expression error', $name, 'Body format'];
}
}
}
}
if (
\is_array($this->new_attr)
&& ! empty($this->new_attr['allowed'])
&& ! empty($this->new_attr['name'])
) {
$name = $this->new_attr['name'];
if (
'no attr' === $name
|| 'Def' === $name
|| isset($this->attrs[$name])
|| ! preg_match(self::ATTR_PATTERN, $name)
) {
return ['Attribute name %s is not valid', $name];
}
if (isset($this->new_attr['format'])) {
if (
! \is_string($this->new_attr['format'])
|| false === @\preg_match($this->new_attr['format'], 'abcdef')
) {
return ['Attribute %1$s, %2$s - regular expression error', $name, 'Format'];
}
}
if (isset($this->new_attr['body format'])) {
if (
! \is_string($this->new_attr['body format'])
|| false === @\preg_match($this->new_attr['body format'], 'abcdef')
) {
return ['Attribute %1$s, %2$s - regular expression error', $name, 'Body format'];
}
}
}
return null;
}
protected function testPHP(?string $code): ?string
{
if (
null === $code
|| '' === $code
) {
return null;
}
// тест на парность скобок
$testCode = \preg_replace('%//[^\r\n]*+|#[^\r\n]*+|/\*.*?\*/|\'.*?(?<!\\\\)\'|".*?(?<!\\\\)"%s', '', $code);
if (false === \preg_match_all('%[(){}\[\]]%s', $testCode, $matches)) {
throw new RuntimeException('The preg_match_all() returned an error');
}
$round = 0;
$square = 0;
$curly = 0;
foreach ($matches[0] as $value) {
switch ($value) {
case '(':
++$round;
break;
case ')':
--$round;
if ($round < 0) {
return '\')\' > \'(\'.';
}
break;
case '[':
++$square;
break;
case ']':
--$square;
if ($square < 0) {
return '\']\' > \'[\'.';
}
break;
case '{':
++$curly;
break;
case '}':
--$curly;
if ($curly < 0) {
return '\'}\' > \'{\'.';
}
break;
default:
throw new RuntimeException('Unknown bracket type');
}
}
if (0 !== $round) {
return '\'(\' != \')\'.';
}
if (0 !== $square) {
return '\'[\' != \']\'.';
}
if (0 !== $curly) {
return '\'{\' != \'}\'.';
}
// тест на выполнение DANGER! DANGER! DANGER! O_o
$testCode = "\$testVar = function(\$body, \$attrs, \$parser) { {$code} };\nreturn true;";
try {
$result = @eval($testCode);
if (true !== $result) {
$error = error_get_last();
$message = $error['message'] ?? 'Unknown error';
$line = $error['line'] ?? '';
return "{$message}: [$line]";
}
} catch (Throwable $e) {
return "{$e->getMessage()}: [{$e->getLine()}]";
}
return null;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace ForkBB\Models\BBCodeList;
use ForkBB\Models\Method;
use ForkBB\Models\BBCodeList\Model as BBCodeList;
use ForkBB\Models\BBCodeList\Structure;
use RuntimeException;
class Update extends Method
{
/**
* Обновляет структуру bb-кода
*/
public function update(int $id, Structure $structure): BBCodeList
{
if (null !== $structure->getError()) {
throw new RuntimeException('BBCode structure has error');
}
$vars = [
':id' => $id,
':tag' => $structure->tag,
':structure' => $structure->toString(),
];
$query = 'UPDATE ::bbcode
SET bb_structure=?s:structure
WHERE id=?i:id AND bb_tag=?s:tag AND bb_edit=1';
$this->c->DB->exec($query, $vars);
return $this->model->reset();
}
}

View file

@ -246,19 +246,79 @@ class BBCode extends Parser
}
$this->bbTypes = $bbTypes;
if ('POST' === $method) {
}
if ($id > 0) {
$title = __('Edit bbcode head');
$this->formAction = $this->c->Router->link('AdminBBCodeEdit', ['id' => $id]);
$this->formToken = $this->c->Csrf->create('AdminBBCodeEdit', ['id' => $id]);
$page = 'AdminBBCodeEdit';
$pageArgs = ['id' => $id];
} else {
$title = __('Add bbcode head');
$this->formAction = $this->c->Router->link('AdminBBCodeNew');
$this->formToken = $this->c->Csrf->create('AdminBBCodeNew');
$page = 'AdminBBCodeNew';
$pageArgs = [];
}
$this->formAction = $this->c->Router->link($page, $pageArgs);
$this->formToken = $this->c->Csrf->create($page, $pageArgs);
if ('POST' === $method) {
$v = $this->c->Validator->reset()
->addValidators([
'check_all' => [$this, 'vCheckAll'],
])->addRules([
'token' => 'token:' . $page,
'tag' => $id > 0 ? 'absent' : 'required|string:trim|regex:%^[a-z\*][a-z\d-]{0,10}$%',
'type' => 'required|string|in:' . \implode(',', $bbTypes),
'type_new' => 'string:trim|regex:%^[a-z][a-z\d-]{0,19}$%',
'parents.*' => 'required|string|in:' . \implode(',', $bbTypes),
'handler' => 'string:trim|max:65535',
'text_handler' => 'string:trim|max:65535',
'recursive' => 'required|integer|in:0,1',
'text_only' => 'required|integer|in:0,1',
'tags_only' => 'required|integer|in:0,1',
'pre' => 'required|integer|in:0,1',
'single' => 'required|integer|in:0,1',
'auto' => 'required|integer|in:0,1',
'self_nesting' => 'required|integer|min:0|max:10',
'no_attr.allowed' => 'required|integer|in:0,1',
'no_attr.body_format' => 'string:trim|max:1024',
'no_attr.text_only' => 'required|integer|in:0,1',
'def_attr.allowed' => 'required|integer|in:0,1',
'def_attr.required' => 'required|integer|in:0,1',
'def_attr.format' => 'string:trim|max:1024',
'def_attr.body_format' => 'string:trim|max:1024',
'def_attr.text_only' => 'required|integer|in:0,1',
'other_attrs.*.allowed' => 'required|integer|in:0,1',
'other_attrs.*.required' => 'required|integer|in:0,1',
'other_attrs.*.format' => 'string:trim|max:1024',
'other_attrs.*.body_format' => 'string:trim|max:1024',
'other_attrs.*.text_only' => 'required|integer|in:0,1',
'new_attr.name' => 'string:trim|regex:%^(?:\|[a-z-]{2,15})$%',
'new_attr.allowed' => 'required|integer|in:0,1',
'new_attr.required' => 'required|integer|in:0,1',
'new_attr.format' => 'string:trim|max:1024',
'new_attr.body_format' => 'string:trim|max:1024',
'new_attr.text_only' => 'required|integer|in:0,1',
'save' => 'check_all',
])->addAliases([
])->addArguments([
'token' => $pageArgs,
'save' => $structure,
])->addMessages([
]);
if ($v->validation($_POST)) {
if ($id > 0) {
$this->c->bbcode->update($id, $structure);
$message = 'BBCode updated redirect';
} else {
$id = $this->c->bbcode->insert($structure);
$message = 'BBCode added redirect';
}
return $this->c->Redirect->page('AdminBBCodeEdit', ['id' => $id])->message($message);
}
$this->fIswev = $v->getErrors();
}
$this->aCrumbs[] = [
$this->formAction,
$title,
@ -278,6 +338,38 @@ class BBCode extends Parser
return $this;
}
/**
* Проверяет данные bb-кода
*/
public function vCheckAll(Validator $v, string $txt, $attrs, Structure $structure): string
{
if (! empty($v->getErrors())) {
return $txt;
}
$data = $v->getData();
unset($data['token'], $data['save']);
foreach ($data as $key => $value) {
if ('type_new' === $key) {
if (isset($value[0])) {
$structure->type = $value;
}
} else {
$structure->{$key} = $value;
}
}
$error = $structure->getError();
if (\is_array($error)) {
$v->addError(__(...$error));
}
return $txt;
}
/**
* Формирует данные для формы
*/
@ -298,9 +390,9 @@ class BBCode extends Parser
],
];
$yn = [1 => __('Yes'), 0 => __('No')];
$yn = [1 => __('Yes'), 0 => __('No')];
$form['sets']["structure"] = [
$form['sets']['structure'] = [
'class' => 'structure',
// 'legend' => ,
'fields' => [
@ -406,78 +498,106 @@ class BBCode extends Parser
$tagStr = $id > 0 ? $structure->tag : 'TAG';
$form['sets']["no_attr"] = [
'class' => ['attr', 'no_attr'],
'legend' => __('No attr subhead', $tagStr),
'fields' => [
'no_attr[allowed]' => [
'type' => 'radio',
'value' => null === $structure->no_attr ? 0 : 1,
'values' => $yn,
'caption' => __('Allowed label'),
'info' => __('Allowed no_attr info'),
],
'no_attr[body_format]' => [
'class' => 'format',
'type' => 'text',
'value' => $structure->no_attr['body_format'] ?? '',
'caption' => __('Body format label'),
'info' => __('Body format info'),
],
'no_attr[text_only]' => [
'type' => 'radio',
'value' => empty($structure->no_attr['text_only']) ? 0 : 1,
'values' => $yn,
'caption' => __('Text only label'),
'info' => __('Text only info'),
],
],
];
$form['sets']['no_attr'] = $this->formEditSub(
$structure->no_attr,
'no_attr',
'no_attr',
__('No attr subhead', $tagStr),
__('Allowed no_attr info')
);
$form['sets']["def_attr"] = [
'class' => ['attr', 'def_attr'],
'legend' => __('Def attr subhead', $tagStr),
'fields' => [
'def_attr[allowed]' => [
'type' => 'radio',
'value' => null === $structure->def_attr ? 0 : 1,
'values' => $yn,
'caption' => __('Allowed label'),
'info' => __('Allowed def_attr info'),
],
'def_attr[required]' => [
'type' => 'radio',
'value' => empty($structure->def_attr['required']) ? 0 : 1,
'values' => $yn,
'caption' => __('Required label'),
'info' => __('Required info'),
],
'def_attr[format]' => [
'class' => 'format',
'type' => 'text',
'value' => $structure->def_attr['format'] ?? '',
'caption' => __('Format label'),
'info' => __('Format info'),
],
'def_attr[body_format]' => [
'class' => 'format',
'type' => 'text',
'value' => $structure->def_attr['body_format'] ?? '',
'caption' => __('Body format label'),
'info' => __('Body format info'),
],
'def_attr[text_only]' => [
'type' => 'radio',
'value' => empty($structure->def_attr['text_only']) ? 0 : 1,
'values' => $yn,
'caption' => __('Text only label'),
'info' => __('Text only info'),
],
],
];
$form['sets']['def_attr'] = $this->formEditSub(
$structure->def_attr,
'def_attr',
'def_attr',
__('Def attr subhead', $tagStr),
__('Allowed def_attr info')
);
$fields = [];
foreach ($structure->other_attrs as $name => $attr) {
$form['sets']["{$name}_attr"] = $this->formEditSub(
$attr,
$name,
"{$name}_attr",
__('Other attr subhead', $tagStr, $name),
__('Allowed %s attr info', $name)
);
}
$form['sets']['new_attr'] = $this->formEditSub(
$structure->new_attr,
'new_attr',
'new_attr',
__('New attr subhead'),
__('Allowed new_attr info')
);
return $form;
}
/**
* Формирует данные для формы
*/
protected function formEditSub(/* mixed */ $data, string $name, string $class, string $legend, string $info): array
{
$yn = [1 => __('Yes'), 0 => __('No')];
$fields = [];
$other = '_attr' !== \substr($name, -5);
$key = $other ? "other_attrs[{$name}]" : $name;
if ('new_attr' === $name) {
$fields["{$key}[name]"] = [
'type' => 'text',
'value' => $data['name'] ?? '',
'caption' => __('Attribute name label'),
'info' => __('Attribute name info'),
'maxlength' => 15,
'pattern' => '^[a-z-]{2,15}$',
];
}
$fields["{$key}[allowed]"] = [
'type' => 'radio',
'value' => null === $data ? 0 : 1,
'values' => $yn,
'caption' => __('Allowed label'),
'info' => $info,
];
if ('no_attr' !== $name) {
$fields["{$key}[required]"] = [
'type' => 'radio',
'value' => empty($data['required']) ? 0 : 1,
'values' => $yn,
'caption' => __('Required label'),
'info' => __('Required info'),
];
$fields["{$key}[format]"] = [
'class' => 'format',
'type' => 'text',
'value' => $data['format'] ?? '',
'caption' => __('Format label'),
'info' => __('Format info'),
];
}
$fields["{$key}[body_format]"] = [
'class' => 'format',
'type' => 'text',
'value' => $data['body_format'] ?? '',
'caption' => __('Body format label'),
'info' => __('Body format info'),
];
$fields["{$key}[text_only]"] = [
'type' => 'radio',
'value' => empty($data['text_only']) ? 0 : 1,
'values' => $yn,
'caption' => __('Text only label'),
'info' => __('Text only info'),
];
return [
'class' => ['attr', $class],
'legend' => $legend,
'fields' => $fields,
];
}
}

View file

@ -269,3 +269,51 @@ msgstr "Format"
msgid "Format info"
msgstr "The regular expression, sets the rule for this attribute."
msgid "Other attr subhead"
msgstr "Settings for BBCode type [%1$s %2$s=<i>value</i>]..."
msgid "Allowed %s attr info"
msgstr "Allow use of this BBCode with <b><code>%s</code></b> attribute. No - this attribute will be deleted."
msgid "New attr subhead"
msgstr "New attribute"
msgid "Allowed new_attr info"
msgstr "Add new attribute"
msgid "Attribute name label"
msgstr "New attribute name"
msgid "Attribute name info"
msgstr "Format: <code>^[a-z-]{2,15}$</code>"
msgid "Tag name not specified"
msgstr "Tag name not specified."
msgid "PHP code error in Handler: %s"
msgstr "PHP code error in Handler: %s"
msgid "PHP code error in Text handler: %s"
msgstr "PHP code error in Text handler: %s"
msgid "Recursive and Tags only are enabled at the same time"
msgstr "Recursive and Tags only are enabled at the same time."
msgid "Recursive and Single are enabled at the same time"
msgstr "Recursive and Single are enabled at the same time."
msgid "Text only and Tags only are enabled at the same time"
msgstr "Text only and Tags only are enabled at the same time."
msgid "Attribute %1$s, %2$s - regular expression error"
msgstr "Attribute %1$s, field %2$s - regular expression error."
msgid "Attribute name %s is not valid"
msgstr "Attribute name %s is not valid."
msgid "BBCode updated redirect"
msgstr "BBCode updated."
msgid "BBCode added redirect"
msgstr "BBCode added."

View file

@ -269,3 +269,51 @@ msgstr "Format"
msgid "Format info"
msgstr "Регулярное выражение, задает правило для атрибута."
msgid "Other attr subhead"
msgstr "Настройки для BB-кода вида [%1$s %2$s=<i>значение</i>]..."
msgid "Allowed %s attr info"
msgstr "Разрешить использовать этот BB-код с атрибутом <b><code>%s</code></b>. Нет - атрибут буден удален."
msgid "New attr subhead"
msgstr "Новый атрибут"
msgid "Allowed new_attr info"
msgstr "Добавить новый атрибут"
msgid "Attribute name label"
msgstr "Имя нового атрибута"
msgid "Attribute name info"
msgstr "Формат: <code>^[a-z-]{2,15}$</code>"
msgid "Tag name not specified"
msgstr "Имя тега не указано."
msgid "PHP code error in Handler: %s"
msgstr "Ошибка PHP кода в Handler: %s"
msgid "PHP code error in Text handler: %s"
msgstr "Ошибка PHP кода в Text handler: %s"
msgid "Recursive and Tags only are enabled at the same time"
msgstr "Одновременно включены Recursive и Tags only."
msgid "Recursive and Single are enabled at the same time"
msgstr "Одновременно включены Recursive и Single."
msgid "Text only and Tags only are enabled at the same time"
msgstr "Одновременно включены Text only и Tags only."
msgid "Attribute %1$s, %2$s - regular expression error"
msgstr "Атрибут %1$s, поле %2$s - ошибка в регулярном выражении."
msgid "Attribute name %s is not valid"
msgstr "Имя %s для атрибута недопустимо."
msgid "BBCode updated redirect"
msgstr "BB-код обновлен."
msgid "BBCode added redirect"
msgstr "BB-код добавлен."