This commit is contained in:
2025-06-19 13:40:31 +02:00
parent 24d03302b6
commit 60b66ef57c
9 changed files with 138 additions and 28 deletions

10
package-lock.json generated
View File

@@ -20,6 +20,7 @@
"dompurify": "^3.0.9",
"electron": "^29.1.0",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
"jsdom": "^24.0.0",
"marked": "^12.0.0",
"next": "14.1.0",
@@ -3897,6 +3898,15 @@
"node": ">= 0.4"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",

View File

@@ -11,14 +11,19 @@
"electron-dev": "concurrently \"npm run dev\" \"npm run electron\""
},
"dependencies": {
"@tailwindcss/typography": "^0.5.16",
"@types/node": "^20.11.24",
"@types/react": "^18.2.61",
"@types/react-dom": "^18.2.19",
"autoprefixer": "^10.4.17",
"bcrypt": "^5.0.2",
"bcryptjs": "^2.4.3",
"chokidar": "^3.6.0",
"date-fns": "^3.6.0",
"dompurify": "^3.0.9",
"electron": "^29.1.0",
"gray-matter": "^4.0.3",
"highlight.js": "^11.11.1",
"jsdom": "^24.0.0",
"marked": "^12.0.0",
"next": "14.1.0",
@@ -26,19 +31,15 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"date-fns": "^3.6.0",
"@tailwindcss/typography": "^0.5.16",
"bcryptjs": "^2.4.3",
"bcrypt": "^5.0.2"
"typescript": "^5.3.3"
},
"devDependencies": {
"@types/bcryptjs": "^2.4.6",
"@types/dompurify": "^3.0.5",
"@types/jsdom": "^21.1.6",
"@types/marked": "^5.0.2",
"concurrently": "^8.2.2",
"eslint": "^8.57.0",
"eslint-config-next": "14.1.0",
"@types/bcryptjs": "^2.4.6"
"eslint-config-next": "14.1.0"
}
}

View File

@@ -94,6 +94,14 @@ print("Hello, world!")
```
### Code Blog (test)
```Python
def hello():
return true;
```
### Code Block (indented)
```Python
def indented_example():

View File

@@ -3,6 +3,8 @@
import { useState, useEffect, useCallback, useRef } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { marked } from 'marked';
import hljs from 'highlight.js';
interface Post {
slug: string;
@@ -67,6 +69,7 @@ export default function AdminPage() {
const [changePwNew, setChangePwNew] = useState('');
const [changePwConfirm, setChangePwConfirm] = useState('');
const [changePwFeedback, setChangePwFeedback] = useState<string | null>(null);
const [previewHtml, setPreviewHtml] = useState('');
const router = useRouter();
const usernameRef = useRef<HTMLInputElement>(null);
const passwordRef = useRef<HTMLInputElement>(null);
@@ -85,6 +88,20 @@ export default function AdminPage() {
useEffect(() => {
localStorage.setItem('pinnedPosts', JSON.stringify(pinned));
}, [pinned]);
useEffect(() => {
marked.setOptions({
gfm: true,
breaks: true,
highlight: function(code: string, lang: string) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
} else {
return hljs.highlightAuto(code).value;
}
}
} as any);
setPreviewHtml(marked.parse(newPost.content || '') as string);
}, [newPost.content]);
const loadContent = async () => {
try {
@@ -659,13 +676,21 @@ export default function AdminPage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700">Content (Markdown)</label>
<textarea
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 font-mono"
rows={10}
required
/>
<div className="flex flex-col md:flex-row gap-4 mt-1">
{/* Markdown Editor */}
<textarea
value={newPost.content}
onChange={(e) => setNewPost({ ...newPost, content: e.target.value })}
className="w-full md:w-1/2 rounded-md border border-gray-300 px-3 py-2 font-mono min-h-[200px] md:min-h-[300px] resize-y"
rows={10}
required
/>
{/* Live Markdown Preview */}
<div className="w-full md:w-1/2 p-4 border rounded bg-gray-50 overflow-auto min-h-[200px] md:min-h-[300px]">
<div className="text-xs text-gray-500 mb-2">Live Preview</div>
<div className="prose prose-sm max-w-none" dangerouslySetInnerHTML={{ __html: previewHtml }} />
</div>
</div>
</div>
<button
type="submit"

View File

@@ -7,9 +7,26 @@ import matter from 'gray-matter';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
import hljs from 'highlight.js';
const postsDirectory = path.join(process.cwd(), 'posts');
const renderer = new marked.Renderer();
renderer.code = (code, infostring, escaped) => {
const lang = (infostring || '').match(/\S*/)?.[0];
const highlighted = lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
: hljs.highlightAuto(code).value;
const langClass = lang ? `language-${lang}` : '';
return `<pre><code class="hljs ${langClass}">${highlighted}</code></pre>`;
};
marked.setOptions({
gfm: true,
breaks: true,
renderer,
});
// Function to get file creation date
function getFileCreationDate(filePath: string): Date {
const stats = fs.statSync(filePath);
@@ -25,12 +42,6 @@ async function getPostBySlug(slug: string) {
let processedContent = '';
try {
// Configure marked options
marked.setOptions({
gfm: true,
breaks: true
});
// Convert markdown to HTML
const rawHtml = marked.parse(content);

View File

@@ -7,6 +7,7 @@ import matter from 'gray-matter';
import { marked } from 'marked';
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
import hljs from 'highlight.js';
const postsDirectory = path.join(process.cwd(), 'posts');
@@ -26,6 +27,22 @@ function getFileCreationDate(filePath: string): Date {
return stats.birthtime ?? stats.mtime;
}
const renderer = new marked.Renderer();
renderer.code = (code, infostring, escaped) => {
const lang = (infostring || '').match(/\S*/)?.[0];
const highlighted = lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
: hljs.highlightAuto(code).value;
const langClass = lang ? `language-${lang}` : '';
return `<pre><code class="hljs ${langClass}">${highlighted}</code></pre>`;
};
marked.setOptions({
gfm: true,
breaks: true,
renderer,
});
async function getPostByPath(filePath: string, relPath: string) {
const fileContents = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContents);
@@ -33,10 +50,6 @@ async function getPostByPath(filePath: string, relPath: string) {
let processedContent = '';
try {
marked.setOptions({
gfm: true,
breaks: true
});
const rawHtml = marked.parse(content);
const window = new JSDOM('').window;
const purify = DOMPurify(window);

View File

@@ -1,3 +1,4 @@
@import './highlight-github.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@@ -24,4 +25,20 @@ body {
.prose a:hover {
color: #1d4ed8;
}
/* Ensure highlight.js styles override Tailwind prose for code blocks */
.prose pre code.hljs {
background: inherit !important;
color: inherit !important;
padding: 0;
font-size: inherit;
/* Remove Tailwind's border radius if you want the highlight.js look */
border-radius: 0;
}
.prose pre {
background: #292d3e !important;
color: #fff !important;
border-radius: 0.5em;
overflow-x: auto;
}

View File

@@ -0,0 +1,12 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: GitHub
Description: Light theme as seen on github.com
Author: github.com
Maintainer: @Hirse
Updated: 2021-05-15
Outdated base version: https://github.com/primer/github-syntax-light
Current colors taken from GitHub's CSS
*/.hljs{color:#24292e;background:#fff}.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-template-tag,.hljs-template-variable,.hljs-type,.hljs-variable.language_{color:#d73a49}.hljs-title,.hljs-title.class_,.hljs-title.class_.inherited__,.hljs-title.function_{color:#6f42c1}.hljs-attr,.hljs-attribute,.hljs-literal,.hljs-meta,.hljs-number,.hljs-operator,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-variable{color:#005cc5}.hljs-meta .hljs-string,.hljs-regexp,.hljs-string{color:#032f62}.hljs-built_in,.hljs-symbol{color:#e36209}.hljs-code,.hljs-comment,.hljs-formula{color:#6a737d}.hljs-name,.hljs-quote,.hljs-selector-pseudo,.hljs-selector-tag{color:#22863a}.hljs-subst{color:#24292e}.hljs-section{color:#005cc5;font-weight:700}.hljs-bullet{color:#735c0f}.hljs-emphasis{color:#24292e;font-style:italic}.hljs-strong{color:#24292e;font-weight:700}.hljs-addition{color:#22863a;background-color:#f0fff4}.hljs-deletion{color:#b31d28;background-color:#ffeef0}.hljs-string, .hljs-meta .hljs-string {
color: #b2d485 !important;
}

View File

@@ -6,6 +6,7 @@ import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';
import chokidar from 'chokidar';
import type { FSWatcher } from 'chokidar';
import hljs from 'highlight.js';
export interface Post {
slug: string;
@@ -26,6 +27,22 @@ function getFileCreationDate(filePath: string): Date {
return stats.birthtime;
}
const renderer = new marked.Renderer();
renderer.code = (code, infostring, escaped) => {
const lang = (infostring || '').match(/\S*/)?.[0];
const highlighted = lang && hljs.getLanguage(lang)
? hljs.highlight(code, { language: lang }).value
: hljs.highlightAuto(code).value;
const langClass = lang ? `language-${lang}` : '';
return `<pre><code class="hljs ${langClass}">${highlighted}</code></pre>`;
};
marked.setOptions({
gfm: true,
breaks: true,
renderer,
});
export async function getPostBySlug(slug: string): Promise<Post> {
const realSlug = slug.replace(/\.md$/, '');
const fullPath = path.join(postsDirectory, `${realSlug}.md`);
@@ -35,10 +52,6 @@ export async function getPostBySlug(slug: string): Promise<Post> {
let processedContent = '';
try {
marked.setOptions({
gfm: true,
breaks: true
});
const rawHtml = marked.parse(content);
const window = new JSDOM('').window;
const purify = DOMPurify(window);