diff --git a/package-lock.json b/package-lock.json index c3d0a73..9db557f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,8 @@ "dompurify": "^3.2.7", "dotenv": "^16.4.5", "express": "^4.19.2", + "github-markdown-css": "^5.9.0", + "highlight.js": "^11.11.1", "jose": "^5.9.3", "katex": "^0.16.22", "lucide-react": "^0.546.0", @@ -2013,6 +2015,18 @@ "node": ">= 0.4" } }, + "node_modules/github-markdown-css": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/github-markdown-css/-/github-markdown-css-5.9.0.tgz", + "integrity": "sha512-tmT5sY+zvg2302XLYEfH2mtkViIM1SWf2nvYoF5N1ZsO0V6B2qZTiw3GOzw4vpjLygK/KG35qRlPFweHqfzz5w==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -2049,6 +2063,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", diff --git a/package.json b/package.json index 8109812..9fd4004 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "dompurify": "^3.2.7", "dotenv": "^16.4.5", "express": "^4.19.2", + "github-markdown-css": "^5.9.0", + "highlight.js": "^11.11.1", "jose": "^5.9.3", "katex": "^0.16.22", "lucide-react": "^0.546.0", diff --git a/src/lib/render-markdown.ts b/src/lib/render-markdown.ts index 466ee3a..4b03610 100644 --- a/src/lib/render-markdown.ts +++ b/src/lib/render-markdown.ts @@ -4,6 +4,7 @@ import type { TokenizerAndRendererExtension } from 'marked'; import DOMPurify from 'dompurify'; import katex from 'katex'; import 'katex/dist/katex.min.css'; +import hljs from 'highlight.js'; export type { MarkdownRenderMode }; export { renderMarkdown }; @@ -123,6 +124,8 @@ function configureMarkedOnce() { }, tokenizer(src) { if (src[0] !== '$' || src[1] === '$') return undefined; + // Don't match currency: $ followed by a digit or comma (e.g. $1,200) + if (src[1] !== undefined && /[\d,]/.test(src[1])) return undefined; let index = 1; let closing = -1; while (index < src.length) { @@ -159,7 +162,19 @@ function configureMarkedOnce() { } }, }; - marked.use({ extensions: [blockMathExtension, inlineMathExtension] }); + // Syntax-highlight fenced code blocks using highlight.js. + // Falls back to plain text for unknown languages. + const codeRenderer = { + code(code: string, infostring: string | undefined): string { + let lang = (infostring ?? '').split(/\s/)[0] ?? ''; + let valid = lang !== '' && hljs.getLanguage(lang) !== undefined; + let highlighted = (valid && lang !== '') + ? hljs.highlight(code, { language: lang as string }).value + : hljs.highlightAuto(code).value; + return `
${highlighted}
\n`; + }, + }; + marked.use({ extensions: [blockMathExtension, inlineMathExtension], renderer: codeRenderer }); markedConfigured = true; } diff --git a/src/main.tsx b/src/main.tsx index 95bb6e6..40d5459 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App'; import './styles.css'; +import 'github-markdown-css/github-markdown-light.css'; +import 'highlight.js/styles/github.css'; const root = createRoot(document.getElementById('root')!); root.render(); diff --git a/src/share/ShareApp.tsx b/src/share/ShareApp.tsx index 338f60f..62d5fa4 100644 --- a/src/share/ShareApp.tsx +++ b/src/share/ShareApp.tsx @@ -1,6 +1,8 @@ // Public share viewer entry that resolves the share ref and renders the note. import React, { useEffect, useMemo, useState } from 'react'; import { renderMarkdown } from '../lib/render-markdown'; +import 'github-markdown-css/github-markdown-light.css'; +import 'highlight.js/styles/github.css'; import { fetchShareContent, fetchShareMeta, buildAssetUrl, parseShareUrl, type ShareRef, type ShareMetaResponse } from './api'; type ViewerState = @@ -121,7 +123,7 @@ export function ShareApp() {

-
+
diff --git a/src/share/share.css b/src/share/share.css index 7b76e05..b49dc6b 100644 --- a/src/share/share.css +++ b/src/share/share.css @@ -2,7 +2,7 @@ body { margin: 0; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; - background: #f6f8fa; + background: #ffffff; color: #0f172a; } @@ -39,23 +39,18 @@ body { .share-main { flex: 1; - padding: clamp(16px, 5vw, 32px); + padding: clamp(24px, 5vw, 48px) clamp(16px, 8vw, 56px); display: flex; justify-content: center; align-items: flex-start; } .share-article { - max-width: clamp(720px, 70vw, 960px); + max-width: 861px; width: 100%; - background: #ffffff; - border: 1px solid #d0d7de; - border-radius: 20px; - padding: clamp(20px, 3.5vw, 32px); - box-shadow: 0 12px 32px rgba(15, 23, 42, 0.08); display: flex; flex-direction: column; - gap: clamp(20px, 4vw, 32px); + gap: 24px; } .share-article-header { @@ -89,40 +84,17 @@ body { display: block; } -.share-article img { - max-width: 100%; - display: block; - margin: 16px auto; - border-radius: 12px; -} - -.share-article a { - color: #0969da; - text-decoration: none; -} - -.share-article a:hover { - text-decoration: underline; +/* Fit github-markdown-css inside our card — neutralise its own box model */ +.share-content.markdown-body { + background: transparent; + font-family: inherit; + font-size: 1rem; + color: inherit; } @media (max-width: 600px) { .share-main { - padding: 0; - justify-content: flex-start; - align-items: stretch; - } - - .share-article { - max-width: none; - border-radius: 0; - border: none; - box-shadow: none; - min-height: 100vh; - box-sizing: border-box; - } - - .share-content { - flex: 1; + padding: 16px; } } diff --git a/src/styles.css b/src/styles.css index e0e27c0..c774b9d 100644 --- a/src/styles.css +++ b/src/styles.css @@ -878,53 +878,14 @@ textarea:focus-visible { .workspace-panels .preview { overflow: auto; - line-height: 1.68; - color: var(--text); -} - -.workspace-panels .preview h1, -.workspace-panels .preview h2, -.workspace-panels .preview h3, -.workspace-panels .preview h4 { - margin-top: 1.5em; - margin-bottom: 0.75em; - line-height: 1.25; -} - -.workspace-panels .preview h1 { - font-size: 2rem; -} - -.workspace-panels .preview h2 { - font-size: 1.6rem; -} - -.workspace-panels .preview h3 { - font-size: 1.3rem; -} - -.workspace-panels .preview p { - margin: 0 0 1em; } -.workspace-panels .preview pre { - background: var(--surface-subtle); - border-radius: 10px; - padding: 12px; - border: 1px solid var(--border); - overflow: auto; -} - -.workspace-panels .preview code { - font: var(--code-font); -} - -.workspace-panels .preview blockquote { - border-left: 4px solid var(--accent-border); - margin: 0 0 1em; - padding: 0.2em 1em; - background: rgba(9, 105, 218, 0.08); - border-radius: 0 8px 8px 0; +/* github-markdown-css provides all content styles; override only the + properties that would conflict with the card layout. */ +.workspace-panels .preview.markdown-body { + font-family: inherit; + font-size: 1rem; + color: inherit; } .empty-state { diff --git a/src/ui/Editor.tsx b/src/ui/Editor.tsx index 811e1f2..ec28a77 100644 --- a/src/ui/Editor.tsx +++ b/src/ui/Editor.tsx @@ -128,7 +128,7 @@ export function Editor({ doc, onChange, readOnly = false, slug, loadAsset, onImp )}