Таблицы, код и прежняя HTML-разметка
Наш сборщик уже создаёт страницы из Markdown и подключает общие ресурсы. Однако при обновлении старого сайта часть материала может оказаться неудобной для чистого Markdown: таблицы содержат объединённые ячейки, на заголовки ведут ссылки с якорями, а примеры кода оформлены HTML. Принудительная замена всей разметки способна изменить смысл или потерять адресуемый фрагмент.
При переносе ProfessorWeb мы сохраняли содержание и старые маршруты, а сложные фрагменты оставляли в HTML. Здесь разберём тот же принцип на небольшом собственном файле. Реальную библиотеку заново импортировать не будем. Результатом урока станет третья статья по адресу /my/legacy/first.php, использующая наши общие шаблоны.
Что именно переносится из старой страницы
Создайте папку original в учебном проекте. В original/lesson.html сохраните полный документ:
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Сохранённая HTML-страница</title>
</head>
<body>
<header>Прежняя шапка учебного сайта</header>
<main>
<h1>Сохранённая HTML-страница</h1>
<p>Сборка получает исходник и записывает готовый HTML.</p>
<h2 id="overview">Файлы проекта</h2>
<table>
<caption>Исходники и результат</caption>
<thead><tr><th scope="col">Группа</th><th scope="col">Файл</th></tr></thead>
<tbody>
<tr><th rowspan="2" scope="rowgroup">Исходники</th><td>content/index.md</td></tr>
<tr><td>layouts/page.html</td></tr>
<tr><th scope="row">Результат</th><td>dist/index.html</td></tr>
</tbody>
</table>
<h2 id="example">Пример преобразования</h2>
<pre><code class="language-python">source = "<p>Текст</p>"
print(source)
</code></pre>
<p><a href="#overview">Вернуться к списку файлов</a>.</p>
</main>
<footer>Прежний футер учебного сайта</footer>
</body>
</html>
Сохранённый оригинал нужен для сравнения, а не для дальнейшего редактирования. Старые header, footer, head и основной H1 относятся к оболочке: в новой странице их создают шаблоны и метаданные. Содержанием являются абзац, два раздела, таблица, код и ссылка. Если скопировать целый документ внутрь Markdown, он попадёт в уже существующий article; получится вложенный html и повторный заголовок.
В таблице слово «Исходники» относится к двум строкам. Атрибут rowspan выражает эту связь; преобразование её в обычную таблицу с пустой второй ячейкой меняет структуру. caption даёт название, а scope указывает назначение заголовочных ячеек. Мы сохраним эти сведения вместе с таблицей, не пытаясь представить их вертикальными чертами Markdown.
Якоря overview и example тоже часть интерфейса материала. Ссылка /my/legacy/first.php#overview состоит из адреса документа и идентификатора внутри него. Сохранение только .php не сохранит такой переход, если исчезнет id. Поэтому при переносе нужно учитывать как страницы, так и фрагменты, на которые уже могли ссылаться.
Markdown с сохранёнными HTML-фрагментами
Добавьте content/legacy.md. Ниже приведён полный исходник третьей статьи учебного сайта:
---
title: Сохранённая HTML-страница
description: Перенос учебной HTML-страницы.
permalink: /my/legacy/first.php
---
Сборка получает исходник и записывает готовый HTML.
<h2 id="overview">Файлы проекта</h2>
<table>
<caption>Исходники и результат</caption>
<thead><tr><th scope="col">Группа</th><th scope="col">Файл</th></tr></thead>
<tbody>
<tr><th rowspan="2" scope="rowgroup">Исходники</th><td>content/index.md</td></tr>
<tr><td>layouts/page.html</td></tr>
<tr><th scope="row">Результат</th><td>dist/index.html</td></tr>
</tbody>
</table>
<h2 id="example">Пример преобразования</h2>
```python
source = "<p>Текст</p>"
print(source)
```
[Вернуться к списку файлов](#overview).
Метаданные задают тот же видимый заголовок, но сам H1 больше не находится в теле. Обычный абзац и ссылка стали Markdown. Сложная таблица и заголовки с существующими id остались HTML. Такая смесь допустима: формат исходника служит удобству редактирования, а браузер всё равно получает один HTML-документ.
В блоке Python теперь написаны настоящие символы < и >. Они не должны превращаться в HTML-теги, поскольку находятся внутри fenced-блока кода. Преобразователь экранирует их для результата. Если вместо этого оставить в Markdown буквальный текст <p>, посетитель может увидеть сами обозначения сущностей: мы получили бы двойное экранирование. При переносе кода нужно различать исходный текст программы и его старую HTML-запись.
Пустые строки вокруг таблицы отделяют блочный HTML от соседнего Markdown. Внутри сохранённого table используем обычную HTML-разметку целиком. Например, **Исходники** внутри такого блока не следует считать гарантированным выделением: по умолчанию содержимое блочного HTML не разбирается как Markdown. Это поведение и отдельное расширение для смешанного разбора описаны в документации Python-Markdown. В нашем примере расширение md_in_html не требуется; сохранённые ячейки уже содержат нужный текст.
Обратите внимание и на адрес. Сборщик создаст dist/my/legacy/first.php, но содержимым файла будет HTML, а не PHP-программа. Это сохраняет форму URL на уровне файлов. Как хостинг будет выдавать такое расширение, мы разберём в уроке о публикации. Переименование страницы в .html без решения для прежнего адреса разрушило бы входящие ссылки, даже если сам текст остался доступным.
Обычные таблицы и нумерация списков
Не всякая таблица требует HTML. Когда каждая строка имеет одинаковый набор ячеек, Markdown значительно удобнее. В build.py замените только функцию render_markdown; её имя и остальные функции сохраняются:
def render_markdown(text):
return markdown.markdown(
text,
extensions=["fenced_code", "tables", "sane_lists"],
output_format="html",
)
Мы не добавили новую библиотеку. tables и sane_lists входят в Python-Markdown, но включаются явно. Теперь допишите в конец того же content/legacy.md учебное дополнение:
## Дополнительные сведения
| Место | Назначение |
| --- | --- |
| content | Тексты и метаданные |
| layouts | Общая оболочка |
| public | Готовые ресурсы |
Продолжение последовательности:
4. Преобразовать содержание.
5. Добавить общую оболочку.
6. Записать HTML.
Это новый фрагмент нашего материала, а не результат преобразования прежней таблицы. После сборки ожидается обычный table с двумя колонками и список, начинающийся с четвёртого пункта. Расширение Tables предназначено для простых таблиц такого вида. Оно не заменяет rowspan или colspan; если эти отношения важны, оставляем исходный HTML.
Нумерация в конце демонстрирует полезное свойство Sane Lists: начало упорядоченного списка учитывается в HTML. Значение 4 превращается в start="4" у ol. Без этого выбранная обработка списков могла бы начать отображение с единицы, независимо от первой цифры в исходнике.
Начальные страницы сохраняют свои файлы и адреса. Изменение render_markdown относится ко всем статьям, поэтому стоит впоследствии просмотреть и старые списки: расширение задаёт общие правила разбора, а не настройку только одного документа. Для предсказуемого результата разделяйте абзацы и списки пустыми строками.
Локальный просмотр страницы с расширением PHP
После добавления legacy.md выполните .venv/bin/python build.py. Сборщик должен сообщить о трёх страницах. Для их просмотра предыдущий сервер python -m http.server теперь недостаточен: по расширению .php он может выбрать MIME-тип, при котором браузер предложит загрузить файл вместо отображения HTML. Расширение файла и формат его содержимого — разные сведения.
Остановите предыдущий сервер сочетанием Ctrl+C и создайте рядом с build.py полный serve.py:
from functools import partial
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path
ROOT = Path(__file__).resolve().parent
class StaticHandler(SimpleHTTPRequestHandler):
def guess_type(self, path):
if Path(path).suffix.lower() == ".php":
return "text/html; charset=utf-8"
return super().guess_type(path)
def main():
handler = partial(StaticHandler, directory=str(ROOT / "dist"))
with ThreadingHTTPServer(("127.0.0.1", 8000), handler) as server:
print("Просмотр: http://127.0.0.1:8000/")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nЛокальный сервер остановлен")
if __name__ == "__main__":
main()
Запустите его из виртуального окружения:
.venv/bin/python serve.py
Метод guess_type определяет значение Content-Type. Для .php мы явно задаём HTML с UTF-8, а для остальных ресурсов оставляем обычное поведение. Параметр directory привязывает выдачу к папке dist, независимо от текущей папки запуска. Сервер слушает только локальный адрес; при Ctrl+C работа заканчивается, а контекстный менеджер закрывает серверный сокет. Используемые классы и ограничения учебного сервера описаны в документации Python.
Теперь откройте http://127.0.0.1:8000/my/legacy/first.php. Ожидается отображение статьи, а не выполнение Python или PHP. Наш обработчик просто выдаёт байты уже созданного файла. Эта настройка действует только у локального помощника: поведение Beget нужно будет настроить и проверить отдельно. В следующих уроках для просмотра этого проекта будем использовать serve.py.
Сравнение содержания и ограничения переноса
В результате появится файл dist/my/legacy/first.php. Общая оболочка, CSS и знак будут взяты из предыдущего урока. Внутри статьи ожидаются сохранённые разделы, объединённая ячейка «Исходники», оба идентификатора и дополнительная простая таблица.
В коде результата ожидается следующий существенный фрагмент; расположение кавычек и пробелов вне него не является целью переноса:
<h2 id="example">Пример преобразования</h2>
<pre><code class="language-python">source = "<p>Текст</p>"
print(source)
</code></pre>
В браузере код должен показывать строку source = "<p>Текст</p>". Это важнее буквального совпадения HTML-файлов: у нового документа другие хедер, футер и служебные метаданные, поэтому сравнение целых файлов всегда найдёт различия. Сопоставлять нужно содержимое статьи: текст, порядок разделов, данные таблиц, код, картинки и работоспособность ссылок.
Для собственного переноса удобно вести небольшую запись: прежний адрес, новый permalink, старые якоря, ресурсы и фрагменты, оставленные HTML. Она помогает обнаружить потери до публикации. Если старая картинка имела относительный путь images/example.png, учитывайте адрес страницы: на /my/legacy/first.php он означает /my/legacy/images/example.png. Можно сохранить ресурс на этом пути либо явно изменить ссылку; перенос только файла в произвольную папку не исправит запрос браузера.
Сохранённый HTML должен происходить из доверенных исходников. Python-Markdown не очищает результат от опасной разметки — это прямо указано в официальном описании безопасности. Для собственного проверенного материала это соответствует нашей модели, но принимать таким способом произвольный пользовательский HTML нельзя. Экранирование title и description в шаблоне не очищает body.
Статическая сборка также не переносит серверное поведение формы, форума или загрузки файлов. HTML формы можно сохранить визуально, однако прежний обработчик не появляется от преобразования Markdown. В этом уроке у собственного документа нет такой функции, поэтому результатом является именно сохранённая учебная страница. Следующий шаг — включить все три материала в каталог, сохраняя один адрес для каждого.