Как сделать иерархическое меню?
Нередко возникает задача формирования меню, обладающего иерархической структурой. Другими словами, у пунктов такого меню могут быть подпункты (практически каждый раз это приходится делать, например, при разработке веб-сайтов). При этом часто некоторый элемент является "активным" и меню должно быть соответствующим образом "раскрыто".
В настоящей статье предлагается решение этой проблемы средствами языка PHP, а также альтернативное решение с использованием процедур MySQL.
Статья эта носит, скорее, академический характер, чем практический: наверняка читатель не единожды решал эту задачу самостоятельно и уже располагает нужным кодом. Тем не менее, в статье могут встретиться интересные моменты, и чтение будет ненапрасным. Статья также может быть полезна начинающим программистам.
Структура данных.
Положим, что у каждого элемента иерархической структуры должны быть как минимум два параметра: уникальный идентификатор, который позволяет однозначно определить данный элемент, и указатель на родительский элемент.
В реальной жизни у элементов меню обычно есть и другие свойства, такие как текст (собственно, название элемента) и др. Вышесказанное может быть воплощено в виде таблицы:
| id | parentid | sort | text |
+----+----------+------+--------------+
| 1 | 0 | 0 | Главная |
| 2 | 0 | 0 | Продукция |
| 3 | 0 | -100 | Контакты |
| 4 | 0 | -200 | О нас |
| 5 | 2 | 20 | Балки |
| 6 | 2 | 50 | Швеллеры |
| 7 | 2 | 0 | Черепица |
| 9 | 6 | 0 | 16 П |
| 10 | 6 | 0 | 16 У |
| 11 | 6 | 0 | 18 П |
| 12 | 6 | 0 | 10 П |
| 13 | 6 | 0 | 12 П |
| 14 | 6 | 0 | 14 П |
| 15 | 5 | 0 | 12 б |
| 16 | 5 | 0 | 30 Б1 |
| 17 | 5 | 0 | 35 б1 |
+----+----------+------+--------------+
Хранить такую таблицу удобно в какой-нибудь СУБД, например, MySQL (которая и будет использоваться далее), где структура данных приводимой таблицы описывается так:
id INT unsigned NOT NULL AUTO_INCREMENT,
parentid INT UNSIGNED NOT NULL DEFAULT 0,
sort INT NOT NULL DEFAULT 0,
text VARCHAR(255) DEFAULT NULL,
PRIMARY KEY (id),
KEY parentid (parentid)
)
При этом хотелось разработать такой код, чтобы не было зависимости от имени таблицы, в которой хранится меню (это важно еще и потому, что меню может быть несколько — не писать же под каждую из них свои функции).
Несколько слов о сортировке.
Возможно, вызывает вопросы поле sort. Нужно оно вот для чего: часто бывает необходимо жёстко контролировать порядок отображения элементов меню одного уровня, и при этом сортировка ни по алфавиту, ни по id не подходит (например, хочется, чтобы элемент "Швеллеры" всегда показывался над элементом "Балки"). Для этого и вводится дополнительный параметр, по которому сортируют.
Следует отметить, что в конечном итоге sort повлияет лишь на относительное расположение тех элементов, у которых один и тот же родитель (parentid). То есть, элемент "Балки" никогда не окажется выше элемента "Продукция" или "Главная", хотя sort у него больше.
Код для PHP
Для формирования готовых для работы данных меню требуется вызывать функцию getmenu(), которая формирует массив вида
(
[1] => Array
(
[id] => 1
[text] => Главная
[level] => 1
[active] =>
)
[2] => Array
(
[id] => 2
[text] => Продукция
[level] => 1
[active] => 1
)
[6] => Array
(
[id] => 6
[text] => Швеллеры
[level] => 2
[active] =>
)
...
)
Параметр active показывает, является ли данный пункт меню активным; это бывает нужно, например, чтобы подсветить его жирным, кроме того, в меню веб-сайтов активный пункт, в отличие от всех остальных, обычно не является ссылкой. Из такого массива легко сделать HTML-код:
function drawmenu($menudata) {
// упрощенный вариант без URL для пунктов меню
$html = '';
foreach ($menudata as $a) {
extract($a);
$html .= '<div style="padding-left: '.(($level-1)*25).'px;">';
if ($active) $html.= "<strong>$text</strong>";
else $html .= $text;
$html .= '</div>';
}
return $html;
}
// соединяемся с базой данных и т.п.
// пусть меню хранится в таблице mainmenu
$menutable = 'mainmenu',
$menuid = 11;
echo drawmenu(getmenu($menutable, $menuid));
?>
Получится так:
Необходимый код представлен ниже:
function getmenu($tablename, $activeid) {
$parents = menuparents($tablename);
$chain = makechain($parents, $activeid);
$parentid = 0; $level = 1;
$list = build_hierarchy($parentid, $level, $chain, $parents);
$menudata = menudata($tablename, $list, $activeid);
return $menudata;
}
function menuparents($tablename) {
$parents = array();
$sql = "SELECT id, parentid FROM $tablename ORDER BY sort DESC";
$result = mysql_query($sql) OR die (
"<p><strong>Error at line ".__LINE__."</strong>:<br/>"
.mysql_error().
"<br/>
<strong>SQL was</strong>: $sql</p>"
);
while ($row = mysql_fetch_assoc($result)) {
$parents[$row{'id'}] = $row['parentid'];
}
return $parents;
}
function makechain($hierachy, $activeid) {
$chain = array();
if (!isset($hierachy[$activeid])) {
return array();
}
$current = $activeid;
do {
$chain[] = $current;
$current = $hierachy[$current];
}
while ($current);
return $chain;
}
function build_hierarchy($parentid, $level, $chain, $hierarchy) {
$list = array();
$brothers = array_keys($hierarchy, $parentid);
foreach ($brothers as $value) {
$list[$value] = $level;
if (in_array($value, $chain)) {
$children = build_hierarchy($value, $level+1, $chain, $hierarchy);
$list = $list + $children;
}
}
return $list;
}
function menudata($tablename, $list, $activeid) {
$ids = array_keys($list); // вспомним, что id элементов хранятся в ключах массива
$in = implode(',', $ids);
$sql = "SELECT id, text FROM $tablename WHERE id IN ($in)";
$result = mysql_query($sql) OR die (
"<p><b>Error at line ".__LINE__."</b>: "
.mysql_error().
"<br/>
<b>SQL was</b>: $sql</p>"
);
$tempdata = array();
while ($row = mysql_fetch_assoc($result)) {
$id = $row['id'];
$level = $list[$id];
$active = ($id == $activeid) ? TRUE : FALSE;
$a = $row + compact('level', 'active');
$tempdata[$row{'id'}] = $a;
}
foreach ($list as $key => $value) {
$data[$key] = $tempdata[$key];
}
return $data;
}
?>
Далее предложенный код подробно разбирается. Те, кого разбор кода не интересует, могут перейти к описанию реализации на MySQL или — если есть желание оставить комментарий — к концу статьи.
/*
getmenu() является интерфейсом для работы с предлагаемым пакетом функций (т.е., чтобы всё заработало, нужно вызывать именно её).
Функция принимает два аргумента:
1. имя таблицы в БД, в которой хранится информация о пунктах меню
2. id "активного" элемента меню; если активных элементов меню нет, следует передавать любое число, не совпадающее ни с одним id из имеющихся (например, 0)
*/
$parents = menuparents($tablename);
$chain = makechain($parents, $activeid);
$parentid = 0; $level = 1;
$list = build_hierarchy($parentid, $level, $chain, $parents);
$menudata = menudata($tablename, $list, $activeid);
return $menudata;
}
function menuparents($tablename) {
/*
menuparents() формирует массив, хранящий информацию об иерархии элементов меню.
Массив одномерный, каждому пункту меню соответствует один элемент массива, ключом при этом будет id пункта меню, значением - id родительского пункта меню (для корневых пунктов это будет 0).
Для работы функции требуется имя таблицы в БД, где хранится меню.
*/
$parents = array();
$sql = "SELECT id, parentid FROM $tablename ORDER BY sort DESC";
/*
SQL-инъекций давайте в этом месте бояться не будем, т.к. аргумент с именем таблицы должен формироваться программистом (а не пользователями и т.п.)
В случае, если какие-то опасности все-таки есть, следует обработать их до передачи аргументов функции getmenu().
*/
$result = mysql_query($sql) OR die (
"<p><strong>Error at line ".__LINE__."</strong>:<br/>"
.mysql_error().
"<br/>
<strong>SQL was</strong>: $sql</p>"
);
while ($row = mysql_fetch_assoc($result)) {
$parents[$row{'id'}] = $row['parentid'];
}
return $parents;
}
function makechain($hierachy, $activeid) {
/*
Меню может быть развёрнуто: если есть некий "активный" элемент, то должны также отображаться все его дочерние, если они имеются. Также раскрыт должен быть любой элемент, если его дочерний элемент является "активным", причем родство может быть как прямое, так и через несколько уровней (например, когда активен дочерний элемент дочернего элемента и т.д).
Задача этой функции - составить список элементов меню, которые должны быть раскрыты (т.е. чьи дочерние элементы нужно показать).
*/
$chain = array();
if (!isset($hierachy[$activeid])) {
/*
Так проверяется существование элемента меню с данным id.
Если в массиве $hierachy нет соответствующего элемента, значит, элемента меню с таким id нет вообще.
В этом случае логично сразу завершить работу функции, вернув пустой массив.
*/
return array();
}
// если же элемент найден в $parents - можно начинать построение цепочки
$current = $activeid; // цепочка начинается с активного элемента
do {
/*
Следует отметить, что, поскольку id каждого элемента уникален, нет нужды указывать у каждого из членов цепочки его уровень или же строить цепочку в каком-то специальном порядке - важно лишь наличие элемента в возвращаемом списке
*/
$chain[] = $current;
$current = $hierachy[$current]; // переходим на уровень выше - к "родителю" текущего элемента
}
// если оказалось, что id элемента равен 0 (отсюда условие ($current == FALSE) верно) - значит, мы добрались до самого верхнего уровня, цепочка построена и выполнение цикла нужно прекратить
while ($current);
return $chain;
}
function build_hierarchy($parentid, $level, $chain, $hierarchy) {
/*
Эта функция выполняет ключевую роль в построении правильно упорядоченного списка элементов меню. Фактически требуется просто список id элементов (поскольку id элемента однозначно его идентифицирует). В этот список должны войти только отображаемые элементы, причем список должен быть сформирован именно в том порядке, в котором элементы будут следовать в меню.
Наиболее удобным оказалось построить функцию, отталкиваясь от id родительского элемента (который передается первым аргументом) - в меню в любом случае отображаются все пункты самого верхнего уровня (parentid = 0), следовательно, нужно их все включить в список, а далее просто для каждого элемента проверить, не раскрыт ли он (это проверяется по наличию текущего элемента в цепочке раскрытых, которая передается третьим аргументом - $chain). В случае, если раскрыт, настоящая функция просто вызывается так, что текущий элемент выступает уже в качестве родительского, и так далее, пока цепочка раскрытых не закончится.
Бывает нужен также уровень элемента, поэтому функция построена с учетом такой необходимости и возвращает массив, ключи которого принимают значения id элементов меню (id уникальны, поэтому никаких конфликтов внутри массива не будет), а значения - уровень этих элементов. Для расчета уровня служит второй аргумент функции ($level); хотелось бы отметить, однако, что он несет лишь прикладную функцию и для построения меню без информации об уровнях не требуется. Вообще уровень можно было бы вычислять и отдельно - считать количество элементов в цепочке, возвращаемой функцией chain(), но это потребовало бы дополнительных действий, основная часть которых итак выполняется в настоящей функции.
Массив $hierarchy, передающийся в функцию четвертым аргументом, является продуктом вышеописанной функции menuparents() и требуется для получения дочерних элементов по id родительского.
*/
$list = array();
$brothers = array_keys($hierarchy, $parentid);
foreach ($brothers as $value) {
$list[$value] = $level;
/*
Если элемент есть среди раскрытых, значит, нужно получить его дочерние элементы
причем эти дочерние элементы должны быть добавлены перед тем, как переходить к следующим, поскольку в меню именно в таком порядке они и должны быть:
*/
if (in_array($value, $chain)) {
$children = build_hierarchy($value, $level+1, $chain, $hierarchy);
$list = $list + $children;
}
}
return $list;
}
function menudata($tablename, $list, $activeid) {
/*
menudata() получает данные о нужных элементах меню, возвращая готовый отсортированный в нужном порядке массив со всей информацией.
Устроена функция несколько более сложно, чем могла бы быть: обычно в таких случаях из БД запрашивают информацию обо всех пунктах меню и потом выбирают нужные. Однако хочется все же выбрать только те данные, которые необходимы - зачем вытаскивать всю таблицу меню, если нужны сведения только о некоторых из элементов (хотя это вряд ли скажется на производительности, поскольку обычно таблицы с меню очень маленькие). Поэтому функции нужно передать список элементов, которые будут показаны в меню; делается это с помощью аргумента $list - результата работы функции build_hierarchy().
*/
$ids = array_keys($list); // вспомним, что id элементов хранятся в ключах массива
$in = implode(',', $ids);
$sql = "SELECT id, text FROM $tablename WHERE id IN ($in)";
$result = mysql_query($sql) OR die (
"<p><strong>Error at line ".__LINE__."</strong>:<br/>"
.mysql_error().
"<br/>
<strong>SQL was</strong>: $sql</p>"
);
$tempdata = array();
// сначала выберем данные в двумерный массив с ключами, равными id соотв. элементов
// нужно помнить, что в результате запроса хотя и содержатся только нужные элементы, отсортированы они там неизвестно как
while ($row = mysql_fetch_assoc($result)) {
$id = $row['id'];
$level = $list[$id];
$active = ($id == $activeid) ? TRUE : FALSE;
$a = $row + compact('level', 'active');
$tempdata[$row{'id'}] = $a;
}
// а теперь воспользуемся тем обстоятельством, что в массиве из функции build_hierarchy() элементы отсортированы именно в том порядке, в котором их следует выводить
// сохранить этот порядок в результирующем массиве легко:
foreach ($list as $key => $value) {
$data[$key] = $tempdata[$key];
}
return $data;
}
Код для MySQL
Реализация средствами MySQL приводится больше для сравнения с остальными способами, чем для реальной жизни: как правило, логику построения меню целесообразно переносить на клиент. Реализовывать на SQL менее удобно, чем на многих других современных языках. В MySQL, например, нет массивов и нельзя обращаться к идентификаторам (именам столбцов и таблиц) через переменные — для всего этого приходится использовать всяческие ухищрения типа временных таблиц и PREPARED STATEMENTS. Кроме того, из-за необходимости создания временных таблиц всё это еще и медленно работает.
Однако, интересно было посмотреть, как вообще такое может выглядеть. Возможно, в коде встретятся приемы, которые могли бы пригодиться в решении реальных, злободневных задач.
Код, пригодный для MySQL версии 5.0 и выше, выглядит так:
CREATE PROCEDURE getmenu (IN tablename VARCHAR(255), IN activeid INT)
BEGIN
DECLARE current, counter, depth INT;
DECLARE tmptablename VARCHAR(64);
SET depth = @@session.max_sp_recursion_depth;
SET @@session.max_sp_recursion_depth = 255;
SET tmptablename = 'menutmptable';
IF tmptablename = tablename THEN SET tmptablename = 'menutmptable2'; END IF;
SET @q = CONCAT('CREATE TEMPORARY TABLE ', tmptablename, ' SELECT * FROM ', tablename);
PREPARE s FROM @q;
EXECUTE s;
CREATE TEMPORARY TABLE chain (id INT);
SELECT id FROM menutmptable WHERE id = activeid INTO current;
IF current IS NOT NULL THEN BEGIN
forchain: LOOP
INSERT INTO chain VALUES (current);
SELECT parentid FROM menutmptable WHERE id = current INTO current;
IF current = 0 THEN LEAVE forchain; END IF;
END LOOP;
END; END IF;
CREATE TEMPORARY TABLE hierarchy (id INT, level INT, counter INT);
SET counter = 1;
CALL build_hierarchy(0, 1, counter);
SELECT m.*, h.level, IF(h.id = activeid, 1, 0) AS active FROM menutmptable m INNER JOIN hierarchy h USING(id) ORDER BY h.counter ASC;
SET @@session.max_sp_recursion_depth = depth;
DROP TEMPORARY TABLE chain;
DROP TEMPORARY TABLE hierarchy;
SET @q2 = CONCAT('DROP TEMPORARY TABLE ', tmptablename);
PREPARE s2 FROM @q2;
EXECUTE s2;
END $$
CREATE PROCEDURE build_hierarchy (IN parent INT, IN level INT, INOUT counter INT)
BEGIN
DECLARE currentid INT;
DECLARE chk, exit_flag BOOL DEFAULT 0;
DECLARE c CURSOR FOR SELECT id FROM menutmptable WHERE parentid = parent;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET exit_flag = 1;
OPEN c;
fetch_loop: LOOP
FETCH c INTO currentid;
IF exit_flag = 1 THEN LEAVE fetch_loop; END IF;
INSERT INTO hierarchy (id, level, counter) VALUES (currentid, level, counter);
SET counter = counter + 1;
SELECT IF(COUNT(*)>0, 1, 0) FROM chain WHERE id = currentid INTO chk;
IF chk != 0 THEN CALL build_hierarchy(currentid, level + 1, counter); END IF;
END LOOP;
CLOSE c;
END $$
DELIMITER ;
Разберем предложенный код (для читателей, не нуждающихся в разборе, дальнейшее содержание статьи, по всей видимости, не будет интересным; если хочется что-то обсудить в комментариях, то можно быстро перейти ближе к концу статьи).
CREATE PROCEDURE getmenu (IN tablename VARCHAR(255), IN activeid INT)
BEGIN
DECLARE current, counter, depth INT;
DECLARE tmptablename VARCHAR(64); -- переменная для имени временной таблицы - все равно больше 64 символов нельзя
/*
MySQL не позволяет бесконечную рекурсию в процедурах и функциях. Для регулировки уровня рекурсии существует специальная переменная под названием max_sp_recursion_depth. Подробнее см.
http://dev.mysql.com/doc/refman/5.1/en/server-system-variables.html#sysvar_max_sp_recursion_depth
http://dev.mysql.com/doc/refman/5.1/en/stored-routines-syntax.html
*/
SET depth = @@session.max_sp_recursion_depth; -- сохраним текущее значение переменой, чтобы потом вернуть его обратно
SET @@session.max_sp_recursion_depth = 255; -- чтобы чувствовать себя свободнее, поставим максимально возможное значение
/*
Теперь нужно как-то добиться того, чтобы работа процедуры не зависела от конкретного имени таблицы, где хранится меню.
Фактически требуется, чтобы можно было обращаться к требуемым данным, используя какой-то один фиксированный идентификатор (имя). Для этого подходит либо VIEW, либо временная таблица. Однако, VIEW будет виден и за пределами сессии, поэтому лучше работать с временой таблицей (временные VIEW, к сожалению, создавать нельзя).
Не стоит забывать, что при создании временной таблицы делается копия данных и при больших объемах это может потребовать значительных ресурсов.
При конфликте имен с другими таблицами временная таблица перекроет существующую; если при этом окажется, что имя, под которым создаётся временная таблица, совпадёт с именем таблицы, где хранится меню, последняя перестанет быть видна процедуре. Чтобы избежать такого, сделаем дополнительную проверку:
*/
SET tmptablename = 'menutmptable';
IF tmptablename = tablename THEN SET tmptablename = 'menutmptable2'; END IF;
/*
Теперь нужно, собственно, временную таблицу создать. MySQL не позволяет вставлять переменные в те места запросов, где должны быть имена таблиц. Обойти это ограничение можно только с помощью PREPARE STATEMENT, которые позволяют выполнить в качестве запроса любую строку.
Мало того, для создания PREPARE STATEMENT еще и придется использовать глобальную переменную сессии, а не внутреннюю: PREPARE STATEMENTS не принимают внутреннюю переменную в качестве строки запроса.
*/
SET @q = CONCAT('CREATE TEMPORARY TABLE ', tmptablename, ' SELECT * FROM ', tablename); -- составляем текст запроса
PREPARE s FROM @q; -- готовим выражение
EXECUTE s; -- выполняем выражение - после этого временная таблица будет создана
CREATE TEMPORARY TABLE chain (id INT); -- сделаем таблицу для хранения раскрытых элементов
SELECT id FROM menutmptable WHERE id = activeid INTO current;
/*
Такое выражение позволяет одновременно и проверить наличие элемента меню с заданным id, и, в случае наличия такового, сразу же присвоить это значение вспомогательной переменной, которая будет использоваться далее.
Если в переменную current попало значение - стало быть, запись с таковым id существует, и можно начинать построение цепочки.
В противном случае таблица с цепочкой останется пустой, т.к. активных элементов нет.
*/
IF current IS NOT NULL THEN BEGIN
forchain: LOOP
INSERT INTO chain VALUES (current);
SELECT parentid FROM menutmptable WHERE id = current INTO current;
IF current = 0 THEN LEAVE forchain; END IF; -- выбираем, пока не достигнем верхнего уровня меню
END LOOP;
END;END IF;
/*
Теперь сделаем еще одну вспомогательную временную таблицу. Основная информация, которая будет храниться в ней - это id нужных нам элементов (тех, которые будут отображаться в меню).
Не менее важная информация - это порядок, в котором элементы будут следовать в меню в конечном итоге. Чтобы иметь возможность установить этот порядок, понадобится специальная колонка - пусть она будет называться counter.
На всякий случай получим и уровень каждого элемента (чтобы, при надобности, всё по многу раз не пересчитывать).
*/
CREATE TEMPORARY TABLE hierarchy (id INT, level INT, counter INT);
/*
Обратите внимание, что в процедуре build_hierarchy() третий аргумент имеет тип INOUT (это означает, что изменения, которые этот аргумент претерпевает во время её работы, сохранятся и после вызова процедуры этот аргумент будет хранить уже измененное значение). По этой причине третий аргумент обязательно должен быть в виде переменной, поскольку новое значение (полученное в результате работы процедуры) должно быть куда-то записано. Первый и второй аргументы имеют тип IN и потому могут передаваться в виде констант.
Начнем с корневых элементов - первый аргумент (id родительского элемента) - равен 0. Уровени меню (здесь - второй аргумент) пусть начинаются с первого. Счетчик начнем с единицы.
*/
SET counter = 1;
CALL build_hierarchy(0, 1, counter);
-- после вызова процедуры build_hierarchy() таблица hierarchy будет нужным образом заполнена, и остаётся только выбрать остальные параметры элементов (такие как название и др.):
SELECT
m.*, h.level, IF(h.id = activeid, 1, 0) AS active
FROM menutmptable m INNER JOIN hierarchy h USING(id) -- USING(id) - то же, что ON h.id = m.id
ORDER BY h.counter ASC;
-- "уберёмся" за собой: вернём переменной прежнее значение и удалим временные таблицы
SET @@session.max_sp_recursion_depth = depth;
DROP TEMPORARY TABLE chain;
DROP TEMPORARY TABLE hierarchy;
-- поскольку имя таблицы в переменной, опять приходится использовать PREPARE STATEMENT и глобальные переменные
SET @q2 = CONCAT('DROP TEMPORARY TABLE ', tmptablename);
PREPARE s2 FROM @q2;
EXECUTE s2;
END $$
CREATE PROCEDURE build_hierarchy (IN parent INT, IN level INT, INOUT counter INT)
/*
Эта процедура занимается заполнением таблицы hierarchy, на основе которой потом строится список элементов меню, причем строится именно в том порядке, в котором они потом будут отображаться. Для каждого из этих элементов процедура должна получить:
1. id
2. уровень
3. порядковый номер (требуется для правильной сортировки)
*/
BEGIN
DECLARE currentid INT;
DECLARE chk, exit_flag BOOL DEFAULT 0; -- BOOL - то же, что TINYINT
DECLARE c CURSOR FOR SELECT id FROM menutmptable WHERE parentid = parent;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET exit_flag = 1;
OPEN c;
fetch_loop: LOOP
FETCH c INTO currentid;
IF exit_flag = 1 THEN LEAVE fetch_loop; END IF;
INSERT INTO hierarchy (id, level, counter) VALUES (currentid, level, counter);
SET counter = counter + 1;
SELECT IF(COUNT(*)>0, 1, 0) FROM chain WHERE id = currentid INTO chk;
IF chk != 0 THEN CALL build_hierarchy(currentid, level + 1, counter); END IF; -- проверяем, есть ли текущий элемент среди раскрытых; если есть - вызываем build_hierarchy() уже для него
END LOOP;
CLOSE c;
END $$
DELIMITER ;
© Все права на данную статью принадлежат порталу webew.ru. Перепечатка в интернет-изданиях разрешается только с указанием автора и прямой ссылки на оригинальную статью. Перепечатка в печатных изданиях допускается только с разрешения редакции.