Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

markdown.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

markdown.tsBlame133 lines · 1 contributor
79136bbClaude1/**
2 * Markdown rendering — server-side, sanitized.
3 */
4
5import { Marked } from "marked";
6import { highlightCode } from "./highlight";
7
8const marked = new Marked({
9 gfm: true,
10 breaks: false,
11 renderer: {
12 code({ text, lang }) {
13 if (lang) {
14 const { html } = highlightCode(text, `code.${lang}`);
15 return `<pre><code class="language-${escapeAttr(lang)}">${html}</code></pre>`;
16 }
17 return `<pre><code>${escapeHtml(text)}</code></pre>`;
18 },
19 link({ href, title, text }) {
20 // Sanitize: only allow http(s) and relative links
21 const safeHref =
22 href.startsWith("http://") ||
23 href.startsWith("https://") ||
24 href.startsWith("/") ||
25 href.startsWith("#") ||
26 href.startsWith(".")
27 ? href
28 : "#";
29 const titleAttr = title ? ` title="${escapeAttr(title)}"` : "";
30 return `<a href="${escapeAttr(safeHref)}"${titleAttr}>${text}</a>`;
31 },
32 image({ href, title, text }) {
33 const titleAttr = title ? ` title="${escapeAttr(title)}"` : "";
34 return `<img src="${escapeAttr(href)}" alt="${escapeAttr(text)}"${titleAttr} style="max-width: 100%;" loading="lazy" />`;
35 },
36 },
37});
38
39export function renderMarkdown(source: string): string {
40 const html = marked.parse(source);
41 if (typeof html !== "string") return "";
42 return html;
43}
44
45function escapeHtml(str: string): string {
46 return str
47 .replace(/&/g, "&amp;")
48 .replace(/</g, "&lt;")
49 .replace(/>/g, "&gt;")
50 .replace(/"/g, "&quot;");
51}
52
53function escapeAttr(str: string): string {
54 return str
55 .replace(/&/g, "&amp;")
56 .replace(/"/g, "&quot;")
57 .replace(/'/g, "&#39;")
58 .replace(/</g, "&lt;")
59 .replace(/>/g, "&gt;");
60}
61
62export const markdownCss = `
63 .markdown-body {
64 font-size: 15px;
65 line-height: 1.7;
66 word-wrap: break-word;
67 padding: 20px;
68 }
69 .markdown-body h1, .markdown-body h2, .markdown-body h3,
70 .markdown-body h4, .markdown-body h5, .markdown-body h6 {
71 margin-top: 24px;
72 margin-bottom: 16px;
73 font-weight: 600;
74 line-height: 1.25;
75 padding-bottom: 8px;
76 border-bottom: 1px solid var(--border);
77 }
78 .markdown-body h1 { font-size: 2em; }
79 .markdown-body h2 { font-size: 1.5em; }
80 .markdown-body h3 { font-size: 1.25em; border-bottom: none; }
81 .markdown-body h4 { font-size: 1em; border-bottom: none; }
82 .markdown-body p { margin-bottom: 16px; }
83 .markdown-body ul, .markdown-body ol { padding-left: 2em; margin-bottom: 16px; }
84 .markdown-body li { margin-bottom: 4px; }
85 .markdown-body code {
86 font-family: var(--font-mono);
87 font-size: 85%;
88 padding: 2px 6px;
89 background: var(--bg-tertiary);
90 border-radius: 3px;
91 }
92 .markdown-body pre {
93 padding: 16px;
94 overflow-x: auto;
95 background: var(--bg-secondary);
96 border: 1px solid var(--border);
97 border-radius: var(--radius);
98 margin-bottom: 16px;
99 line-height: 1.5;
100 }
101 .markdown-body pre code {
102 padding: 0;
103 background: transparent;
104 font-size: 13px;
105 }
106 .markdown-body blockquote {
107 padding: 0 1em;
108 color: var(--text-muted);
109 border-left: 3px solid var(--border);
110 margin-bottom: 16px;
111 }
112 .markdown-body table {
113 width: 100%;
114 border-collapse: collapse;
115 margin-bottom: 16px;
116 }
117 .markdown-body table th, .markdown-body table td {
118 padding: 8px 16px;
119 border: 1px solid var(--border);
120 text-align: left;
121 }
122 .markdown-body table th { background: var(--bg-secondary); font-weight: 600; }
123 .markdown-body hr {
124 height: 2px;
125 background: var(--border);
126 border: none;
127 margin: 24px 0;
128 }
129 .markdown-body a { color: var(--text-link); }
130 .markdown-body img { max-width: 100%; border-radius: var(--radius); }
131 .markdown-body .task-list-item { list-style: none; }
132 .markdown-body .task-list-item input { margin-right: 8px; }
133`;