Andrey Epifancev 372a195ae0 chore(openspec): архив wiki-render (#53)
Дельта web-app влита в openspec/specs: изменено «Карточка задачи», добавлено «Разметка Jira в карточке».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 09:23:28 +04:00

18 KiB
Raw Blame History

Context

main 86739cf. Устройство, на которое опираются правки:

  • Данные. GET /issues/{key} отдаёт description: string | null и comments: [{comment_id, author, created, updated, body}] — тексты Jira как есть (вики-разметка Jira Server, REST v2). Хаб и MCP не меняются.
  • Карточка — web/src/issues/IssueCard.tsx: Description выводит текст в <pre class="description"> со свёрткой до 8 строк (-webkit-line-clamp на .description.clamped, признак «длинное» — больше 8 строк или scrollHeight > clientHeight, кнопка «развернуть» / «свернуть»); Comments выводит <pre class="body"> в каждом <li>, над ним .who (автор, время); «Скрыть» / «Показать» раздела — useCommentsHidden (localStorage). Стили — web/src/issues/issues.css, токены цветов — var(--panel), var(--line), var(--muted) и др.
  • Ссылка на карточку — IssueLink (web/src/issues/IssueLink.tsx): <Link to="/issues/{key}" state={openCardState(loc)}>, открывает задачу в том же окне с сохранением страницы под ним.
  • Реальные тексты AURORA (выборка 17.10.2026): заголовки h3.; нумерованные # и маркированные * списки; * пункт с пробелом перед маркером и пустыми строками между пунктами (шаблон ошибок); *Жирный заголовок.* в начале комментария; ключи задач AURORA-1445 в тексте; в описаниях, написанных через MCP, — Markdown-конструкции (`код`, - пункт), которые Jira показывает как есть (- — список Jira).

Goals / Non-Goals

Goals: описание и комментарии читаются так, как в Jira; исходник любого блока — одной кнопкой; рендер не ломает карточку ни на каком тексте и не открывает путь для вставки разметки или скриптов.

Non-Goals: см. proposal.md.

Decisions

  1. Модуль web/src/issues/wiki/. parse.ts — чистая функция parseWiki(text: string): Block[] (дерево блоков и строчных узлов, без React); WikiText.tsx — компонент <WikiText text=… />, превращающий дерево в React-элементы. Разделение даёт тесты разборщика без DOM и один компонент для описания и комментариев.
  2. Дерево. Блоки: heading {level, inlines}, paragraph {inlines} (одиночные переносы внутри — узел br), list {ordered, items: [{inlines, children: list[]}]}, table {rows: [{cells: [{header, inlines}]}]}, pre {text} ({code}, {noformat}), quote {blocks} ({quote}, bq.), hr. Строчные: text, strong, em, del, ins, sup, sub, code ({{…}}), link {href, inlines}, issue {key, inlines?}, mention {login}, br. Рендер: h1–h6 (внутри карточки уровни сдвигаются стилем, не тегом — тег по разметке), p, ul/ol/li, table/th/td, pre, blockquote, hr, strong, em, del, ins, sup, sub, code, <a target="_blank" rel="noopener noreferrer">, IssueLink.
  3. Разбор блоков — построчно. Строка классифицируется по началу: h[1-6]. , маркеры списка ^\s*[*#]+ или ^\s*- (ровно один -; --, *-, -# — не список), |, {code…} / {noformat} / {quote} (открывающий тег в начале строки после необязательных пробелов; содержимое — всё до первого закрывающего тега, который может стоять в той же строке или в любой последующей; остаток строки после закрывающего тега разбирается как новая строка; тег не в начале строки — обычный текст), bq. , ----, пустая строка; прочее — абзац. Соседние строки списка образуют один список; пустая строка между пунктами одного вида списка не рвёт список (так Jira показывает шаблон ошибок с пустыми строками между *). Незакрытый {code} / {noformat} / {quote} — открывающая строка разбирается как обычный текст (видна как есть).
  4. Строчный разбор — однопроходный по позиции с проверкой границ из спеки: открывающий маркер — в начале текста или после пробела / знака препинания, за ним не пробел; за открывающим не тот же символ (**x**, написанное через MCP Markdown-ом, остаётся текстом как есть — читается лучше полуразобранного); закрывающий — тот же символ после не-пробела, за ним не буква и не цифра; закрывающий ищется до конца строки (выделение не переходит через перенос). Без пары — символ остаётся текстом. Вложенность (жирный курсив *_x_*) — рекурсивным разбором содержимого. {{…}} и содержимое {code} не разбираются. \ экранирует следующий символ разметки.
  5. Ссылки и безопасность. Адрес ссылки проходит проверку схемы (http:, https:, mailto:, без учёта регистра, после обрезки пробелов); иначе вся конструкция […] — текст. Голые адреса — https?:// до пробела, хвостовые .,;:!?) не входят в адрес. Ключ задачи — \b<project_key>-\d+\b, где project_key — из настроек хаба (useSettings(), сейчас AURORA), не внутри адреса, {{…}}, {code}; parseWiki(text, {projectKey}), без projectKey (настройки ещё не загружены) ключи — текст. Так UTF-8, SHA-256, DOC-221280 ссылками не становятся. Рендер только через React-элементы: текст экранирует React, href — после проверки схемы; dangerouslySetInnerHTML в модуле нет (проверка — тестом поиска по исходникам модуля).
  6. Отказоустойчивость. WikiText — собственная маленькая граница ошибок (класс с getDerivedStateFromError, по образцу shell/ErrorBoundary.tsx, но с фолбэком «исходник», а не «Страница сломалась»), внутри — разбор и рендер: при исключении блок показывается исходником (<pre class="wiki-source">), остальная карточка работает. Разборщик не бросает на нормальном пути; граница — страховка.
  7. Переключатель. Компонент SourceToggle — кнопка .link с текстом «исходник» (в режиме рендера) / «текст» (в режиме исходника), aria-pressed. Состояние — useState(false) внутри Description и внутри компонента одного комментария (Comment, ключ — comment_id); IssueModal монтирует карточку заново при смене задачи (<ErrorBoundary key={issueKey}>), поэтому выбор не переживает переход по связи и закрытие окна. Без localStorage.
  8. Свёртка в режиме рендера. -webkit-line-clamp не работает на контейнере с блочными детьми (списки, таблицы), поэтому свёрнутое описание ограничивается высотой: max-height = 8 строк (calc(8 * 1.5em) при line-height: 1.5) и overflow: hidden, признак «длинное» в режиме рендера — только измерение scrollHeight > clientHeight + 1 после рендера (без счёта строк исходника: шаблон ошибок с пустыми строками между пунктами иначе показал бы «развернуть» без нужды). В режиме исходника свёртка и признак прежние (больше 8 строк исходника или измерение). Переключение режима не сбрасывает «развёрнуто».
  9. Вид. Отрендеренный текст — в том же «листе», что и сейчас (.description: фон var(--panel), рамка, отступ), шрифт интерфейса; заголовки — 15/14/13 px полужирным, отступы абзацев и списков компактные; таблица — рамки var(--line), заголовочные ячейки — фоном; {code} / {noformat} / {{…}} — var(--font-mono); цитата — левая полоса var(--line); широкая таблица или pre — горизонтальная прокрутка внутри блока, окно не расширяется.
  10. Тесты. Разборщик — таблица случаев «текст → дерево» в parse.test.ts (все конструкции спеки, границы выделения, ложные срабатывания data_type, даты, a*b*c, числа с -, незакрытые блоки, опасные схемы, ключи задач, тексты по образцу реальных AURORA). Компонент — WikiText.test.tsx (теги, ссылки, IssueLink, неизвестное текстом, фолбэк при ошибке). Карточка — IssueCard.test.tsx (кнопки у описания и у каждого комментария, независимость, сброс при новом монтировании, свёртка в обоих режимах). e2e — сценарий «Описание со структурой» и «Переключение на исходник» в web/e2e/issues.spec.ts. Фикстуры — синтетические тексты по образцу реальных, без данных заказчика (R-1).

Risks / Trade-offs

  • Собственный разборщик расходится с Jira в краевых случаях (вложенные таблицы, смешанные списки, макросы). Смягчение: неразобранное — текстом, исходник — одной кнопкой; расхождение видно и чинится точечно.
  • Высота свёртки в режиме рендера — приблизительно 8 строк (заголовок или таблица выше строки текста).

Decisions made without discussion

Маркер Дата Решение Обоснование Цена ошибки
R-1 2026-10-17 Тестовые тексты — синтетические по образцу реальных описаний AURORA, без копирования текстов задач в репозиторий В задачах — названия организаций и цифры заказчика; конструкции разметки воспроизводятся без них Пропущенная редкая конструкция реального текста; видна в карточке, чинится тестом
R-2 2026-10-17 Ссылками становятся только ключи проекта project_key из настроек хаба (AURORA-123), открывают карточку в окне хаба, а не в Jira Jira делает ссылками ключи существующих проектов; хаб знает только свой проект, а общий шаблон [A-Z]+-\d+ ловит UTF-8, SHA-256, DOC-221280 (builder check); переход в Jira есть в шапке Ключи других проектов Jira — текстом; задача AURORA вне зеркала — «нет в зеркале»
R-3 2026-10-17 [~логин] показывается как «@логин»: отображаемых имён пользователей в хабе нет Jira показывает имя, но словаря логин → имя в зеркале нет Логин вместо ФИО
R-4 2026-10-17 Пустая строка между пунктами списка одного вида не рвёт список Так выглядит шаблон ошибок AURORA ( * пункт, пустая строка, * пункт); один список читается лучше трёх Два задуманных отдельными списка сливаются в один
R-5 2026-10-17 Свёрнутое описание в режиме рендера ограничивается высотой ≈ 8 строк, а не числом строк -webkit-line-clamp не работает на блочном содержимом Свёрнутая часть немного выше или ниже 8 строк
R-6 2026-10-17 Подсветка синтаксиса {code}, макросы {panel}, {color}, картинки, эмодзи, типографика -- — вне change Утверждённый объём; неразобранное видно текстом Эти конструкции видны исходником
R-7 2026-10-17 Открывающий маркер выделения, за которым тот же символ (**, __, --), не считается маркером Markdown **жирный** из описаний, написанных через MCP, при буквальном применении правил Jira разобрался бы в «жирное *x и *» — хуже исходника; Jira-разметка двойных маркеров не использует **x** видно со звёздочками (как и в Markdown-исходнике)
R-8 2026-10-17 Блочные макросы {code} / {noformat} / {quote} распознаются только с открывающим тегом в начале строки; закрывающий — где угодно дальше; остаток строки после него — обычный текст Однострочный {quote} посреди фразы в задачах AURORA не встречался; построчный разбор проще и предсказуемее Такой {quote} виден исходником
R-9 2026-10-17 Селектор .issue .comments ul (список комментариев без маркеров, flex) сужен до .issue .comments>ul Иначе списки разметки внутри комментариев теряли маркеры и раскладывались flex-ом Нет: список комментариев — прямой потомок раздела
R-10 2026-10-17 Тесты свёртки по измерению подменяют scrollHeight/clientHeight (vi.spyOn(Element.prototype, …)) В jsdom высоты нулевые; прежний тест свёртки опирался на счёт строк исходника, а в режиме рендера признак — только измерение (design #8) Нет: поведение в браузере проверяет e2e
R-11 2026-10-17 В e2e узкого экрана проверяется ширина окна карточки, а не document.scrollWidth; таблица разметки на телефоне — отдельный e2e на 375 px (после I-1) Страница под модальным окном шире экрана и до этого change (931 > 600 px), прокрутка под окном заблокирована Ширина страницы под окном не проверяется — прежнее поведение
R-12 2026-10-17 Перемер переполнения описания — ResizeObserver на контейнере и его первом ребёнке, пока описание свёрнуто (C-1) Карточка из кэша монтируется до showModal() с нулевыми размерами; ресайз окна и догрузка шрифтов тоже меняют высоту Без ResizeObserver (старый браузер) — прежний однократный замер
R-13 2026-10-17 Подпись ссылки `[подпись адрес]` разбирается без ссылок, голых адресов и ключей задач (находка ревью M-1, повышена до I-2) `[AURORA-1445