Skip to content

Commit

Permalink
Merge pull request #2 from piterator-org/reply-image
Browse files Browse the repository at this point in the history
Reply image
  • Loading branch information
bohanjun authored Jul 13, 2023
2 parents fc6e1cd + 9d56233 commit ba60810
Show file tree
Hide file tree
Showing 15 changed files with 1,144 additions and 60 deletions.
12 changes: 12 additions & 0 deletions packages/viewer/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ const nextConfig = {
),
})
);

// https://webpack.js.org/guides/asset-modules/#replacing-inline-loader-syntax
config.module.rules.forEach((rule) => {
// eslint-disable-next-line no-param-reassign
if (!rule.resourceQuery) rule.resourceQuery = { not: [/raw/] };
});
config.module.rules.push({
resourceQuery: /raw/,
type: "asset/source",
});
return config;
},

images: {
remotePatterns: [{ protocol: "https", hostname: "cdn.luogu.com.cn" }],
},

output: "standalone",
};

Expand Down
1 change: 1 addition & 0 deletions packages/viewer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"jsdom": "^22.1.0",
"katex": "^0.16.8",
"next": "^13.4.9",
"puppeteer": "^20.8.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-infinite-scroll-component": "^6.1.0",
Expand Down
6 changes: 4 additions & 2 deletions packages/viewer/src/app/SaveInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function SaveInput() {
}}
>
<input
className="form-control shadow"
className="form-control shadow rounded-start-4 border-0"
autoComplete="off"
placeholder="帖子链接或编号"
disabled={disabled}
Expand All @@ -40,7 +40,9 @@ export default function SaveInput() {
}}
/>
<button
className={`btn btn-${error ? "danger" : "success"} shadow`}
className={`btn btn-${
error ? "danger" : "success"
} rounded-end-4 shadow`}
type="submit"
disabled={disabled}
>
Expand Down
8 changes: 6 additions & 2 deletions packages/viewer/src/app/explore/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,16 @@ export default function Page() {
style={{ maxWidth: "40em" }}
>
<input
className="form-control shadow"
className="form-control shadow rounded-start-4 border-0"
autoComplete="off"
placeholder="帖子关键词、发布者"
disabled
/>
<button className="btn btn-primary shadow" type="button" disabled>
<button
className="btn btn-primary shadow rounded-end-4"
type="button"
disabled
>
即将上线
</button>
</div>
Expand Down
23 changes: 23 additions & 0 deletions packages/viewer/src/app/r/[rid]/get-reply-raw.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";

export default async (id: number) =>
(await prisma.reply.findUnique({
select: {
id: true,
author: true,
time: true,
content: true,
discussion: {
select: {
id: true,
snapshots: {
select: { title: true, authorId: true },
orderBy: { time: "desc" },
take: 1,
},
},
},
},
where: { id },
})) ?? notFound();
24 changes: 24 additions & 0 deletions packages/viewer/src/app/r/[rid]/image/compact/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse, type NextRequest } from "next/server";
import { notFound } from "next/navigation";
import { serializeReplyNoninteractive } from "@/lib/serialize-reply";
import getReplyRaw from "../../get-reply-raw";
import generateImage from "../generate-image";
import templateCompact from "./template-compact";

export async function GET(
request: NextRequest,
{ params }: { params: { rid: string } }
) {
const id = parseInt(params.rid, 10);
if (Number.isNaN(id)) notFound();
const replyRaw = await getReplyRaw(id);
const reply = {
...replyRaw,
...(await serializeReplyNoninteractive(replyRaw)),
};
return new NextResponse(
await generateImage(reply, templateCompact, {
width: Number(request.nextUrl.searchParams.get("width") ?? "840"),
})
);
}
159 changes: 159 additions & 0 deletions packages/viewer/src/app/r/[rid]/image/compact/template-compact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { getUserAvatarUrl } from "@/lib/luogu";
import type getReplyRaw from "../../get-reply-raw";

export default (
reply: Omit<Awaited<ReturnType<typeof getReplyRaw>>, "time"> & {
time: string;
},
{ width }: { width: number }
) => `<!DOCTYPE html>
<html>
<style>
* {
box-sizing: border-box;
}
a {
color: #0d6efd;
text-decoration: underline;
}
.badge {
display: inline-block;
padding: 0.35em 0.65em;
font-size: 0.75em;
font-weight: 700;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.375rem;
}
.markdown p {
line-height: 1.3em;
}
.link-at-user {
text-decoration: none;
color: #00b5ad;
}
.link-at-user::after {
content: "#" attr(data-uid);
font-weight: 300;
color: rgba(144, 146, 148);
font-size: 0.75em;
}
a.link-failed {
color: #dc3545;
}
a[data-linkid] {
text-decoration: none;
}
a[data-linkid]::after {
content: "[" attr(data-linkid) "]";
font-weight: 300;
font-size: 0.75em;
position: relative;
top: -0.5em;
}
.markdown ul > li {
margin-left: -0.2em !important;
}
.markdown ul > li::before {
top: -0.25em;
left: -1.5em !important;
}
#content {
margin-left: 3rem;
}
#content::before {
content: "“";
font-family: "Source Han Sans";
font-size: 5.5rem;
position: absolute;
top: .1rem;
left: 0;
color: #eeedee;
}
</style>
<body
style="
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue',
'Noto Sans', 'Liberation Sans', Arial, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
width: ${width}px;
margin: 0;
padding: 0;
"
>
<div>
<span style="white-space: nowrap;">
<span
class="lg-fg-${reply.author.color}"
style="text-decoration-line: none;"
>
${reply.author.username}
</span>
${
reply.author.checkmark
? `<svg
xmlns="http://www.w3.org/2000/svg"
width="1em"
height="1em"
viewBox="0 0 16 16"
fill="${reply.author.checkmark}"
style="
position: relative;
top: .13em;
margin-left: .05em;
"
>
<path d="M16 8C16 6.84375 15.25 5.84375 14.1875 5.4375C14.6562 4.4375 14.4688 3.1875 13.6562 2.34375C12.8125 1.53125 11.5625 1.34375 10.5625 1.8125C10.1562 0.75 9.15625 0 8 0C6.8125 0 5.8125 0.75 5.40625 1.8125C4.40625 1.34375 3.15625 1.53125 2.34375 2.34375C1.5 3.1875 1.3125 4.4375 1.78125 5.4375C0.71875 5.84375 0 6.84375 0 8C0 9.1875 0.71875 10.1875 1.78125 10.5938C1.3125 11.5938 1.5 12.8438 2.34375 13.6562C3.15625 14.5 4.40625 14.6875 5.40625 14.2188C5.8125 15.2812 6.8125 16 8 16C9.15625 16 10.1562 15.2812 10.5625 14.2188C11.5938 14.6875 12.8125 14.5 13.6562 13.6562C14.4688 12.8438 14.6562 11.5938 14.1875 10.5938C15.25 10.1875 16 9.1875 16 8ZM11.4688 6.625L7.375 10.6875C7.21875 10.8438 7 10.8125 6.875 10.6875L4.5 8.3125C4.375 8.1875 4.375 7.96875 4.5 7.8125L5.3125 7C5.46875 6.875 5.6875 6.875 5.8125 7.03125L7.125 8.34375L10.1562 5.34375C10.3125 5.1875 10.5312 5.1875 10.6562 5.34375L11.4688 6.15625C11.5938 6.28125 11.5938 6.5 11.4688 6.625Z" />
</svg>`
: ""
}
${
reply.author.badge
? `<span
class="badge lg-bg-${reply.author.color}"
style="
display: inline-block;
position: relative;
top: -.18em;
margin-left: .17em;
"
>
${reply.author.badge}
</span>`
: ""
}
</span>
<span
style="
display: inline-block;
font-size: .8em;
font-weight: 500;
color: rgba(144, 146, 148);
"
>
(于帖子
<span style="color: #0d6efd;">
${reply.discussion.snapshots[0].title}<span
style="
font-weight: 300;
color: rgba(144, 146, 148);
font-size: 0.7rem;
"
>#${reply.discussion.id}</span></span>)
</span>
<div style="float: right;">
<span style="font-size: .8rem; color: rgba(144, 146, 148);">
${reply.time}
</span>
</div>
</div>
<div id="content">
<div class="markdown">
${reply.content}
</div>
</div>
</body>
</html>`;
61 changes: 61 additions & 0 deletions packages/viewer/src/app/r/[rid]/image/generate-image.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import puppeteer from "puppeteer";
import type renderMathInElement from "katex/contrib/auto-render";
import katex from "katex/dist/katex.min?raw";
import autoRender from "katex/dist/contrib/auto-render.min?raw";
import katexStyles from "katex/dist/katex.min.css?raw";
import luogu3Styles from "@/components/luogu3.css?raw";
import markdownStyles from "@/components/markdown.css?raw";
import type getReplyRaw from "../get-reply-raw";

const browser = await puppeteer.launch();

export default async (
reply: Omit<Awaited<ReturnType<typeof getReplyRaw>>, "time"> & {
time: string;
},
template: (
reply: Omit<Awaited<ReturnType<typeof getReplyRaw>>, "time"> & {
time: string;
},
{
width,
}: {
width: number;
}
) => string,
{ width }: { width?: number }
) => {
const page = await browser.newPage();
await page.setViewport({
width: width ?? 840,
height: 1,
deviceScaleFactor: 1.5,
});
await page.setContent(template(reply, { width: width ?? 840 }));

await page.addScriptTag({ content: katex });
await page.addScriptTag({ content: autoRender });
await page.addStyleTag({ content: katexStyles });
await page.addStyleTag({ content: luogu3Styles });
await page.addStyleTag({ content: markdownStyles });
await page.evaluate(() =>
(
window as typeof window & {
renderMathInElement: typeof renderMathInElement;
}
).renderMathInElement(document.body, {
delimiters: [
{ left: "$$", right: "$$", display: true },
{ left: "$", right: "$", display: false },
],
})
);

const screenshot = await page.screenshot({
fullPage: true,
omitBackground: true,
});
await page.close();

return screenshot;
};
24 changes: 24 additions & 0 deletions packages/viewer/src/app/r/[rid]/image/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { NextResponse, type NextRequest } from "next/server";
import { notFound } from "next/navigation";
import { serializeReplyNoninteractive } from "@/lib/serialize-reply";
import getReplyRaw from "../get-reply-raw";
import generateImage from "./generate-image";
import templateDefault from "./template-default";

export async function GET(
request: NextRequest,
{ params }: { params: { rid: string } }
) {
const id = parseInt(params.rid, 10);
if (Number.isNaN(id)) notFound();
const replyRaw = await getReplyRaw(id);
const reply = {
...replyRaw,
...(await serializeReplyNoninteractive(replyRaw)),
};
return new NextResponse(
await generateImage(reply, templateDefault, {
width: Number(request.nextUrl.searchParams.get("width") ?? "840"),
})
);
}
Loading

0 comments on commit ba60810

Please sign in to comment.