import React, { useEffect, useState } from "react"; import CodeMirror from "@uiw/react-codemirror"; import { gutter, GutterMarker } from "@codemirror/view"; import { markdown } from "@codemirror/lang-markdown"; import { html } from "@codemirror/lang-html"; import { json } from "@codemirror/lang-json"; import MarkdownIt from "markdown-it"; import MarkdownItTaskLists from "markdown-it-task-lists"; import { FetchBlame, FetchFile, GetAvatarUrl, GetFileUrl, GetRawFileUrl, SaveFile } from "../Api.js"; const MarkdownRenderer = new MarkdownIt({ breaks: true }).use(MarkdownItTaskLists, { enabled: true, label: true, }); const DefaultImageRenderer = MarkdownRenderer.renderer.rules.image || ((Tokens, Index, Options, Env, Self) => Self.renderToken(Tokens, Index, Options)); MarkdownRenderer.renderer.rules.image = (Tokens, Index, Options, Env, Self) => { const Token = Tokens[Index]; const SrcAttrIndex = Token.attrIndex("src"); if (SrcAttrIndex >= 0 && Env && Env.ResolveImageSrc) { Token.attrs[SrcAttrIndex][1] = Env.ResolveImageSrc(Token.attrs[SrcAttrIndex][1]); } return DefaultImageRenderer(Tokens, Index, Options, Env, Self); }; const TextTypes = ["markdown", "text", "config", "html"]; const LinkedFileTypeMap = { ".md": "markdown", ".markdown": "markdown", ".txt": "text", ".toml": "config", ".yaml": "config", ".yml": "config", ".json": "config", ".html": "html", ".htm": "html", ".png": "image", ".jpg": "image", ".jpeg": "image", ".gif": "image", ".webp": "image", ".svg": "image", ".mp4": "video", ".webm": "video", ".mov": "video", ".mkv": "video", }; function ClassifyLinkedFile(LinkedPath) { const DotIndex = LinkedPath.lastIndexOf("."); const Extension = DotIndex === -1 ? "" : LinkedPath.slice(DotIndex).toLowerCase(); return LinkedFileTypeMap[Extension] || "unknown"; } function ResolveRelativeLink(CurrentFilePath, Href) { if (/^([a-z][a-z0-9+.-]*:)?\/\//i.test(Href) || Href.startsWith("#") || Href.startsWith("mailto:")) { return null; } const CurrentDirectory = CurrentFilePath.includes("/") ? CurrentFilePath.slice(0, CurrentFilePath.lastIndexOf("/")) : ""; const Combined = CurrentDirectory ? `${CurrentDirectory}/${Href}` : Href; const ResolvedParts = []; for (const Part of Combined.split("/")) { if (Part === "" || Part === ".") continue; if (Part === "..") { ResolvedParts.pop(); } else { ResolvedParts.push(Part); } } return ResolvedParts.join("/"); } function GetLanguageExtension(FileType) { if (FileType === "markdown") return [markdown()]; if (FileType === "html") return [html()]; if (FileType === "config") return [json()]; return []; } class BlameMarker extends GutterMarker { constructor(Mode, Content, Colour, Title) { super(); this.Mode = Mode; this.Content = Content; this.Colour = Colour; this.Title = Title; } eq(Other) { return ( this.Mode === Other.Mode && this.Content === Other.Content && this.Colour === Other.Colour && this.Title === Other.Title ); } toDOM() { if (this.Mode === "avatar") { const Image = document.createElement("img"); Image.className = "BlameGutterAvatar"; Image.src = this.Content; Image.title = this.Title; Image.alt = ""; return Image; } const Element = document.createElement("span"); Element.className = `BlameGutterMarker${this.Mode === "hidden" ? " BlameGutterHidden" : ""}`; Element.textContent = this.Content; Element.title = this.Title; if (this.Colour) { Element.style.color = `var(--${this.Colour})`; } return Element; } } function BuildBlameGutter(BlameMap, ShowAvatars) { return gutter({ class: "BlameGutter", lineMarker(view, line) { const LineNumber = view.state.doc.lineAt(line.from).number; const Entry = BlameMap[String(LineNumber)]; if (!Entry) return null; if (Entry.Hidden) { const Title = Entry.Timestamp ? new Date(Entry.Timestamp).toLocaleString() : ""; return new BlameMarker("hidden", "•••", null, Title); } const Title = `${Entry.Username} - ${new Date(Entry.Timestamp).toLocaleString()}`; if (ShowAvatars && Entry.HasAvatar) { return new BlameMarker("avatar", GetAvatarUrl(Entry.Username), null, Title); } return new BlameMarker("text", Entry.Username.slice(0, 6), Entry.Colour, Title); }, }); } export default function PreviewPane({ RootName, FilePath, FileType, OnNavigateLink, ShowGitBlame, ShowBlameAvatars, StickyToolbar, }) { const [Content, SetContent] = useState(""); const [IsEditing, SetIsEditing] = useState(false); const [IsSaving, SetIsSaving] = useState(false); const [SaveError, SetSaveError] = useState(null); const [BlameMap, SetBlameMap] = useState({}); useEffect(() => { SetIsEditing(false); SetSaveError(null); SetBlameMap({}); if (FilePath && TextTypes.includes(FileType)) { FetchFile(RootName, FilePath).then(SetContent); if (ShowGitBlame) { FetchBlame(RootName, FilePath) .then(SetBlameMap) .catch(() => SetBlameMap({})); } } }, [RootName, FilePath, FileType, ShowGitBlame]); if (!FilePath) { return
{Content}