diff --git a/package-lock.json b/package-lock.json index 892ac05..e29fdd1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "emoji-picker-react": "^4.12.2", "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", + "isomorphic-dompurify": "^2.25.0", "jsdom": "^24.0.0", "marked": "^12.0.0", "monaco-editor": "^0.52.2", @@ -1212,19 +1213,6 @@ "parse5": "^7.0.0" } }, - "node_modules/@types/jsdom/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -5183,6 +5171,92 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, + "node_modules/isomorphic-dompurify": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.25.0.tgz", + "integrity": "sha512-bcpJzu9DOjN21qaCVpcoCwUX1ytpvA6EFqCK5RNtPg5+F0Jz9PX50jl6jbEicBNeO87eDDfC7XtPs4zjDClZJg==", + "license": "MIT", + "dependencies": { + "dompurify": "^3.2.6", + "jsdom": "^26.1.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/isomorphic-dompurify/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/isomorphic-dompurify/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/isomorphic-dompurify/node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -5327,18 +5401,6 @@ "node": ">= 14" } }, - "node_modules/jsdom/node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/jsdom/node_modules/rrweb-cssom": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", @@ -6314,6 +6376,18 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -8246,6 +8320,24 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "license": "MIT" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/package.json b/package.json index b324d03..c234607 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "emoji-picker-react": "^4.12.2", "gray-matter": "^4.0.3", "highlight.js": "^11.11.1", + "isomorphic-dompurify": "^2.25.0", "jsdom": "^24.0.0", "marked": "^12.0.0", "monaco-editor": "^0.52.2", diff --git a/src/app/admin/MonacoEditor.tsx b/src/app/admin/MonacoEditor.tsx new file mode 100644 index 0000000..f8c2137 --- /dev/null +++ b/src/app/admin/MonacoEditor.tsx @@ -0,0 +1,12 @@ +import Editor from "@monaco-editor/react"; + +export default function MonacoEditorWrapper(props: any) { + return ( + + ); +} \ No newline at end of file diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 1e0f452..25cd903 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -1,9 +1,15 @@ 'use client'; +export const dynamic = "force-dynamic"; /********************************************* * This is the main admin page for the blog. * * Written Jun 19 2025 +* Rewritten fucking 15 times cause of the +* fucking +* typescript linter. +* +* If any Issues about "Window" (For Monaco) pop up. Its not my fucking fault **********************************************/ import { useState, useEffect, useCallback, useRef } from 'react'; @@ -12,17 +18,21 @@ import Link from 'next/link'; import { marked } from 'marked'; import hljs from 'highlight.js'; import matter from 'gray-matter'; -import dynamic from 'next/dynamic'; +import dynamicImport from 'next/dynamic'; import { Theme } from 'emoji-picker-react'; import '../highlight-github.css'; -import MonacoEditor from '@monaco-editor/react'; -import { initVimMode, VimMode } from 'monaco-vim'; +const MonacoEditor = dynamicImport(() => import('./MonacoEditor'), { ssr: false }); +// Import monaco-vim only on client side +let initVimMode: any = null; +let VimMode: any = null; + +if (typeof window !== 'undefined') { + const monacoVim = require('monaco-vim'); + initVimMode = monacoVim.initVimMode; + VimMode = monacoVim.VimMode; +} import '@fontsource/jetbrains-mono'; -// @ts-ignore-next-line -// eslint-disable-next-line -// If you want, you can move this to a global.d.ts file -// declare module 'monaco-vim'; interface Post { slug: string; @@ -55,7 +65,7 @@ interface Post { type Node = Post | Folder; -const EmojiPicker = dynamic(() => import('emoji-picker-react'), { ssr: false }); +const EmojiPicker = dynamicImport(() => import('emoji-picker-react'), { ssr: false }); // Patch marked renderer to always add 'hljs' class to code blocks const renderer = new marked.Renderer(); @@ -89,12 +99,7 @@ export default function AdminPage() { }); const [showManageContent, setShowManageContent] = useState(false); const [managePath, setManagePath] = useState([]); - const [pinned, setPinned] = useState(() => { - if (typeof window !== 'undefined') { - return JSON.parse(localStorage.getItem('pinnedPosts') || '[]'); - } - return []; - }); + const [pinned, setPinned] = useState([]); const [pinFeedback, setPinFeedback] = useState(null); const [showChangePassword, setShowChangePassword] = useState(false); const [changePwOld, setChangePwOld] = useState(''); @@ -104,12 +109,7 @@ export default function AdminPage() { const [previewHtml, setPreviewHtml] = useState(''); const [editingPost, setEditingPost] = useState<{ slug: string, path: string } | null>(null); const [isDocker, setIsDocker] = useState(false); - const [rememberExportChoice, setRememberExportChoice] = useState(() => { - if (typeof window !== 'undefined') { - return localStorage.getItem('rememberExportChoice') === 'true'; - } - return false; - }); + const [rememberExportChoice, setRememberExportChoice] = useState(false); const [lastExportChoice, setLastExportChoice] = useState(null); const [emojiPickerOpen, setEmojiPickerOpen] = useState(null); const [emojiPickerAnchor, setEmojiPickerAnchor] = useState(null); @@ -536,15 +536,7 @@ export default function AdminPage() { }; function handleExportTarball() { - // Check if we should use the remembered choice - if (rememberExportChoice && lastExportChoice) { - if (lastExportChoice === 'docker') { - exportFromEndpoint('/api/admin/export'); - } else if (lastExportChoice === 'local') { - exportFromEndpoint('/api/admin/exportlocal'); - } - return; - } + if (typeof window === 'undefined') return; // Create popup modal const modal = document.createElement('div'); @@ -637,6 +629,7 @@ export default function AdminPage() { } function exportFromEndpoint(endpoint: string) { + if (typeof window === 'undefined') return; fetch(endpoint) .then(async (res) => { if (!res.ok) throw new Error('Export failed'); @@ -660,6 +653,15 @@ export default function AdminPage() { setLastExportChoice(null); }; + // Hydrate pinned, rememberExportChoice, lastExportChoice from localStorage on client only + useEffect(() => { + if (typeof window !== 'undefined') { + setPinned(JSON.parse(localStorage.getItem('pinnedPosts') || '[]')); + setRememberExportChoice(localStorage.getItem('rememberExportChoice') === 'true'); + setLastExportChoice(localStorage.getItem('lastExportChoice')); + } + }, []); + // Simple and reliable emoji update handler const handleSetFolderEmoji = async (folderPath: string, emoji: string) => { try { @@ -772,7 +774,7 @@ export default function AdminPage() { // Attach/detach Vim mode when vimMode changes useEffect(() => { - if (vimMode && monacoRef.current) { + if (vimMode && monacoRef.current && initVimMode) { // @ts-ignore vimInstanceRef.current = initVimMode(monacoRef.current, vimStatusRef.current); } else if (vimInstanceRef.current) { @@ -917,7 +919,7 @@ export default function AdminPage() { - + @@ -1182,7 +1184,7 @@ export default function AdminPage() { height="100%" defaultLanguage="markdown" value={newPost.content} - onChange={(value) => setNewPost({ ...newPost, content: value || '' })} + onChange={(value?: string) => setNewPost({ ...newPost, content: value || '' })} options={{ minimap: { enabled: false }, wordWrap: 'on', @@ -1193,7 +1195,7 @@ export default function AdminPage() { automaticLayout: true, fontFamily: 'JetBrains Mono, monospace', }} - onMount={(editor) => { + onMount={(editor: any) => { monacoRef.current = editor; }} /> diff --git a/src/app/api/posts/route.ts b/src/app/api/posts/route.ts index 123c9f8..1df2bc9 100644 --- a/src/app/api/posts/route.ts +++ b/src/app/api/posts/route.ts @@ -5,8 +5,7 @@ import fs from 'fs'; import path from 'path'; import matter from 'gray-matter'; import { marked } from 'marked'; -import DOMPurify from 'dompurify'; -import { JSDOM } from 'jsdom'; +import createDOMPurify from 'isomorphic-dompurify'; import hljs from 'highlight.js'; import { getPostsDirectory } from '@/lib/postsDirectory'; @@ -106,10 +105,8 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi let processedContent = ''; try { - const rawHtml = marked.parse(content); - const window = new JSDOM('').window; - const purify = DOMPurify(window); - processedContent = purify.sanitize(rawHtml as string, { + const rawHtml = marked.parse(content) as string; + processedContent = createDOMPurify.sanitize(rawHtml, { ALLOWED_TAGS: [ 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'a', 'ul', 'ol', 'li', 'blockquote', @@ -123,7 +120,7 @@ async function getPostByPath(filePath: string, relPath: string, pinnedData: { pi 'src', 'alt', 'title', 'width', 'height', 'frameborder', 'allowfullscreen' ], - ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i + ALLOWED_URI_REGEXP: /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|cid|xmpp):|[^a-z]|[a-z+\.\-]+(?:[^a-z+\.\-:]|$))/i }); } catch (err) { console.error(`Error processing markdown for ${relPath}:`, err);