Fix screenshot assets and code blocks
This commit is contained in:
+28
-1
@@ -56,7 +56,7 @@ async function downloadAsset(assetUrl: string, slug: string): Promise<string | n
|
|||||||
const filename = safeAssetName(assetUrl, contentType, bytes);
|
const filename = safeAssetName(assetUrl, contentType, bytes);
|
||||||
await fs.ensureDir(artifactAssetsDir(slug));
|
await fs.ensureDir(artifactAssetsDir(slug));
|
||||||
await fs.writeFile(path.join(artifactAssetsDir(slug), filename), bytes);
|
await fs.writeFile(path.join(artifactAssetsDir(slug), filename), bytes);
|
||||||
return `/aishare/${slug}/assets/${filename}`;
|
return `assets/${filename}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function localizeAssets(conversation: NormalizedConversation, slug: string): Promise<{ failures: string[] }> {
|
async function localizeAssets(conversation: NormalizedConversation, slug: string): Promise<{ failures: string[] }> {
|
||||||
@@ -103,6 +103,33 @@ function normalizeCopiedHtml(html: string): string {
|
|||||||
|
|
||||||
$("script, style, iframe, object, embed, form, button, textarea, input, nav").remove();
|
$("script, style, iframe, object, embed, form, button, textarea, input, nav").remove();
|
||||||
|
|
||||||
|
function escapeCode(value: string): string {
|
||||||
|
return value.replace(/&/g, "&").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(
|
||||||
|
`<div class="code-block">` +
|
||||||
|
`<div class="code-header"><span>${escapeCode(language || "code")}</span><button type="button" data-copy-code>Copy</button></div>` +
|
||||||
|
`<pre><code>${escapeCode(code.trimEnd())}</code></pre>` +
|
||||||
|
`</div>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$("a").each((_, anchor) => {
|
$("a").each((_, anchor) => {
|
||||||
const link = $(anchor);
|
const link = $(anchor);
|
||||||
const decorativeImages = link.find("img").filter((_, image) => {
|
const decorativeImages = link.find("img").filter((_, image) => {
|
||||||
|
|||||||
+16
-1
@@ -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 > p { margin: 0.2em 0; }
|
||||||
.content li > ul, .content li > ol { margin: 0.35em 0 0.45em; padding-left: 1.25em; }
|
.content li > ul, .content li > ol { margin: 0.35em 0 0.45em; padding-left: 1.25em; }
|
||||||
.content strong { font-weight: 700; }
|
.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 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; }
|
.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 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 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); }
|
.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";
|
copy.textContent = "Copied";
|
||||||
setTimeout(function () { copy.textContent = "Copy link"; }, 1400);
|
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]");
|
var form = document.querySelector("[data-shot-form]");
|
||||||
if (form) form.addEventListener("submit", function (event) {
|
if (form) form.addEventListener("submit", function (event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|||||||
+7
-2
@@ -20,6 +20,7 @@ import {
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.disable("x-powered-by");
|
app.disable("x-powered-by");
|
||||||
|
app.enable("strict routing");
|
||||||
app.use(express.json({ limit: "1mb" }));
|
app.use(express.json({ limit: "1mb" }));
|
||||||
|
|
||||||
function requireAdmin(req: Request, res: Response, next: NextFunction): void {
|
function requireAdmin(req: Request, res: Response, next: NextFunction): void {
|
||||||
@@ -49,7 +50,7 @@ function publicCapture(row: ReturnType<typeof store.getCapture>) {
|
|||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
capturedAt: row.captured_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);
|
const row = store.getCaptureBySlug(req.params.slug);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
res.status(404).send("Not found");
|
res.status(404).send("Not found");
|
||||||
@@ -193,6 +194,10 @@ app.get(`${config.basePath}/:slug`, async (req, res) => {
|
|||||||
.send(`<!doctype html><meta name="robots" content="noindex"><title>${row.title || "AI Share"}</title><p>${row.error_public || `Capture is ${row.status}.`}</p>`);
|
.send(`<!doctype html><meta name="robots" content="noindex"><title>${row.title || "AI Share"}</title><p>${row.error_public || `Capture is ${row.status}.`}</p>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.get(`${config.basePath}/:slug`, (req, res) => {
|
||||||
|
res.redirect(308, `${config.basePath}/${req.params.slug}/`);
|
||||||
|
});
|
||||||
|
|
||||||
app.get(config.basePath, (_req, res) => {
|
app.get(config.basePath, (_req, res) => {
|
||||||
res.redirect(301, `${config.basePath}/admin`);
|
res.redirect(301, `${config.basePath}/admin`);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user