import { useMemo, type ComponentProps, type ReactNode } from "react"; import Markdown, { type Components } from "react-markdown"; import { Link } from "react-router"; import remarkGfm from "remark-gfm"; import { Notice, IconExternal, IconPaperclip } from "../../ui/index.ts"; import { fmtStamp } from "../jira/board/model/util.ts"; import { useWikiPage } from "./api/queries.ts"; import { useWikiDownload } from "./download.ts"; import { wikiUrlTransform } from "./urlTransform.ts"; const PLUGINS = [remarkGfm]; const safeUrl = (u: string | null) => (u && /^https?:\/\//i.test(u.trim()) ? u : null); // копия начинается заголовком «# название» — он повторяет заголовок страницы и в тексте не нужен function withoutTitle(md: string, title: string | null): string { const m = /^# ([^\n]*)\n+/.exec(md); return m && title !== null && m[1].trim() === title.split(/\s+/).filter(Boolean).join(" ") ? md.slice(m[0].length) : md; } const decode = (s: string) => { try { return decodeURIComponent(s); } catch { return s; } }; // страница вики: заголовок, дата синхронизации, «открыть в Eva» и Markdown (таблицы GFM, без сырого HTML) export function PageView({ project, code, hrefOf }: { project: string; code: string; hrefOf(code: string): string }) { const page = useWikiPage(project, code); const dl = useWikiDownload(project, code); const components = useMemo(() => ({ a({ href, children }: ComponentProps<"a">) { if (!href) return <>{children}; if (/^page:/i.test(href)) return {children}; if (/^attachment:/i.test(href)) return {children}; if (/^https?:/i.test(href)) return {children}; return {children}; }, // картинки по внешним адресам не загружаются (трекинг, чужие ресурсы): виден только текст img: ({ alt }: ComponentProps<"img">) => [изображение{alt ? `: ${alt}` : ""}], table: ({ children }: ComponentProps<"table">) =>
{children}
, }), [hrefOf, dl]); if (page.isPending) return

Загрузка страницы…

; if (page.isError) return {page.error.message}; const d = page.data; const eva = safeUrl(d.url); return (

{d.title || d.code}

{d.code} {d.synced_at && синхронизировано {fmtStamp(d.synced_at)}} {eva && открыть в Eva}

{withoutTitle(d.markdown, d.title)}
); } // ссылка attachment:: запрос на скачивание, ожидание, сохранение файла; ошибка — текстом рядом function AttachmentLink({ id, dl, children }: { id: string; dl: ReturnType; children?: ReactNode }) { const { busy, error } = dl.status(id); return ( <> {error && {error}} ); }