Однажды я видел, как разработчик переименовывал файл в «ПХПШторме». На экране несколько секунд заполнялся прогрессбар. Нет, спасибо.
Мне написал Александр Яскевич:
То, что ты видел — рефакторинг. Если на этот файл в проекте были ссылки, ПХПШторм их также менял в то время, пока ты наблюдал прогрессбар. Ну или просто искал. Даже если ничего не нашел, время на поиск тратится. В таких мелочах ИДЕ от редактора и отличается.
Я в курсе, что это рефакторинг. Тут есть две проблемы.
Во-первых, непонятно, зачем ему искать? Он же ИДЕ. Даже «просто редактор» умеет индексировать проекты в фоновом режиме. Он безо всякого поиска знает, где упоминается мой файл. И даже если действительно нужно изменить упоминания в нескольких файлах, на современном компьютере это не займёт заметного времени. Поэтому рефакторинг прогрессбару не оправдание.
Во-вторых, непонятно, зачем мне показывать модальное окно с ним? Даже если работа действительно продолжительная, почему я не могу продолжать работать, пока он там переименовывает? По хорошему нужно было бы показать прогрессбар где-нибудь в строке состояния, и если какие-то файлы в итоге изменятся, я должен увидеть соответствующие отметки около них в списке файлов сбоку.
16-го июня прочитаю на Питер-ЦСС доклад о декларативных АПИ на английском. Организаторы конференции ещё напоминают, что с завтрашнего дня билет стоит 4500 ₽ (сегодня ещё 4000 ₽).
Предположим, вы сделали какой-нибудь веб-компонент. Например умную выпадайку с автодополнением под названием superComplete. Тогда вы, скорее всего, предложите инициализировать его как-нибудь так:
var superCompleteInstance = new SuperComplete ('#el')
Или, если это плагин для Джейквери, то так:
$ ('#el').superComplete ()
А надо не так. Пусть оно инициализируется само, увидев знакомый класс:
<input id="address" class="supercomplete" />
Я называю это декларативными АПИ: вместо того, чтобы давать команды, ты описываешь свойства компонента параметрами. Научился я такому сто лет назад у Артёма Поликарпова с его Фоторамой, и использую в своих веб-продуктах.
В «Эмёрдже» чтобы элемент появился только после полной загрузки всего содержимого, а до того отображалась крутилка, нужно завернуть его в такой див:
<div class="emerge" data-spin="true">...</div>
Чем больше всего умеет компонент, тем сложнее придумать такой синтаксис. Особенно сложно становится, когда у вас много параметров, и они определяют поведение во времени и при условиях. Сначала кажется, что в этих случаях без Джаваскрипта никак не обойтись. Но если немного подумать, то часто выясняется, что вполне обойтись.
Расскажу, почему это клёво и как придумывать синтаксис таких вот декларативных АПИ.
Как сказал Стив Джобс, «Design is how it works». Это доклад про дизайн.
Другие объявленные доклады:
Paint the Web with CSS. On Creating Art with Code
Creating Magic With Houdini
Designing Data-Driven Products. Controlled Chaos and Evolution
В отличие от сборки с моего сайта, версия в НПМе не содержит в себе Хоулер. Вместо этого он указан в качестве зависимости. Если вы делаете всё правильно, он подключится автоматически.
Готова Эгея 2.6 (сборка v3101) с автосохранением, новым поиском и платными фичами. Эта заметка частично повторяет анонс беты и заметку о платных фичах, чтобы всё было в одном месте.
Локальное автосохранение заметок
Заметки теперь непрерывно сохраняются в браузере, пока их пишешь, поэтому изменения не потеряются, если пропадёт интернет или отрубится электричество. В анонсе беты я в деталях писал, как это работает.
Красная точка слева — признак того, что есть несохранённые изменения. Если закрыть редактор, а потом открыть заново, он откроется ровно в таком же состоянии: с тем же текстом, с той же красной точкой, означающей, что данные не сохранены, и с курсором, мигающим в том же месте. Чтобы вернуться к сохранённой версии, нажмите ⌘ Z.
Красная точка помогает найти заметки, в которых есть несохранённые изменения:
Локальное автосохранение работает только для заметок и черновиков. Его нет у тегов и комментариев.
Автосохранение запрограммировал Игорь Адаменко — он же ранее помогал мне с новой версией моего сайта. Игорь крутой.
Новый поиск
Поиск научился склонять слова, а выдача стала намного удобнее: вместо простыни заметок она показывает небольшие «сниппеты», содержащие найденный фрагмент текста и все картинки из заметки. Узнать нужную заметку по картинкам — самый кайф:
Если у вас много заметок, Эгее понадобится несколько минут после обновления, чтобы сначала всё проиндексировать. Результаты поиска в это время будут показываться по старой технологии. Вам ничего не нужно делать, Эгея управляет индексацией сама.
Картинки и обложки в редакторе
Эгея с прошлой версии отдаёт картинки в качестве «обложек» при отправке ссылок в соцсети. Но теперь она умеет отдавать так и стоп-кадры видеороликов с Ютюба и Вимео. Ну и заодно у заметок с видеороликами появился шаринг в Пинтерес.
Кстати, эти стоп-кадры попадают и в выдачу поиска наравне с другими картинками, что тоже упрощает нахождение глазами нужной заметки.
При загрузке файлов колечко теперь не просто крутится, а заполняется, показывая прогресс. Удобно, если вы загружаете кучу больших фоток или 15-мегабайтную песню.
Раньше Эгея считала, что картинка используется в заметке, только если её упоминание было в тексте заметки. А если удалить упоминание из текста, то картинка «терялась» и из превьюшек снизу. Теперь это независимые вещи: заметка помнит загруженные в неё картинки, даже если они не используются в тексте.
Это даёт несколько приятностей:
Обложкой для соцсети может быть картинка, которая не используется в самой заметке. Просто загрузите картинку в заметку, но не вставляйте её название в текст. Для этого перетащите её на страницу заметки, но за пределы текстового поля (раньше можно было перетащить только в текстовое поле).
Если вы перетаскивали картинку, но файл с таким именем уже был на сервере, Эгея переименовывала его, добавляя в конец число. В прошлой версии добавилось перетаскивание с зажатым Альтом — в этом случае старая картинка заменялась новой. Теперь работает чуть умнее: если на сервере файл есть, но это тот же самый файл, то она просто добавляет его в ту заметку, куда вы его перетаскиваете, не делая бессмысленную копию на сервере.
Когда вы удаляете картинку из превьюшек в редакторе, раньше Эгея просто удаляла файл на сервере. Теперь так делать нельзя, ведь «официально» поддерживается использование одного файла в нескольких заметках. Эгея смотрит, не используется ли файл где-то ещё, и удаляет его только, если он больше нигде не нужен.
Наконец, Эгея не «потеряет» картинку просто из-за того, что вы убрали её имя текста заметки.
Ещё теперь можно загружать картинки в СВГ.
Всё, что я написал про картинки, верно и для аудиофайлов. Но аудиофайл или СВГ не могут быть обложками для соцсетей.
В анонсе беты ещё рассказывал про залипание лейблов больших полей и про то, что кнопка удаления переехала из подвала формы редактирования заметки в правый верхний угол.
Теперь стандартное форматирование Эгеи поддерживается внутри тегов div, table и blockquote. Если вы хотите поставить текст на плашку или разбить на колонки, можете это сделать, а внутри продолжать использовать привычный синтаксис.
Фичи для платных пользователей
Фичи из этого раздела доступны пользователям с коммерческой лицензией. Такая лицензия нужна, если вы ведёте коммерческий блог или просто хотите такие фичи.
В настройке появились поля Яндекс-метрики и Гугль-аналитики:
В бесплатной версии эти штуки можно — и всегда можно было — прикрутить по-старинке, через допблоки. В платной просто чуть удобнее.
При просмотре черновиков снизу появилась «Секретная ссылка»:
Это ссылка на черновик, которую можно дать кому-то, чтобы он увидел черновик. Я этим пользуюсь, когда публикую рекламу: присылаю рекламодателю, чтобы он утвердил заметку перед публикацией. Если черновик отредактировать, ссылка «сгорает», нужно взять новую.
С бесплатной версией можно — и всегда можно было — просто прислать скриншот.
Теперь вы можете легко прикрутить свою кнопку «Подписаться» (см. у меня под заметкой). Это реализовано через допблок subscribe-sheet. Вы пишете ХТМЛ того, что будет внутри окна подписки, а кнопка под заметками появляется сама. При этом там есть готовые куски, чтобы сделать логотипы фейсбуков и прочего.
Так выглядит комплектное, не настроенное окно подписки:
Заходите в шаблон, убираете лишнее, вставляете свои ссылки — и готово. Если нужны другие соцсети, иконку придётся нарисовать свою. Бардак с русским и английским тут связан с тем, что выбран русский язык интерфейса, но локализованы только заголовок окна и слово «РСС» . Я решил, что локализовывать названия соцсетей смысла нет, потому что вы всё равно сможете написать тут что хотите.
В бесплатной версии можно — и всегда можно было — сверстать это вручную.
И ещё одна фича — глобальные стили. Это немного облегчает прикручивание Эгеи в качестве одного из разделов большого сайта, где уже есть свои стили. Например, на моём сайте с ранних бет 2.6 Эгея использует ЦСС от основного сайта. Для этого надо в файле theme-info.php от вашей темы оформления прописать ключ global_styles. У меня этот файл выглядит так:
В бесплатной версии можно — и всегда можно было — в своей теме оформления копировать стили от основного сайта. Я это раньше делал «Галпом», чтобы два раза не писать, а теперь вот не надо.
Если у вас уже есть платная лицензия, и вы хотите сборку беты 2.6 с этими фичами, напишите мне письмо.
Теперь снова о том, что доступно всем.
Автомиграция при подключении к старой базе
Когда вы обновляли Эгею, она меняла структуру базы данных для совместимости с новой версией — так было всегда.
Но раньше движок конвертировал базу только при обновлении, а вообще исходил из того, что база в актуальном формате. Поэтому если вы просто подключали его к существующей базе, а она была в старом формате, возникали ошибки.
Теперь движок наоборот исходит из того, что данные в базе могут быть в любом формате, начиная с формата Эгеи 2.0. Если вы устанавливаете движок начисто, а не обновляетесь, но при этом говорите, что данные в базе уже есть, или просто подключаетесь к другой базе в настройке, движок сконвертирует базу в актуальный формат.
Остальное
Исправлена куча багов. Но этим никого не удивишь.
При редактировании описания тега теперь тоже можно загружать файлы. Короче, описание тега теперь может выглядеть как полноценная заметка, у него тоже работают всякие обложки. Редактор тега открывается по Alt+E на странице тега, как у заметки. А сохранение изменений в описании тега заработало в сто раз быстрее.
Для залогиненного автора быстрее открываются заметки с кучей комментариев.
Изменилась логика включения комментариев. Раньше был глобальный флаг «разрешить комментарии», но можно было выключить индивидуально у отдельных заметок. Теперь глобального флага нет, комментарии настраиваются у заметок по отдельности. А глобально настраивается, врубать ли комментарии заметкам во время публикации по умолчанию или нет.
Основной кегль текста в комплектной теме увеличен с 15 до 16 пунктов. Знаки валют прикрепляются к суммам неразрывным пробелом. Форма логина заадаптивилась (в 2.5 недоглядели).
На странице удаления заметки появилась кнопка «Вернуть в черновики». На страницах архива, где показывают заметки списком ссылок, избранные заметки помечаются звёздочками. Добавилась поддержка шаринга в Линкед-ин (потому что обновился Лайкли).
Технические детали
Движок лучше распознаёт, что работает по ХТТПС, и отдаёт по этому же протоколу всякие скрипты и картинки.
Добавилась поддержка Апача 2.4, у которого с какого-то бодуна изменился на полностью нечитаемый синтаксис некоторых опций конфигурации. Движок должен работать под ПХП 7.0 и 7.1 — я теперь тестирую локально под семёркой. А на моём сайте, наоборот, крутится ПХП 5.3 — это теперь низшая из поддерживаемых версий.
Все скрипты теперь минифицируются.
Файл, в котором хранятся параметры настройки теперь называется не settings.psa, а settings.json. Если вдруг захочется вручную что-то отредактировать, теперь это удобнее.
Если вы делали свою тему оформления, посмотрите, что изменилось в комплектной, и аккуратно повторите эти изменения у себя.
Я уже рассказывал, что в новой Эгее 2.6 — новый поисковый движок «Роза» Романа Парпалака. На примере Эгеи расскажу немного о том, как вы можете использовать его в своих продуктах.
Что делает Роза
Роза — движок поиска по переданным ему данным.
У Розы нет «паука» для индексации. Это специально. В случае с движком блога, например, нет никакого смысла искать ссылки, ходить по страницам, разбирать их код. И ещё убеждаться, что не индексируешь одну и ту же заметку несколько раз, потому что она встретились на страницах разных тегов. Это лишняя работа: приложение-клиент знает, как устроены данные в нём, и может отдавать их поиску сразу «в чистом виде».
Роза понимает три команды: «проиндексируй вот это», «удали из индекса вот это» и «найди всё вот по этому запросу». Как устроен индекс и как она находит — клиенту знать не нужно, это чёрный ящик. Клиент должен только обеспечить Розе доступ к хранилищу, где она будет держать индекс, и своевременно сообщать об изменениях, чтобы этот индекс был актуален.
Таким образом, поиск — это взаимная работа клиента и Розы. Прикрутить её сложнее, чем использовать движок поиска общего назначения. Зато понимание структуры данных позволяет гибко форматировать результаты и учитывать их природу при ранжировании.
Любой приличной заметке в современном интернете нужна картинка. Поэтому пусть здесь будет скриншот, иллюстрирующий покрытие Розы автоматизированными тестами:
Теперь к делу.
Хранилище
Хранилище — это объект, через который Роза взаимодействует с базой данных. Клиенту нужно передавать его Розе и для индексации, и для поиска. Поэтому удобно иметь функцию, которая его отдаёт, когда нужно:
Для простоты я убрал некоторые специфичные для Эгеи вещи. Я тут проверяю, нет ли уже и так созданного хранилища; если нет, то создаю; и в любом случае его возвращаю. На место переменных, начинающихся с подчёркивания, надо поставить настоящие реквизиты базы данных.
Индексация
Задача клиента — «рассказать» Розе о данных, чтобы она построила свой индекс. Для этого нужно добавить каждую сущность (в Эгее — заметку) в индекс:
$stemmer = new PorterStemmerRussian ();
$indexer = new Indexer (e2_rose_storage (), $stemmer);
$indexable = new Indexable (
$_note_id,
$_note_title,
$_note_text
);
$indexer -> index ($indexable);
Когда пользователь публикует или изменяет заметку, я вызываю этот код. По идентификатору Роза понимает, нужно ли добавить в индекс новую запись или переиндексировать старую.
Стеммер — это объект, к которому обращается Роза, чтобы сравнивать слова в разных падежах и формах. Его нужно передавать при индексации и при поиске. Он обособлен для того, чтобы можно было сделать свой стеммер для другого языка и подключить, не меняя ничего в индексаторе и поисковике.
Как видите, на индексацию я передаю идентификатор, заголовок и текст заметки, а не адрес её страницы. Когда Роза что-то находит, она мне так же возвращает список идентификаторов найденных сущностей, а не ссылки. Построить страницу выдачи с рабочими ссылками — моя забота (но Роза поможет подсветить результаты поиска в выдаче — об этом ниже).
Можно отдавать и адрес страницы при индексациии, но для Розы это просто текстовое поле, она не пойдёт по этому адресу низачем, просто вернёт его потом вместе с результатами поиска. Я этим не пользуюсь, потому что мне сподручнее получить адрес заметки из неё идентификатора, а не от Розы.
Индексация одной заметки — быстрая операция, поэтому поддержание индекса в актуальном состоянии не составляет проблемы.
Но при обновлении Эгеи со старой версии, мне нужно аккуратно проиндексировать все написанные ранее заметки. В случае с моим блогом это около четырёх тысяч заметок. Поэтому просто запустить их индексацию в цикле нельзя: даже если операция уложится во временной лимит ПХП, всё это время пользователь будет думать, что сайт не отвечает. А если не уложится, то пользователь вообще ничего не увидит. Как обеспечить безболезненную фоновую индексацию — тема отдельной заметки, но важно, что это забота клиента, а не Розы.
Когда пользователь удаляет или отзывает заметку, я делаю примерно так:
$stemmer = new PorterStemmerRussian ();
$indexer = new Indexer (e2_rose_storage (), $stemmer);
return $indexer -> removeById ($_note_id);
Поиск
Чтобы искать, нам понадобится файндер (а ему понадобится стеммер):
$stemmer = new PorterStemmerRussian ();
$finder = new Finder (e2_rose_storage (), $stemmer);
$finder -> setHighlightTemplate (
'<span class="'.CSS_CLASS_HIGHLIGHT.'">%s</span>'
);
Здесь я также настраиваю шаблон для подсветки найденных слов в результатах поиска.
Теперь можно сделать запрос и достать результаты поиска:
Тут я не могу продолжать как ни в чём не бывало и не сделать ремарку. Объекто-ориентированное программирование делает мне больно. Просто так написать нельзя:
С объектами для выражения простых мыслей требуется много строк. Связанные фрагменты кода невозможно разместить в поле зрения, и постоянно приходится скролить туда-сюда, держа вещи в памяти. Я понимаю, что объекты имеют преимущества. Но когда-нибудь программисты поймут, что так писать — тоже не дело, и придумают способ вернуть коду выразительность, не потеряв той пользы, которую им приносят объекты.
Итак, после того, как мы получили $resultSet, мы уже можем выводить результаты. Но это тот момент, когда можно попросить Розу подкрутить релевантность, что я и делаю:
Тут же Розу можно попросить сделать «сниппеты» — так называются фрагменты, выводящиеся в результатах поиска. Роза сама находит предложения с найденными словами, подсвечивает в них эти слова и собирает маленький кусок текста, достаточный для узнавания заметки в выдаче. Это делается так:
Чтобы собрать сниппеты, Розе нужны полные тексты заметок — сама она их не хранит, а о структуре моей базы данных ничего не знает. Поэтому в метод attachSnippets мы тут передаём колбек, который Роза вызывает, чтобы их получить. Он, как видите, вытаскивает заметки из базы.
Я подсвечиваю найденные слова в заголовке и показываю сниппет вместо текста заметки.
Код выдачи результатов поиска неэлегантен — хотя бы потому, что я трижды прохожусь циклом по этим результатам, многократно вызываю одни и те же функции с одними и теми же параметрами. Никаких измеримых проблем эта неэлегантность не создаёт, а её исправление потребовало бы изменения АПИ Розы. Поэтому пока пусть будет так.
В этой заметке я упускаю часть слишком специфичных для Эгеи решений, фрагменты кода местами несколько упрощены. Из примеров полностью выпилен весь код, отвечающий за «смешение» результатов поиска Розы и стандартными средствами БД (я это делаю, потому что у Розы более высокие системные требования, чем у Эгеи, и на некоторых хостингах она может просто не завестись).
Если захотите использовать Розу в своих продуктах, своими мыслями делитесь с Романом, а не со мной.
У меня появляется новая аудитория, поэтому время от времени я повторяю эту заметку.
Многие не знают, что такое РСС (RSS) и как им пользоваться. К сожалению, РСС — немного гиковская технология, не вполне доступная нормальным людям. Но если её освоить, то становится непонятно, как ты до этого жил. В этом смысле РСС похож на торенты: там тоже есть некоторый порог входа, но польза оправдывает затраты сил на его преодоление.
Что такое РСС? Это механизм подписки на блоги и другие сайты с регулярными обновлениями. Вместо того, чтобы самому ходить по разным сайтам и смотреть, не появилось ли там чего новенького, разумно поручить эту тупую работу машине. Пусть она ходит по всем сайтам и аккуратно собирает всё новое в одном месте. Тогда получается, что новости просто приходят к нам, а не мы охотимся за ними.
Чтобы использовать РСС, нужна РСС-читалка. Это программа, которая знает, какие сайты вы любите, и следит за обновлениями на них. Когда сайты обновляются, свежие публикации автоматически скачиваются, и вы получаете свежий выпуск собственного «журнала», где собрано всё новое с любимых сайтов.
Вот как сейчас выглядит моя читалка:
Здесь все непрочитанные посты со всех блогов, за которыми я слежу. Мне не приходится ходить на их сайты, открывать сто букмарок в браузере — всё в одном месте. Не нужно ждать, пока откроется страница — всё уже скачано. Я не боюсь пропустить что-то важное — всё сохранено. Что-то я прочту сразу, что-то в выходные, что-то — когда буду в следующий раз в самолёте без интернета. А что-то не стану читать, просто ткну в него, чтобы оно перестало выглядеть как непрочтённое.
Как подписаться на кого-нибудь по РСС? Многие РСС-читалки позволяют подписать напрямую, то есть они сами будут скачивать всё новое. Но это непрактично: если вы подпишетесь независимо на компьютере, айпаде и айфоне, то три читалки не будут знать, что́ вы уже прочитали, и все заметки вы будете получать по три раза. Намного лучше воспользоваться сервисом-агрегатором, который будет ходить и собирать новости, а читалки с ваших устройств будут просто синхронизироваться с этим сервисом.
Заведите себе акаунт, например, в Фидли. В меню слева сверху есть пункт Add content — введите туда адрес блога, например, моего. Всё, теперь вы подписаны, новые посты будут приходить к вам туда. Подпишитесь так же на все любимые блоги, и теперь сможете читать всё в одном месте, например:
У самого у меня около двух сотен разных источников в читалке. Среди них не только блоги, но и, например, результаты поиска в Яндексе по моей фамилии — так я всегда автоматически вижу, когда кто-то где-то ссылается на меня. Если вы сами читаете этот пост через РСС и недоумеваете, зачем я всё это объясняю — привет!
Конечно же, любой блог на Эгее поддерживает РСС, для этого ничего не нужно настраивать.
Читать РСС прямо в интерфейсе Фидли — это такое же насилие над собой, как использование Гмейла через веб-интерфейс. Кому-то нравится такая фигня, но мне не понять.
Для чтения РСС я предпочитаю Риидер (он платный, есть миллион других, в том числе бесплатных читалок). Риидер бывает для Айфона, Айпада и Мака — везде сказочно красивый (сверху это он на скриншоте). После того, как вы его установите, его надо будет привязать к вашему акаунту в Фидли, и всё, он будет показывать всё, на что вы подписаны. То есть это просто клиент для агрегаторов. Кстати, кроме Фидли есть ещё куча агрегаторов, просто я предпочитаю Фидли: он бесплатный и работает без нареканий. Риидер поддерживает множество разных агрегаторов, комбинировать можно как угодно. Вы можете даже использовать разные клиенты на разных устройствах — всё со всем без проблем синхронизируется.
Ещё часто спрашивают, зачем РСС, если есть соцсети. Вроде как, все туда кидают ссылки на свои посты. Тут дело личное, конечно, но я не понимаю, как можно даже сравнивать по удобству одно с другим.
Во-первых, за соцсетями, чтобы ничего не пропустить, надо внимательно следить, а это съедает целую кучу времени. И Твиттер, и Телеграм я читаю по диагонали и по умолчанию считаю, что там ничего особо ценного нет. А Фейсбук вообще ставит посты в одному ему известном порядке. Читалка ничего не пропустит.
Во-вторых, в большинство соцсетей попадают только ссылки. Чтобы посмотреть хотя бы в общих чертах, о чём статья, насколько она длинная, нужно переходить по ссылке и ждать, пока она откроется в браузере. Это трение. В читалке же к тому моменту, как я туда смотрю, всё уже скачано и показывается мгновенно.
В-третьих, как я уже говорил, я хочу иметь возможность читать что-то, когда это удобно мне, а не когда этим поделился автор. В Твиттере, например, приходится помечать интересное сердечком, потом возвращаться к нему — это всё лишняя работа. В читалке меня просто всё спокойно дожидается, мне не нужно для этого ничего делать.
Теперь вместо браузера и твиттера для чтения блогов вы можете использовать клёвый, удобный, придуманный специально для этого инструмент — РСС-читалку. На здоровье!
На прошедшей неделе мы открыли остаток второго раздела электронного учебника «Пользовательский интерфейс» — главы «Обратная связь», «Листание и прокрутка». Теперь первые два раздела доступны целиком.
Из письма издательства: «Бывало ли у вас такое, что вы приходите в гости, звоните в звонок, но ничего не слышите? Вы звоните ещё раз, но снова тишина. Начинаете стучать в дверь, и тут открывает возмущённый хозяин: „Чё трезвонишь?!“ Проблема в отсутствии обратной связи. Интерфейс дверного звонка прост, а в более сложных интерфейсах вроде управления автомобилем, самолётом или графическим редактором её качество ещё важнее. Из главы вы поймёте, почему руль на автомобиле удобнее, чем стрелки на клавиатуре в автосимуляторах.»
Люблю приводить старые примеры:
И, наоборот, новые:
И снова старые:
И снова новые:
Хотя вроде мода на такую фигню быстро прошла.
В конце опубликованных глав — тест по материалу из них.
У тех, кто оформил предзаказ, время подписки пока не тикает, а первый и теперь часть второго раздела уже есть. Если это вы, бегите читать!