Перейти к содержанию

Таблицы, код и прежняя 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 = "&lt;p&gt;Текст&lt;/p&gt;"
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 буквальный текст &lt;p&gt;, посетитель может увидеть сами обозначения сущностей: мы получили бы двойное экранирование. При переносе кода нужно различать исходный текст программы и его старую 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 = &quot;&lt;p&gt;Текст&lt;/p&gt;&quot;
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. В этом уроке у собственного документа нет такой функции, поэтому результатом является именно сохранённая учебная страница. Следующий шаг — включить все три материала в каталог, сохраняя один адрес для каждого.