diff --git a/src/capture.ts b/src/capture.ts index 8ce3daf..ac214b6 100644 --- a/src/capture.ts +++ b/src/capture.ts @@ -56,7 +56,7 @@ async function downloadAsset(assetUrl: string, slug: string): Promise { @@ -103,6 +103,33 @@ function normalizeCopiedHtml(html: string): string { $("script, style, iframe, object, embed, form, button, textarea, input, nav").remove(); + function escapeCode(value: string): string { + return value.replace(/&/g, "&").replace(//g, ">"); + } + + $("pre") + .toArray() + .filter((pre) => $(pre).parents("pre").length === 0) + .forEach((pre) => { + const block = $(pre); + const nestedCode = block.find("pre code").last(); + const directCode = block.children("code").first(); + const codeNode = nestedCode.length ? nestedCode : directCode.length ? directCode : block.find("code").last(); + const code = codeNode.length ? codeNode.text() : block.text(); + if (!code.trim()) return; + + const labelProbe = block.clone(); + labelProbe.find("pre, code").remove(); + const label = labelProbe.text().replace(/\s+/g, " ").trim(); + const language = label.length > 0 && label.length <= 24 ? label : ""; + block.replaceWith( + `
` + + `
${escapeCode(language || "code")}
` + + `
${escapeCode(code.trimEnd())}
` + + `
` + ); + }); + $("a").each((_, anchor) => { const link = $(anchor); const decorativeImages = link.find("img").filter((_, image) => { diff --git a/src/render.ts b/src/render.ts index def9246..228ffa6 100644 --- a/src/render.ts +++ b/src/render.ts @@ -80,9 +80,14 @@ h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.12; letter-spa .content li > p { margin: 0.2em 0; } .content li > ul, .content li > ol { margin: 0.35em 0 0.45em; padding-left: 1.25em; } .content strong { font-weight: 700; } -.content pre { margin: 1em 0; overflow: auto; padding: 14px 16px; border-radius: 8px; background: var(--code-bg); color: var(--code); font-size: 14px; line-height: 1.55; } +.content pre { margin: 1em 0; overflow: auto; padding: 14px 16px; border-radius: 8px; background: var(--code-bg); color: var(--code); font-size: 14px; line-height: 1.55; white-space: pre; overflow-wrap: normal; word-break: normal; } .content code { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Noto Sans Mono", monospace; } .content :not(pre) > code { background: var(--code-bg); border-radius: 5px; padding: 0.1em 0.32em; } +.code-block { margin: 1em 0; border-radius: 8px; background: var(--code-bg); overflow: hidden; } +.code-block .code-header { min-height: 34px; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 10px 6px 14px; color: var(--muted); font-size: 12px; border-bottom: 1px solid var(--border); } +.code-block .code-header span:empty::before { content: "code"; } +.code-block [data-copy-code] { border: 0; background: transparent; color: var(--muted); font: inherit; font-size: 12px; padding: 4px 6px; cursor: pointer; } +.code-block pre { margin: 0; border-radius: 0; background: transparent; } .content table { width: 100%; border-collapse: collapse; margin: 1em 0; display: block; overflow-x: auto; } .content th, .content td { border: 1px solid var(--border); padding: 8px 10px; text-align: left; vertical-align: top; } .content blockquote { border-left: 3px solid var(--border); margin: 1em 0; padding-left: 14px; color: var(--muted); } @@ -125,6 +130,16 @@ function pageScript(slug: string): string { copy.textContent = "Copied"; setTimeout(function () { copy.textContent = "Copy link"; }, 1400); }); + document.querySelectorAll("[data-copy-code]").forEach(function (button) { + button.addEventListener("click", async function () { + var block = button.closest(".code-block"); + var code = block && block.querySelector("code"); + if (!code) return; + await navigator.clipboard.writeText(code.textContent || ""); + button.textContent = "Copied"; + setTimeout(function () { button.textContent = "Copy"; }, 1400); + }); + }); var form = document.querySelector("[data-shot-form]"); if (form) form.addEventListener("submit", function (event) { event.preventDefault(); diff --git a/src/server.ts b/src/server.ts index 2b885b1..49d3890 100644 --- a/src/server.ts +++ b/src/server.ts @@ -20,6 +20,7 @@ import { const app = express(); app.disable("x-powered-by"); +app.enable("strict routing"); app.use(express.json({ limit: "1mb" })); function requireAdmin(req: Request, res: Response, next: NextFunction): void { @@ -49,7 +50,7 @@ function publicCapture(row: ReturnType) { createdAt: row.created_at, updatedAt: row.updated_at, capturedAt: row.captured_at, - publicUrl: row.slug ? `${config.baseUrl}/${row.slug}` : null + publicUrl: row.slug ? `${config.baseUrl}/${row.slug}/` : null }; } @@ -177,7 +178,7 @@ app.get(`${config.basePath}/:slug/download.zip`, async (req, res, next) => { } }); -app.get(`${config.basePath}/:slug`, async (req, res) => { +app.get(`${config.basePath}/:slug/`, async (req, res) => { const row = store.getCaptureBySlug(req.params.slug); if (!row) { res.status(404).send("Not found"); @@ -193,6 +194,10 @@ app.get(`${config.basePath}/:slug`, async (req, res) => { .send(`${row.title || "AI Share"}

${row.error_public || `Capture is ${row.status}.`}

`); }); +app.get(`${config.basePath}/:slug`, (req, res) => { + res.redirect(308, `${config.basePath}/${req.params.slug}/`); +}); + app.get(config.basePath, (_req, res) => { res.redirect(301, `${config.basePath}/admin`); });