)/g, '$1src$2');
+ const author = content(item).find('a.xw1').text().trim();
+
+ return renderToString(
+ <>
+ {author}
+ {contentHtml ? raw(contentHtml) : null}
+ {pattlHtml ? raw(pattlHtml) : null}
+
+ >
+ );
+ })
.join('\n');
return item;
diff --git a/lib/routes/txrjy/templates/fornumtopic.art b/lib/routes/txrjy/templates/fornumtopic.art
deleted file mode 100644
index f75b8b695..000000000
--- a/lib/routes/txrjy/templates/fornumtopic.art
+++ /dev/null
@@ -1,6 +0,0 @@
-{{ author }}
-{{@ content }}
-{{ if pattl }}
-{{@ pattl }}
-{{ /if }}
-
diff --git a/lib/routes/udn/breaking-news.ts b/lib/routes/udn/breaking-news.tsx
similarity index 92%
rename from lib/routes/udn/breaking-news.ts
rename to lib/routes/udn/breaking-news.tsx
index a8a2cb104..871602aaf 100644
--- a/lib/routes/udn/breaking-news.ts
+++ b/lib/routes/udn/breaking-news.tsx
@@ -1,12 +1,10 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import timezone from '@/utils/timezone';
export const route: Route = {
@@ -73,10 +71,14 @@ async function handler(ctx) {
let description = '';
if (data.image) {
- description += art(path.join(__dirname, 'templates/figure.art'), {
- src: data.image.contentUrl,
- alt: data.image.name,
- });
+ description += renderToString(
+
+
+
+
+ {data.image.name}
+
+ );
}
if (content.length) {
description += content.html();
diff --git a/lib/routes/udn/templates/figure.art b/lib/routes/udn/templates/figure.art
deleted file mode 100644
index b6b596dff..000000000
--- a/lib/routes/udn/templates/figure.art
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-{{ alt }}
-
\ No newline at end of file
diff --git a/lib/routes/uptimerobot/rss.ts b/lib/routes/uptimerobot/rss.tsx
similarity index 77%
rename from lib/routes/uptimerobot/rss.ts
rename to lib/routes/uptimerobot/rss.tsx
index ad7d4ff1a..6b1afae07 100644
--- a/lib/routes/uptimerobot/rss.ts
+++ b/lib/routes/uptimerobot/rss.tsx
@@ -1,12 +1,10 @@
-import path from 'node:path';
-
import dayjs from 'dayjs';
+import { renderToString } from 'hono/jsx/dom/server';
import Parser from 'rss-parser';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import type { Route } from '@/types';
import { fallback, queryToBoolean } from '@/utils/readable-social';
-import { art } from '@/utils/render';
const titleRegex = /(.+)\s+is\s+([A-Z]+)\s+\((.+)\)/;
@@ -128,19 +126,43 @@ async function handler(ctx) {
throw new InvalidParameterError('Unexpected status, please open an issue.');
}
- const desc = art(path.join(__dirname, 'templates/rss.art'), {
- status,
- link,
- id: showID ? id : null,
- duration: formatTime(duration),
- uptime: formatTime(monitor.uptime),
- downtime: formatTime(monitor.downtime),
- uptime_ratio: Number(monitor.uptimeRatio()).toLocaleString(undefined, {
- style: 'percent',
- minimumFractionDigits: 2,
- }),
- details: item.content,
- });
+ const desc = renderToString(
+ <>
+ Already {status} for {formatTime(duration)}
+
+
+ {showID && id ? (
+ <>
+ Monitor ID:{' '}
+ {link ? (
+
+ {id}
+
+ ) : (
+ id
+ )}
+
+
+ >
+ ) : null}
+ Uptime: {formatTime(monitor.uptime)}
+
+ Downtime: {formatTime(monitor.downtime)}
+
+ Availability:{' '}
+ {Number(monitor.uptimeRatio()).toLocaleString(undefined, {
+ style: 'percent',
+ minimumFractionDigits: 2,
+ })}
+ {item.content && item.content.trim() !== 'Alert Details:' ? (
+ <>
+
+
+ {item.content}
+ >
+ ) : null}
+ >
+ );
return {
...item,
diff --git a/lib/routes/uptimerobot/templates/rss.art b/lib/routes/uptimerobot/templates/rss.art
deleted file mode 100644
index 856b346c0..000000000
--- a/lib/routes/uptimerobot/templates/rss.art
+++ /dev/null
@@ -1,20 +0,0 @@
-Already {{ status }} for {{ duration }}
-
-{{ if id }}
- Monitor ID:
- {{ if link }}
- {{ id }}
- {{ else }}
- {{ id }}
- {{ /if }}
-
-{{ /if }}
-Uptime: {{ uptime }}
-
-Downtime: {{ downtime }}
-
-Availability: {{ uptime_ratio }}
-{{ if details && details.trim() !== 'Alert Details:' }}
-
- {{ details }}
-{{ /if }}
diff --git a/lib/routes/urbandictionary/random.ts b/lib/routes/urbandictionary/random.tsx
similarity index 62%
rename from lib/routes/urbandictionary/random.ts
rename to lib/routes/urbandictionary/random.tsx
index 405ba1d69..52c54c887 100644
--- a/lib/routes/urbandictionary/random.ts
+++ b/lib/routes/urbandictionary/random.tsx
@@ -1,9 +1,8 @@
-import path from 'node:path';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
export const route: Route = {
path: '/random',
@@ -35,7 +34,27 @@ async function handler() {
const items = data.list.map((item) => ({
title: item.word,
- description: art(path.join(__dirname, 'templates/definition.art'), { item }),
+ description: renderToString(
+ <>
+ {item.definition ? (
+ <>
+ {item.definition}
+
+ >
+ ) : null}
+ {item.example ? (
+ <>
+ {item.example}
+
+ >
+ ) : null}
+ {item.author ? (
+ <>
+ by {item.author}
+ >
+ ) : null}
+ >
+ ),
link: `${baseUrl}/define.php?term=${item.word}`,
guid: item.permalink,
pubDate: parseDate(item.written_on),
diff --git a/lib/routes/urbandictionary/templates/definition.art b/lib/routes/urbandictionary/templates/definition.art
deleted file mode 100644
index 9b3285f08..000000000
--- a/lib/routes/urbandictionary/templates/definition.art
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ if item.definition }}
- {{ item.definition }}
-{{ /if }}
-
-{{ if item.example }}
- {{ item.example }}
-{{ /if }}
-
-{{ if item.author }}
- by {{ item.author }}
-{{ /if }}
diff --git a/lib/routes/utgd/templates/description.art b/lib/routes/utgd/templates/description.art
deleted file mode 100644
index 8ce3b4866..000000000
--- a/lib/routes/utgd/templates/description.art
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ if image }}
-
-
-{{ /if }}
-{{ if membership }}
-
-UNTAG Premium
-{{ /if }}
-{{ if description }}
-{{@ description }}
-{{ /if }}
diff --git a/lib/routes/utgd/utils.ts b/lib/routes/utgd/utils.tsx
similarity index 59%
rename from lib/routes/utgd/utils.ts
rename to lib/routes/utgd/utils.tsx
index 85223d93a..94fb3f6ad 100644
--- a/lib/routes/utgd/utils.ts
+++ b/lib/routes/utgd/utils.tsx
@@ -1,11 +1,10 @@
-import path from 'node:path';
-
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import MarkdownIt from 'markdown-it';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import timezone from '@/utils/timezone';
const md = MarkdownIt({
@@ -29,13 +28,28 @@ export const parseArticle = (item) =>
cache.tryGet(`untag-${item.id}`, async () => {
const data = await ofetch(`${apiRootUrl}/api/v2/article/${item.id}/`);
- item.description = art(path.join(__dirname, 'templates/description.art'), {
- membership: data.article_for_membership,
- image: data.article_image,
- description: md.render(data.article_content),
- });
+ item.description = renderDescription(data.article_image, data.article_for_membership, md.render(data.article_content));
item.category = [...data.article_category.map((c) => c.category_name), ...data.article_tag.map((t) => t.tag_name)];
return item;
});
+
+const renderDescription = (image: string | undefined, membership: boolean, description: string): string =>
+ renderToString(
+ <>
+ {image ? (
+ <>
+
+
+ >
+ ) : null}
+ {membership ? (
+ <>
+
+ UNTAG Premium
+ >
+ ) : null}
+ {description ? raw(description) : null}
+ >
+ );
diff --git a/lib/routes/vcb-s/category.ts b/lib/routes/vcb-s/category.ts
index 83d90138f..bfd8880b9 100644
--- a/lib/routes/vcb-s/category.ts
+++ b/lib/routes/vcb-s/category.ts
@@ -1,10 +1,9 @@
-import path from 'node:path';
-
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+import { renderDescription } from './templates/post';
const rootUrl = 'https://vcb-s.com';
const cateAPIUrl = `${rootUrl}/wp-json/wp/v2/categories`;
@@ -59,7 +58,7 @@ async function handler(ctx) {
const data = response.data;
const items = data.map((item) => {
- const description = art(path.join(__dirname, 'templates/post.art'), {
+ const description = renderDescription({
post: item.content.rendered.replaceAll(/(.*?)<\/pre>/gs, '$1 ').replaceAll(/(.*?)<\/div>/gs, '$1 '),
medias: item._embedded['wp:featuredmedia'],
});
diff --git a/lib/routes/vcb-s/templates/post.art b/lib/routes/vcb-s/templates/post.art
deleted file mode 100644
index 2f026e84c..000000000
--- a/lib/routes/vcb-s/templates/post.art
+++ /dev/null
@@ -1,9 +0,0 @@
-
-{{ if medias }}
-{{ each medias media }}
-
-{{ /each }}
-{{ /if }}
-{{ if post }}
-{{@ post }}
-{{ /if }}
diff --git a/lib/routes/vcb-s/templates/post.tsx b/lib/routes/vcb-s/templates/post.tsx
new file mode 100644
index 000000000..2c1288666
--- /dev/null
+++ b/lib/routes/vcb-s/templates/post.tsx
@@ -0,0 +1,28 @@
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
+
+type Media = {
+ media_details?: {
+ width?: string | number;
+ height?: string | number;
+ };
+ source_url?: string;
+};
+
+type DescriptionData = {
+ medias?: Media[];
+ post?: string;
+};
+
+const VcbPostDescription = ({ medias, post }: DescriptionData) => (
+ <>
+ {medias?.map((media) => (
+
+
+
+ ))}
+ {post ? raw(post) : null}
+ >
+);
+
+export const renderDescription = (data: DescriptionData) => renderToString( );
diff --git a/lib/routes/vice/templates/article.art b/lib/routes/vice/templates/article.art
deleted file mode 100644
index 0bfa7bf84..000000000
--- a/lib/routes/vice/templates/article.art
+++ /dev/null
@@ -1,19 +0,0 @@
-{{ if image }}
-
-
- {{ image.caption || image.credit || image.alt }}
-
-{{ /if }}
-
-{{ if body }}
-{{@ body.html }}
-{{ /if }}
-
-{{ if heading2 }}
-
-{{@ heading2.html }}
-{{ /if }}
-
-{{ if oembed }}
-{{@ oembed.html }}
-{{ /if }}
diff --git a/lib/routes/vice/topic.ts b/lib/routes/vice/topic.tsx
similarity index 83%
rename from lib/routes/vice/topic.ts
rename to lib/routes/vice/topic.tsx
index 43110228f..0c0da7f3c 100644
--- a/lib/routes/vice/topic.ts
+++ b/lib/routes/vice/topic.tsx
@@ -1,14 +1,31 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
-const render = (data) => art(path.join(__dirname, 'templates/article.art'), data);
+const render = (data) =>
+ renderToString(
+ <>
+ {data.image ? (
+
+
+ {data.image.caption || data.image.credit || data.image.alt}
+
+ ) : null}
+ {data.body ? {raw(data.body.html)}
: null}
+ {data.heading2 ? (
+ <>
+
+ {raw(data.heading2.html)}
+ >
+ ) : null}
+ {data.oembed ? raw(data.oembed.html) : null}
+ >
+ );
export const route: Route = {
path: '/topic/:topic/:language?',
diff --git a/lib/routes/vimeo/category.ts b/lib/routes/vimeo/category.ts
index a36ada8ab..f590fdab4 100644
--- a/lib/routes/vimeo/category.ts
+++ b/lib/routes/vimeo/category.ts
@@ -1,5 +1,3 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
import type { Route } from '@/types';
@@ -7,7 +5,8 @@ import { ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+import { renderDescription } from './templates/description';
export const route: Route = {
path: '/category/:category/:staffpicks?',
@@ -79,7 +78,7 @@ async function handler(ctx) {
description: feedDescription,
item: vimeojs.map((item) => ({
title: item.name,
- description: art(path.join(__dirname, 'templates/description.art'), {
+ description: renderDescription({
videoUrl: item.uri.replace(`/videos`, ''),
vdescription: item.description || '',
}),
diff --git a/lib/routes/vimeo/channel.ts b/lib/routes/vimeo/channel.ts
index 606632a27..8d6d169f5 100644
--- a/lib/routes/vimeo/channel.ts
+++ b/lib/routes/vimeo/channel.ts
@@ -1,12 +1,11 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+import { renderDescription } from './templates/description';
export const route: Route = {
path: '/channel/:channel',
@@ -84,7 +83,7 @@ async function handler(ctx) {
const author = item.find('.meta a').text();
return {
title,
- description: art(path.join(__dirname, 'templates/description.art'), {
+ description: renderDescription({
videoUrl: item.find('.more').attr('href'),
vdescription: description[index] || '',
}),
diff --git a/lib/routes/vimeo/templates/description.art b/lib/routes/vimeo/templates/description.art
deleted file mode 100644
index 91ed43daf..000000000
--- a/lib/routes/vimeo/templates/description.art
+++ /dev/null
@@ -1,4 +0,0 @@
-
-{{ if vdescription }}
- {{@ vdescription }}
-{{ /if }}
diff --git a/lib/routes/vimeo/templates/description.tsx b/lib/routes/vimeo/templates/description.tsx
new file mode 100644
index 000000000..3b092c953
--- /dev/null
+++ b/lib/routes/vimeo/templates/description.tsx
@@ -0,0 +1,16 @@
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
+
+type DescriptionProps = {
+ videoUrl: string;
+ vdescription?: string;
+};
+
+const Description = ({ videoUrl, vdescription }: DescriptionProps) => (
+ <>
+
+ {vdescription ? {raw(vdescription)}
: null}
+ >
+);
+
+export const renderDescription = (props: DescriptionProps): string => renderToString( );
diff --git a/lib/routes/vimeo/usr-videos.ts b/lib/routes/vimeo/usr-videos.ts
index 13fab5bea..00bb7db86 100644
--- a/lib/routes/vimeo/usr-videos.ts
+++ b/lib/routes/vimeo/usr-videos.ts
@@ -1,10 +1,9 @@
-import path from 'node:path';
-
import type { Route } from '@/types';
import { ViewType } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+import { renderDescription } from './templates/description';
export const route: Route = {
path: '/user/:username/:cat?',
@@ -91,7 +90,7 @@ async function handler(ctx) {
return {
title: picked ? item.clip.name : item.name,
- description: art(path.join(__dirname, 'templates/description.art'), {
+ description: renderDescription({
videoUrl: picked ? item.clip.uri.replace('/videos', '') : item.uri.replace('/videos', ''),
vdescription: vdescription ? vdescription.replaceAll('\n', ' ') : '',
}),
diff --git a/lib/routes/visionias/templates/description-sub.art b/lib/routes/visionias/templates/description-sub.art
deleted file mode 100644
index e947d6315..000000000
--- a/lib/routes/visionias/templates/description-sub.art
+++ /dev/null
@@ -1,6 +0,0 @@
-{{ if heading }}
- {{ heading }}
-{{ /if }}
-{{ if articleContent }}
- {{@ articleContent }}
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/visionias/templates/description-sub.tsx b/lib/routes/visionias/templates/description-sub.tsx
new file mode 100644
index 000000000..115e3df7d
--- /dev/null
+++ b/lib/routes/visionias/templates/description-sub.tsx
@@ -0,0 +1,16 @@
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
+
+type DescriptionSubProps = {
+ heading?: string;
+ articleContent?: string;
+};
+
+const DescriptionSub = ({ heading, articleContent }: DescriptionSubProps) => (
+ <>
+ {heading ? {heading} : null}
+ {articleContent ? raw(articleContent) : null}
+ >
+);
+
+export const renderDescriptionSub = (props: DescriptionSubProps): string => renderToString( );
diff --git a/lib/routes/visionias/templates/description.art b/lib/routes/visionias/templates/description.art
deleted file mode 100644
index 2f92e6a1c..000000000
--- a/lib/routes/visionias/templates/description.art
+++ /dev/null
@@ -1,12 +0,0 @@
-{{ if heading }}
- {{ heading }}
-{{ /if }}
-{{ if subItems }}
- {{ each subItems item }}
- {{ if item?.description }}
- {{@ item.description }}
- {{ /if }}
- {{ /each }}
-{{else}}
- {{@ articleContent }}
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/visionias/templates/description.tsx b/lib/routes/visionias/templates/description.tsx
new file mode 100644
index 000000000..b3ae2e271
--- /dev/null
+++ b/lib/routes/visionias/templates/description.tsx
@@ -0,0 +1,17 @@
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
+
+type DescriptionProps = {
+ heading?: string;
+ subItems?: Array<{ description?: string }>;
+ articleContent?: string;
+};
+
+const Description = ({ heading, subItems, articleContent }: DescriptionProps) => (
+ <>
+ {heading ? {heading} : null}
+ {subItems ? <>{subItems.map((item, index) => (item?.description ? {raw(item.description)} : null))}> : <>{articleContent ? raw(articleContent) : null}>}
+ >
+);
+
+export const renderDescription = (props: DescriptionProps): string => renderToString( );
diff --git a/lib/routes/visionias/utils.ts b/lib/routes/visionias/utils.ts
index 411e489c2..ea0d1d133 100644
--- a/lib/routes/visionias/utils.ts
+++ b/lib/routes/visionias/utils.ts
@@ -1,12 +1,12 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
import type { DataItem } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+import { renderDescription } from './templates/description';
+import { renderDescriptionSub } from './templates/description-sub';
export const baseUrl = 'https://visionias.in';
@@ -39,7 +39,7 @@ export async function extractNews(item, selector) {
?.nextAll('li')
.toArray()
.map((tag) => $$(tag).text());
- const description = art(path.join(__dirname, 'templates/description.art'), {
+ const description = renderDescription({
heading: title,
articleContent: htmlContent,
});
@@ -55,7 +55,7 @@ export async function extractNews(item, selector) {
return items;
} else if (sections.length === 0) {
const htmlContent = extractArticle(mainGroup.html());
- const description = art(path.join(__dirname, 'templates/description.art'), {
+ const description = renderDescription({
heading,
articleContent: htmlContent,
});
@@ -73,13 +73,13 @@ export async function extractNews(item, selector) {
const mainDiv = $$(element);
const title = mainDiv.find('a > div > h2').text().trim();
const htmlContent = extractArticle(mainDiv.html(), 'div.ck-content');
- const description = art(path.join(__dirname, 'templates/description-sub.art'), {
+ const description = renderDescriptionSub({
heading: title,
articleContent: htmlContent,
});
return { description };
});
- const description = art(path.join(__dirname, 'templates/description.art'), {
+ const description = renderDescription({
heading,
subItems: items,
});
diff --git a/lib/routes/wainao/templates/description.art b/lib/routes/wainao/templates/description.art
deleted file mode 100644
index 1cb5fc8fa..000000000
--- a/lib/routes/wainao/templates/description.art
+++ /dev/null
@@ -1,9 +0,0 @@
-{{ if elements.length > 0 }}
- {{ each elements element }}
- {{ if element.type === 'text' }}
- {{ element.content }}
- {{ else if element.type === 'raw_html' }}
- {{@ element.content }}
- {{ /if }}
- {{ /each }}
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/wainao/topics.ts b/lib/routes/wainao/topics.tsx
similarity index 91%
rename from lib/routes/wainao/topics.ts
rename to lib/routes/wainao/topics.tsx
index 2aba40e01..5ae9aaacb 100644
--- a/lib/routes/wainao/topics.ts
+++ b/lib/routes/wainao/topics.tsx
@@ -1,14 +1,30 @@
-import path from 'node:path';
-
import type { CheerioAPI } from 'cheerio';
import { load } from 'cheerio';
import type { Context } from 'hono';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Data, DataItem, Route } from '@/types';
import { ViewType } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+const renderDescription = (elements) =>
+ renderToString(
+ <>
+ {elements?.length
+ ? elements.map((element) => {
+ if (element.type === 'text') {
+ return {element.content}
;
+ }
+ if (element.type === 'raw_html') {
+ return <>{raw(element.content ?? '')}>;
+ }
+ return null;
+ })
+ : null}
+ >
+ );
export const handler = async (ctx: Context): Promise => {
const { id = 'hotspot' } = ctx.req.param();
@@ -40,9 +56,7 @@ export const handler = async (ctx: Context): Promise => {
.slice(0, limit)
.map((item): DataItem => {
const title: string = item.headlines.basic;
- const description: string = art(path.join(__dirname, 'templates/description.art'), {
- elements: item.content_elements,
- });
+ const description: string = renderDescription(item.content_elements);
const pubDate: number | string = item.publish_date;
const linkUrl: string | undefined = item.website_url;
const categories: string[] = [item.taxonomy?.primary_section?.name].filter(Boolean);
diff --git a/lib/routes/wallpaperhub/index.ts b/lib/routes/wallpaperhub/index.tsx
similarity index 76%
rename from lib/routes/wallpaperhub/index.ts
rename to lib/routes/wallpaperhub/index.tsx
index a7cc669b2..7093c47fe 100644
--- a/lib/routes/wallpaperhub/index.ts
+++ b/lib/routes/wallpaperhub/index.tsx
@@ -1,9 +1,8 @@
-import path from 'node:path';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
export const route: Route = {
path: '/',
@@ -28,10 +27,12 @@ async function handler() {
const list = response.data.entities.map((item) => ({
title: item.entity.title,
- description: art(path.join(__dirname, 'templates/description.art'), {
- description: item.entity.description,
- img: item.entity.variations[0].resolutions[0].url || item.entity.thumbnail,
- }),
+ description: renderToString(
+ <>
+ {item.entity.description}
+
+ >
+ ),
pubDate: parseDate(item.entity.created),
link: `https://wallpaperhub.app/wallpapers/${item.entity.id}`,
}));
diff --git a/lib/routes/wallpaperhub/templates/description.art b/lib/routes/wallpaperhub/templates/description.art
deleted file mode 100644
index 8afb4dab3..000000000
--- a/lib/routes/wallpaperhub/templates/description.art
+++ /dev/null
@@ -1 +0,0 @@
-{{ description }}
diff --git a/lib/routes/wallstreetcn/live.ts b/lib/routes/wallstreetcn/live.tsx
similarity index 83%
rename from lib/routes/wallstreetcn/live.ts
rename to lib/routes/wallstreetcn/live.tsx
index 3ccdc4a47..bad579754 100644
--- a/lib/routes/wallstreetcn/live.ts
+++ b/lib/routes/wallstreetcn/live.tsx
@@ -1,9 +1,9 @@
-import path from 'node:path';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
const titles = {
global: '要闻',
@@ -63,11 +63,13 @@ async function handler(ctx) {
title: item.title || item.content_text,
pubDate: parseDate(item.display_time * 1000),
author: item.author?.display_name ?? '',
- description: art(path.join(__dirname, 'templates/description.art'), {
- description: item.content,
- more: item.content_more,
- images: item.images,
- }),
+ description: renderToString(
+ <>
+ {item.content ? raw(item.content) : null}
+ {item.content_more ? raw(item.content_more) : null}
+ {item.images?.length ? item.images.map((image) => ) : null}
+ >
+ ),
}));
return {
diff --git a/lib/routes/wallstreetcn/templates/description.art b/lib/routes/wallstreetcn/templates/description.art
deleted file mode 100644
index ee1f9e6f2..000000000
--- a/lib/routes/wallstreetcn/templates/description.art
+++ /dev/null
@@ -1,11 +0,0 @@
-{{ if description }}
-{{@ description }}
-{{ /if }}
-{{ if more }}
-{{@ more }}
-{{ /if }}
-{{ if images }}
-{{ each images image }}
-
-{{ /each }}
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/warthunder/news.ts b/lib/routes/warthunder/news.tsx
similarity index 85%
rename from lib/routes/warthunder/news.ts
rename to lib/routes/warthunder/news.tsx
index e92685166..3a452a9c5 100644
--- a/lib/routes/warthunder/news.ts
+++ b/lib/routes/warthunder/news.tsx
@@ -1,14 +1,20 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import timezone from '@/utils/timezone';
-const renderDescription = (desc) => art(path.join(__dirname, 'templates/description.art'), desc);
+const renderDescription = (desc: { description?: string; imglink?: string }) => renderToString( );
+
+const WarthunderDescription = ({ description, imglink }: { description?: string; imglink?: string }) => (
+ <>
+ {description}
+
+
+ >
+);
export const route: Route = {
path: '/news',
diff --git a/lib/routes/warthunder/templates/description.art b/lib/routes/warthunder/templates/description.art
deleted file mode 100644
index 48f298238..000000000
--- a/lib/routes/warthunder/templates/description.art
+++ /dev/null
@@ -1 +0,0 @@
-{{description}}
\ No newline at end of file
diff --git a/lib/routes/washingtonpost/app.ts b/lib/routes/washingtonpost/app.ts
deleted file mode 100644
index dc443a9aa..000000000
--- a/lib/routes/washingtonpost/app.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import path from 'node:path';
-
-import dayjs from 'dayjs';
-import advancedFormat from 'dayjs/plugin/advancedFormat.js';
-import timezone from 'dayjs/plugin/timezone.js';
-import utc from 'dayjs/plugin/utc.js';
-import { FetchError } from 'ofetch';
-
-import type { Route } from '@/types';
-import cache from '@/utils/cache';
-import got from '@/utils/got';
-import { art } from '@/utils/render';
-
-export const route: Route = {
- path: '/app/:category{.+}?',
- categories: ['traditional-media'],
- example: '/washingtonpost/app/national',
- parameters: {
- category: 'Category from the path of the URL of the corresponding site, see below',
- },
- features: {
- requirePuppeteer: false,
- antiCrawler: false,
- supportBT: false,
- supportPodcast: false,
- supportScihub: false,
- },
- name: 'App',
- maintainers: ['quiniapiezoelectricity'],
- radar: [
- {
- source: ['www.washingtonpost.com/:category'],
- target: '/app/:category',
- },
- ],
- handler,
- description: `::: tip
-For example, the category for https://www.washingtonpost.com/national/investigations would be /national/investigations.
-:::`,
-};
-
-function handleDuplicates(array) {
- const objects = {};
- for (const obj of array) {
- objects[obj.id] = objects[obj.id] ? Object.assign(objects[obj.id], obj) : obj;
- }
- return Object.values(objects);
-}
-
-async function handler(ctx) {
- const category = ctx.req.param('category') ?? '';
- const headers = {
- Accept: '*/*',
- Connection: 'keep-alive',
- 'User-Agent': 'Classic/6.70.0',
- };
- dayjs.extend(utc);
- dayjs.extend(timezone);
- dayjs.extend(advancedFormat);
- art.defaults.imports.dayjs = dayjs;
-
- const url = `https://jsonapp1.washingtonpost.com/fusion_prod/v2/${category}`;
- const response = await got.get(url, { headers });
- const title = response.data.tracking.page_title.includes('Washington Post') ? response.data.tracking.page_title : `The Washington Post - ${response.data.tracking.page_title}`;
- const link = 'https://washingtonpost.com' + response.data.tracking.page_path;
- const mains = response.data.regions[0].items.filter((item) => item.items);
- const list = mains.flatMap((main) =>
- main.items[0].items
- .filter((item) => item.is_from_feed === true)
- .map((item) => {
- const object = {
- id: item.id,
- title: item.headline.text,
- link: item.link.url,
- pubDate: item.link.display_date,
- updated: item.link.last_modified,
- };
- if (item.blurbs?.items[0]?.text) {
- object.description = item.blurbs?.items[0]?.text;
- }
- return object;
- })
- );
- const feed = handleDuplicates(list);
- const items = await Promise.all(
- feed.map((item) =>
- cache.tryGet(item.link, async () => {
- let response;
- try {
- response = await got(`https://rainbowapi-a.wpdigital.net/rainbow-data-service/rainbow/content-by-url.json?followLinks=false&url=${item.link}`, { headers });
- } catch (error) {
- if (error instanceof FetchError && error.statusCode === 415) {
- // Interactive or podcast contents will return 415 Unsupported Media Type. Keep calm and carry on.
- return item;
- } else {
- throw error;
- }
- }
- item.title = response.data.title ?? item.title;
- item.author =
- response.data.items
- .filter((entry) => entry.type === 'byline')
- ?.flatMap((entry) => entry.authors.map((author) => author.name))
- ?.join(', ') ?? '';
- item.description = art(path.join(__dirname, 'templates/description.art'), {
- content: response.data.items,
- });
- return item;
- })
- )
- );
-
- return {
- title,
- link,
- item: items,
- };
-}
diff --git a/lib/routes/washingtonpost/app.tsx b/lib/routes/washingtonpost/app.tsx
new file mode 100644
index 000000000..7fca7dbd1
--- /dev/null
+++ b/lib/routes/washingtonpost/app.tsx
@@ -0,0 +1,220 @@
+import dayjs from 'dayjs';
+import advancedFormat from 'dayjs/plugin/advancedFormat.js';
+import timezone from 'dayjs/plugin/timezone.js';
+import utc from 'dayjs/plugin/utc.js';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
+import { FetchError } from 'ofetch';
+
+import type { Route } from '@/types';
+import cache from '@/utils/cache';
+import got from '@/utils/got';
+
+export const route: Route = {
+ path: '/app/:category{.+}?',
+ categories: ['traditional-media'],
+ example: '/washingtonpost/app/national',
+ parameters: {
+ category: 'Category from the path of the URL of the corresponding site, see below',
+ },
+ features: {
+ requirePuppeteer: false,
+ antiCrawler: false,
+ supportBT: false,
+ supportPodcast: false,
+ supportScihub: false,
+ },
+ name: 'App',
+ maintainers: ['quiniapiezoelectricity'],
+ radar: [
+ {
+ source: ['www.washingtonpost.com/:category'],
+ target: '/app/:category',
+ },
+ ],
+ handler,
+ description: `::: tip
+For example, the category for https://www.washingtonpost.com/national/investigations would be /national/investigations.
+:::`,
+};
+
+function handleDuplicates(array) {
+ const objects = {};
+ for (const obj of array) {
+ objects[obj.id] = objects[obj.id] ? Object.assign(objects[obj.id], obj) : obj;
+ }
+ return Object.values(objects);
+}
+
+async function handler(ctx) {
+ const category = ctx.req.param('category') ?? '';
+ const headers = {
+ Accept: '*/*',
+ Connection: 'keep-alive',
+ 'User-Agent': 'Classic/6.70.0',
+ };
+ dayjs.extend(utc);
+ dayjs.extend(timezone);
+ dayjs.extend(advancedFormat);
+
+ const url = `https://jsonapp1.washingtonpost.com/fusion_prod/v2/${category}`;
+ const response = await got.get(url, { headers });
+ const title = response.data.tracking.page_title.includes('Washington Post') ? response.data.tracking.page_title : `The Washington Post - ${response.data.tracking.page_title}`;
+ const link = 'https://washingtonpost.com' + response.data.tracking.page_path;
+ const mains = response.data.regions[0].items.filter((item) => item.items);
+ const list = mains.flatMap((main) =>
+ main.items[0].items
+ .filter((item) => item.is_from_feed === true)
+ .map((item) => {
+ const object = {
+ id: item.id,
+ title: item.headline.text,
+ link: item.link.url,
+ pubDate: item.link.display_date,
+ updated: item.link.last_modified,
+ };
+ if (item.blurbs?.items[0]?.text) {
+ object.description = item.blurbs?.items[0]?.text;
+ }
+ return object;
+ })
+ );
+ const feed = handleDuplicates(list);
+ const items = await Promise.all(
+ feed.map((item) =>
+ cache.tryGet(item.link, async () => {
+ let response;
+ try {
+ response = await got(`https://rainbowapi-a.wpdigital.net/rainbow-data-service/rainbow/content-by-url.json?followLinks=false&url=${item.link}`, { headers });
+ } catch (error) {
+ if (error instanceof FetchError && error.statusCode === 415) {
+ // Interactive or podcast contents will return 415 Unsupported Media Type. Keep calm and carry on.
+ return item;
+ } else {
+ throw error;
+ }
+ }
+ item.title = response.data.title ?? item.title;
+ item.author =
+ response.data.items
+ .filter((entry) => entry.type === 'byline')
+ ?.flatMap((entry) => entry.authors.map((author) => author.name))
+ ?.join(', ') ?? '';
+ item.description = renderDescription(response.data.items);
+ return item;
+ })
+ )
+ );
+
+ return {
+ title,
+ link,
+ item: items,
+ };
+}
+
+const renderDescription = (content): string =>
+ renderToString(
+ <>
+ {content?.map((entry, index) => {
+ if (!entry) {
+ return null;
+ }
+
+ if (entry.type === 'title' && entry.subtype !== 'h1') {
+ const TitleTag = (entry.subtype || 'h2') as keyof JSX.IntrinsicElements;
+ return {entry.mime === 'text/html' ? raw(entry.content) : entry.content} ;
+ }
+
+ if (entry.type === 'sanitized_html') {
+ if (entry.subtype === 'paragraph') {
+ return (
+
+ {entry.mime === 'text/html' ? raw(entry.content) : entry.content}
+ {entry.oembed ? raw(entry.oembed) : null}
+
+ );
+ }
+
+ if (entry.subtype === 'subhead') {
+ const SubheadTag = `h${entry.subhead_level || 4}` as keyof JSX.IntrinsicElements;
+ return (
+
+ {entry.mime === 'text/html' ? raw(entry.content) : entry.content}
+ {entry.oembed ? raw(entry.oembed) : null}
+
+ );
+ }
+ }
+
+ if (entry.type === 'deck') {
+ return (
+
+ {entry.mime === 'text/html' ? raw(entry.content) : entry.content}
+
+ );
+ }
+
+ if (entry.type === 'image') {
+ return (
+
+
+ {entry.fullcaption}
+
+ );
+ }
+
+ if (entry.type === 'video') {
+ if (entry.content?.html) {
+ return {raw(entry.content.html)} ;
+ }
+
+ if (entry.mediaURL) {
+ return (
+
+
+
+
+ {entry.fullcaption ? {entry.fullcaption} : null}
+
+ );
+ }
+ }
+
+ if (entry.type === 'list') {
+ const ListTag = entry.subtype === 'ordered' ? 'ol' : 'ul';
+ return (
+
+ {(entry.content ?? []).map((listItem, itemIndex) => (
+ {entry.mime === 'text/html' ? raw(listItem) : listItem}
+ ))}
+
+ );
+ }
+
+ if (entry.type === 'divider') {
+ return (
+
+
+
+
+
+ );
+ }
+
+ if (entry.type === 'byline' && (entry.subtype === 'live-update' || entry.subtype === 'live-reporter-insight')) {
+ return (
+
+ {entry.mime === 'text/html' ? raw(entry.content) : entry.content}
+
+ );
+ }
+
+ if (entry.type === 'date' && entry.subtype === 'live-update') {
+ return entry.content ? {dayjs.tz(entry.content, 'America/New_York').locale('en').format('dddd, MMMM D, YYYY h:mm A z')} : null;
+ }
+
+ return null;
+ })}
+ >
+ );
diff --git a/lib/routes/washingtonpost/templates/description.art b/lib/routes/washingtonpost/templates/description.art
deleted file mode 100644
index fc7382329..000000000
--- a/lib/routes/washingtonpost/templates/description.art
+++ /dev/null
@@ -1,59 +0,0 @@
-{{ if content }}
-{{ each content }}
- {{ if $value.type == 'title' && $value.subtype != 'h1'}}
- <{{ if $value.subtype }}{{ $value.subtype }}{{ else }}h2{{ /if }}>
- {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }}
- {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }}
-
- {{ /if }}
- {{ if $value.type == 'sanitized_html' }}
- {{ if $value.subtype == 'paragraph' }}{{ else if $value.subtype == 'subhead' }}{{ /if }}
- {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }}
- {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }}
- {{ if $value.oembed }}{{@ $value.oembed }}{{ /if }}
- {{ if $value.subtype == 'paragraph' }}
{{ else if $value.subtype == 'subhead' }}{{ /if }}
- {{ /if }}
- {{ if $value.type == 'deck' }}
-
- {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }}
- {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }}
-
- {{ /if }}
- {{ if $value.type == 'image' }}
- {{ $value.fullcaption }}
- {{ /if }}
- {{ if $value.type == 'video' }}
- {{ if $value.content && $value.content.html }}{{@ $value.content.html }}
- {{ else if $value.mediaURL }}
-
-
-
-
- {{ if $value.fullcaption }}{{ $value.fullcaption }} {{ /if }}
-
- {{ /if }}
- {{ /if }}
- {{ if $value.type == 'list' }}
- {{ if $value.subtype == 'ordered' }}{{ else }}{{ /if }}
- {{ if $value.mime == 'text/html' }}{{ each $value.content }}{{@ $value }} {{ /each }}{{ /if }}
- {{ if $value.mime == 'text/plain' }}{{ each $value.content }}{{ $value }} {{ /each }}{{ /if }}
- {{ if $value.subtype == 'ordered' }} {{ else }}{{ /if }}
- {{ /if }}
- {{ if $value.type == 'divider' }} {{ /if }}
- {{ if $value.type == 'byline' }}
- {{ if $value.subtype == 'live-update' || $value.subtype == 'live-reporter-insight' }}
-
- {{ if $value.mime == 'text/html' }}{{@ $value.content }}{{ /if }}
- {{ if $value.mime == 'text/plain' }}{{ $value.content }}{{ /if }}
-
- {{ /if}}
- {{ /if }}
- {{ if $value.type == 'date' }}
- {{ if $value.subtype == 'live-update'}}
- {{ if $value.content }}{{ $imports.dayjs.tz($value.content,"America/New_York").locale('en').format('dddd, MMMM D, YYYY h:mm A z') }}{{ /if }}
- {{ /if }}
- {{ /if }}
-{{ /each }}
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/wdfxw/bookfree.ts b/lib/routes/wdfxw/bookfree.tsx
similarity index 95%
rename from lib/routes/wdfxw/bookfree.ts
rename to lib/routes/wdfxw/bookfree.tsx
index 26ebe02b0..f6a6e2b53 100644
--- a/lib/routes/wdfxw/bookfree.ts
+++ b/lib/routes/wdfxw/bookfree.tsx
@@ -1,16 +1,14 @@
-import path from 'node:path';
-
import type { Cheerio, CheerioAPI } from 'cheerio';
import { load } from 'cheerio';
import type { Element } from 'domhandler';
import type { Context } from 'hono';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Data, DataItem, Route } from '@/types';
import { ViewType } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
export const handler = async (ctx: Context): Promise => {
const { id } = ctx.req.param();
@@ -34,16 +32,20 @@ export const handler = async (ctx: Context): Promise => {
const title: string = $aEl.attr('title') ?? $aEl.text();
const image: string | undefined = $el.find('div.img img').attr('data-original') ?? $el.find('div.img img').attr('src');
- const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), {
- images: image
- ? [
- {
- src: image,
- alt: title,
- },
- ]
- : undefined,
- });
+ const description: string | undefined = renderToString(
+
+ );
const linkUrl: string | undefined = $aEl.attr('href');
const processedItem: DataItem = {
@@ -528,3 +530,20 @@ export const route: Route = {
],
view: ViewType.Articles,
};
+
+type WdfxwImage = {
+ src?: string;
+ alt?: string;
+};
+
+const WdfxwDescription = ({ images }: { images?: WdfxwImage[] }) => (
+ <>
+ {images?.map((image) =>
+ image?.src ? (
+
+
+
+ ) : null
+ )}
+ >
+);
diff --git a/lib/routes/wdfxw/templates/description.art b/lib/routes/wdfxw/templates/description.art
deleted file mode 100644
index b6263ea05..000000000
--- a/lib/routes/wdfxw/templates/description.art
+++ /dev/null
@@ -1,13 +0,0 @@
-{{ if images }}
- {{ each images image }}
- {{ if image?.src }}
-
-
-
- {{ /if }}
- {{ /each }}
-{{ /if }}
diff --git a/lib/routes/weibo/search/hot.ts b/lib/routes/weibo/search/hot.tsx
similarity index 85%
rename from lib/routes/weibo/search/hot.ts
rename to lib/routes/weibo/search/hot.tsx
index b39308b99..f2b1042ab 100644
--- a/lib/routes/weibo/search/hot.ts
+++ b/lib/routes/weibo/search/hot.tsx
@@ -1,13 +1,12 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import { config } from '@/config';
import type { Route } from '@/types';
import { ViewType } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
-import { art } from '@/utils/render';
import weiboUtils from '../utils';
@@ -146,6 +145,34 @@ function seekContent(clist) {
const $ = load('
');
const stub = $('#wbcontent');
+ const renderDigest = ({ author, msg, link, postinfo, pics }) =>
+ renderToString(
+ <>
+
+ {msg ? raw(msg) : null}
+ {pics.length ? (
+ <>
+
+
+ {pics.map((pic) => (
+
+
+
+ ))}
+
+ >
+ ) : null}
+
+ >
+ );
+
// To for..of per reviewers comment
// Need to find one clist with 'type ==9'
for (const curitem of clist) {
@@ -160,7 +187,7 @@ function seekContent(clist) {
} else {
curcontent('img').remove();
}
- const section = art(path.join(__dirname, 'template/digest.art'), {
+ const section = renderDigest({
author: {
link: curitem.mblog.user.profile_url,
name: curitem.mblog.user.screen_name,
@@ -168,7 +195,6 @@ function seekContent(clist) {
msg: curcontent.html(),
link: curitem.scheme,
postinfo: curitem.mblog.created_at,
- picnum: wpic === 'true' ? curitem.mblog.pic_num : 0,
pics:
wpic === 'true' && curitem.mblog.pic_num > 0
? curitem.mblog.pics.map((item) => {
diff --git a/lib/routes/weibo/search/template/digest.art b/lib/routes/weibo/search/template/digest.art
deleted file mode 100644
index 065bd12f6..000000000
--- a/lib/routes/weibo/search/template/digest.art
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
-{{@ msg }}
-
-
-{{if picnum > 0 }}
-
-
- {{each pics}}
-
- {{/each}}
-
-{{/if}}
-
diff --git a/lib/routes/wellcee/rent.ts b/lib/routes/wellcee/rent.tsx
similarity index 71%
rename from lib/routes/wellcee/rent.ts
rename to lib/routes/wellcee/rent.tsx
index 612f40a49..c7b00f519 100644
--- a/lib/routes/wellcee/rent.ts
+++ b/lib/routes/wellcee/rent.tsx
@@ -1,17 +1,44 @@
-import path from 'node:path';
-
import type { Context } from 'hono';
+import { renderToString } from 'hono/jsx/dom/server';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import type { Route } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import type { District, House } from './types';
import { baseUrl, getCitys, getDistricts } from './utils';
-const render = (data) => art(path.join(__dirname, 'templates/house.art'), data);
+const render = (item: House) =>
+ renderToString(
+ <>
+ 租金: {item.rent}
+
+ {item.dailyRent ? (
+ <>
+ 日租: {item.dailyRent}
+
+ >
+ ) : null}
+
+ {item.video ? (
+ <>
+
+
+
+
+ >
+ ) : null}
+ {item.imgs?.length
+ ? item.imgs.map((img) => (
+ <>
+
+
+ >
+ ))
+ : null}
+ >
+ );
export const route: Route = {
path: '/rent/:city/:district?',
@@ -63,7 +90,7 @@ async function handler(ctx: Context) {
const items = (response.data.list as House[]).map((item) => ({
title: item.address,
link: `${baseUrl}/rent-apartment/${item.id}`,
- description: render({ item }),
+ description: render(item),
pubDate: parseDate(item.loginTime, 'X'),
author: item.userInfo.name,
category: [...item.tags, ...item.typeTags],
diff --git a/lib/routes/wellcee/templates/house.art b/lib/routes/wellcee/templates/house.art
deleted file mode 100644
index 2fb3b7379..000000000
--- a/lib/routes/wellcee/templates/house.art
+++ /dev/null
@@ -1,18 +0,0 @@
-租金: {{ item.rent }}
-{{ if item.dailyRent }}
-日租: {{ item.dailyRent }}
-{{ /if }}
-
-
-{{ if item.video }}
-
-
-
-
-{{ /if }}
-
-{{ if item.imgs }}
- {{ each item.imgs img }}
-
- {{ /each }}
-{{ /if }}
diff --git a/lib/routes/whu/news.ts b/lib/routes/whu/news.ts
index dd2c9461c..66bd1c861 100644
--- a/lib/routes/whu/news.ts
+++ b/lib/routes/whu/news.ts
@@ -1,13 +1,11 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+import { renderDescription } from './templates/description';
import { domain, getMeta, processItems, processMeta } from './util';
export const route: Route = {
@@ -70,7 +68,7 @@ async function handler(ctx) {
title: item.prop('title') ?? item.find('h4.eclips').text(),
link: new URL(item.prop('href'), rootUrl).href,
pubDate: parseDate(item.find('time').text(), ['YYYY.MM.DD', 'DDYYYY.MM']),
- description: art(path.join(__dirname, 'templates/description.art'), {
+ description: renderDescription({
description: item.find('div.txt p').html(),
image: image.prop('src')
? {
diff --git a/lib/routes/whu/templates/description.art b/lib/routes/whu/templates/description.art
deleted file mode 100644
index 2de99c292..000000000
--- a/lib/routes/whu/templates/description.art
+++ /dev/null
@@ -1,39 +0,0 @@
-{{ if description }}
- {{@ description }}
-{{ /if }}
-
-{{ if image }}
-
-
-
-{{ /if }}
-
-{{ if video }}
-
-
-
-{{ /if }}
-
-{{ if attachments && attachments.length > 0 }}
- 附件
-
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/whu/templates/description.tsx b/lib/routes/whu/templates/description.tsx
new file mode 100644
index 000000000..729e37837
--- /dev/null
+++ b/lib/routes/whu/templates/description.tsx
@@ -0,0 +1,55 @@
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
+
+type ImageData = {
+ src: string;
+ alt?: string;
+ width?: string | number;
+};
+
+type VideoData = {
+ src: string;
+ width?: string | number;
+ height?: string | number;
+};
+
+type Attachment = {
+ link: string;
+ title: string;
+};
+
+type DescriptionData = {
+ description?: string;
+ image?: ImageData;
+ video?: VideoData;
+ attachments?: Attachment[];
+};
+
+export const renderDescription = ({ description, image, video, attachments }: DescriptionData): string =>
+ renderToString(
+ <>
+ {description ? raw(description) : null}
+ {image ? (
+
+
+
+ ) : null}
+ {video ? (
+
+
+
+ ) : null}
+ {attachments?.length ? (
+ <>
+ 附件
+
+ >
+ ) : null}
+ >
+ );
diff --git a/lib/routes/whu/util.ts b/lib/routes/whu/util.ts
index b7557c840..12e5ca908 100644
--- a/lib/routes/whu/util.ts
+++ b/lib/routes/whu/util.ts
@@ -1,12 +1,11 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import timezone from '@/utils/timezone';
+import { renderDescription } from './templates/description';
+
const domain = 'whu.edu.cn';
/**
@@ -48,11 +47,11 @@ const getItemDetail = async (item, rootUrl) => {
// Missing the `src` properties for the images.
// The `src` property should be replaced with the value of `orisrc` to show the image.
- // Replace images in the content with custom art template.
+ // Replace images in the content with custom JSX template.
content('p.vsbcontent_img').each(function () {
const image = content(this).find('img');
content(this).replaceWith(
- art(path.join(__dirname, 'templates/description.art'), {
+ renderDescription({
image: {
src: new URL(image.prop('orisrc'), rootUrl).href,
width: image.prop('width'),
@@ -63,11 +62,11 @@ const getItemDetail = async (item, rootUrl) => {
// Missing the `src` properties for the videos.
// The `src` property should be replaced with the value of `vurl` to play the video.
- // Replace videos in the content with custom art template.
+ // Replace videos in the content with custom JSX template.
content('script[name="_videourl"]').each(function () {
const video = content(this);
video.replaceWith(
- art(path.join(__dirname, 'templates/description.art'), {
+ renderDescription({
video: {
src: new URL(video.prop('vurl').split('?')[0], rootUrl).href,
width: content(video).prop('vwidth'),
@@ -99,7 +98,7 @@ const getItemDetail = async (item, rootUrl) => {
const meta = processMeta(detailResponse);
item.title = getMeta(meta, 'ArticleTitle') ?? item.title;
- item.description = art(path.join(__dirname, 'templates/description.art'), {
+ item.description = renderDescription({
description,
attachments,
});
diff --git a/lib/routes/wikipedia/current-events.ts b/lib/routes/wikipedia/current-events.ts
index 427e1e931..c10ec147f 100644
--- a/lib/routes/wikipedia/current-events.ts
+++ b/lib/routes/wikipedia/current-events.ts
@@ -71,7 +71,7 @@ function parseCurrentEventsTemplate(wikitext: string): string | null {
function stripTemplates(wikitext: string): string {
// Remove MediaWiki template delimiters {{...}} but keep the content
- // This prevents conflicts with art-template's {{ }} delimiters in RSS generation
+ // This prevents conflicts with template delimiters during JSX-based rendering
return wikitext.replaceAll(/\{\{([^}]+)\}\}/g, '$1');
}
@@ -228,7 +228,7 @@ export function wikiToHtml(wikitext: string): string {
let html = wikitext;
// Apply transformations in order
- html = stripTemplates(html); // Must be first to prevent art-template conflicts
+ html = stripTemplates(html); // Must be first to prevent template delimiter conflicts
html = convertWikiLinks(html);
html = convertExternalLinks(html);
html = convertTextFormatting(html);
diff --git a/lib/routes/windsurf/blog.ts b/lib/routes/windsurf/blog.tsx
similarity index 88%
rename from lib/routes/windsurf/blog.ts
rename to lib/routes/windsurf/blog.tsx
index 0a18baa48..9efdd4cf1 100644
--- a/lib/routes/windsurf/blog.ts
+++ b/lib/routes/windsurf/blog.tsx
@@ -1,14 +1,12 @@
-import path from 'node:path';
-
import type { CheerioAPI } from 'cheerio';
import { load } from 'cheerio';
import type { Context } from 'hono';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Data, DataItem, Route } from '@/types';
import { ViewType } from '@/types';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
export const handler = async (ctx: Context): Promise => {
const limit: number = Number.parseInt(ctx.req.query('limit') ?? '10', 10);
@@ -34,17 +32,16 @@ export const handler = async (ctx: Context): Promise => {
const items: DataItem[] = response.posts.slice(0, limit).map((item): DataItem => {
const title: string = item.title;
const image: string | undefined = item.images?.[0];
- const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), {
- images: image
- ? [
- {
- src: image,
- alt: title,
- },
- ]
- : undefined,
- intro: item.summary,
- });
+ const description: string | undefined = renderToString(
+ <>
+ {image ? (
+
+
+
+ ) : null}
+ {item.summary ? {item.summary} : null}
+ >
+ );
const pubDate: number | string = item.date;
const linkUrl: string | undefined = item.slug;
const categories: string[] = item.tags;
diff --git a/lib/routes/windsurf/templates/description.art b/lib/routes/windsurf/templates/description.art
deleted file mode 100644
index 249654e7e..000000000
--- a/lib/routes/windsurf/templates/description.art
+++ /dev/null
@@ -1,21 +0,0 @@
-{{ if images }}
- {{ each images image }}
- {{ if image?.src }}
-
-
-
- {{ /if }}
- {{ /each }}
-{{ /if }}
-
-{{ if intro }}
- {{ intro }}
-{{ /if }}
-
-{{ if description }}
- {{@ description }}
-{{ /if }}
\ No newline at end of file
diff --git a/lib/routes/winstall/templates/desc.art b/lib/routes/winstall/templates/desc.art
deleted file mode 100644
index 998a2ff2c..000000000
--- a/lib/routes/winstall/templates/desc.art
+++ /dev/null
@@ -1,5 +0,0 @@
-{{ if installers }}
- {{ each installers installer }}
- {{ installer }}
- {{ /each }}
-{{ /if }}
diff --git a/lib/routes/winstall/update.ts b/lib/routes/winstall/update.tsx
similarity index 81%
rename from lib/routes/winstall/update.ts
rename to lib/routes/winstall/update.tsx
index 49320057b..8b7778d60 100644
--- a/lib/routes/winstall/update.ts
+++ b/lib/routes/winstall/update.tsx
@@ -1,12 +1,22 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { renderToString } from 'hono/jsx/dom/server';
import { config } from '@/config';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
-import { art } from '@/utils/render';
+
+const renderDesc = (installers?: string[]) =>
+ renderToString(
+ <>
+ {installers?.map((installer) => (
+ <>
+ {installer}
+
+ >
+ ))}
+ >
+ );
export const route: Route = {
path: '/:appId',
@@ -51,9 +61,7 @@ async function handler(ctx) {
const { app } = response.pageProps;
const items = app.versions.map((item) => ({
title: `${app.name} ${item.version}`,
- description: art(path.join(__dirname, 'templates/desc.art'), {
- installers: item.installers,
- }),
+ description: renderDesc(item.installers),
author: app.publisher,
category: app.tags,
guid: `winstall:${appId}:${item.version}`,
diff --git a/lib/routes/wise/pair.ts b/lib/routes/wise/pair.tsx
similarity index 71%
rename from lib/routes/wise/pair.ts
rename to lib/routes/wise/pair.tsx
index 352b4adc8..0338a02cc 100644
--- a/lib/routes/wise/pair.ts
+++ b/lib/routes/wise/pair.tsx
@@ -1,16 +1,41 @@
-import path from 'node:path';
-
import dayjs from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat.js';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
dayjs.extend(customParseFormat);
-const renderDesc = (content) => art(path.join(__dirname, 'templates/description.art'), content);
+const renderDesc = (content) =>
+ renderToString(
+ <>
+
+ {content.source} to {content.target}
+
+
+
+
+
+ Date
+
+
+ Rate
+
+
+
+ {content.yesterday}
+ {content.yRate}
+
+
+ {content.dayBefore}
+ {content.byRate}
+
+
+
+ >
+ );
export const route: Route = {
path: '/pair/:source/:target',
diff --git a/lib/routes/wise/templates/description.art b/lib/routes/wise/templates/description.art
deleted file mode 100644
index 7a49bce5f..000000000
--- a/lib/routes/wise/templates/description.art
+++ /dev/null
@@ -1,17 +0,0 @@
-{{ source }} to {{ target }}
-
-
-
- Date
- Rate
-
-
- {{ yesterday }}
- {{ yRate }}
-
-
- {{ dayBefore }}
- {{ byRate }}
-
-
-
diff --git a/lib/routes/wmc-bj/publish.ts b/lib/routes/wmc-bj/publish.tsx
similarity index 84%
rename from lib/routes/wmc-bj/publish.ts
rename to lib/routes/wmc-bj/publish.tsx
index ec56268dd..e16afd072 100644
--- a/lib/routes/wmc-bj/publish.ts
+++ b/lib/routes/wmc-bj/publish.tsx
@@ -1,11 +1,9 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import timezone from '@/utils/timezone';
export const route: Route = {
@@ -39,11 +37,13 @@ async function handler(ctx) {
{
title: `${datetime} ${title}`,
link: currentUrl,
- description: art(path.join(__dirname, 'templates/description.art'), {
- image: {
- src: img.prop('src').replace(/\/medium\//, '/'),
- },
- }),
+ description: renderToString(
+ img.prop('src') ? (
+
+
+
+ ) : null
+ ),
category: categories,
guid: `${currentUrl}#${datetime}`,
pubDate: timezone(parseDate(/^[A-Za-z]{3}/.test(datetime) ? datetime.replace(/^\w+/, '') : datetime, ['DD MMM HH:mm', 'MM/DD HH:mm']), +0),
diff --git a/lib/routes/wmc-bj/templates/description.art b/lib/routes/wmc-bj/templates/description.art
deleted file mode 100644
index f287a9799..000000000
--- a/lib/routes/wmc-bj/templates/description.art
+++ /dev/null
@@ -1,5 +0,0 @@
-{{ if image?.src }}
-
-
-
-{{ /if }}
diff --git a/lib/routes/wnacg/common.ts b/lib/routes/wnacg/common.tsx
similarity index 88%
rename from lib/routes/wnacg/common.ts
rename to lib/routes/wnacg/common.tsx
index 600a615ea..16cf18bfb 100644
--- a/lib/routes/wnacg/common.ts
+++ b/lib/routes/wnacg/common.tsx
@@ -1,12 +1,11 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import InvalidParameterError from '@/errors/types/invalid-parameter';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
const categories = {
1: '同人誌 漢化',
@@ -92,10 +91,13 @@ export async function handler(ctx) {
item.author = author;
item.category = category;
- item.description = art(path.join(__dirname, 'templates/manga.art'), {
- description,
- imgList,
- });
+ item.description = renderToString(
+ <>
+ {description ? raw(description) : null}
+
+ {imgList ? imgList.map((img) => ) : null}
+ >
+ );
return item;
})
diff --git a/lib/routes/wnacg/templates/manga.art b/lib/routes/wnacg/templates/manga.art
deleted file mode 100644
index b3ae22822..000000000
--- a/lib/routes/wnacg/templates/manga.art
+++ /dev/null
@@ -1,9 +0,0 @@
-{{ if description }}
- {{@ description }}
-{{ /if }}
-
-{{ if imgList }}
- {{ each imgList img }}
-
- {{ /each }}
-{{ /if }}
diff --git a/lib/routes/wsj/templates/article-description.art b/lib/routes/wsj/templates/article-description.art
deleted file mode 100644
index 53569ee56..000000000
--- a/lib/routes/wsj/templates/article-description.art
+++ /dev/null
@@ -1,6 +0,0 @@
-
- {{@ item.subTitle}}
- {{@ item.article}}
-
-
-
diff --git a/lib/routes/wsj/utils.ts b/lib/routes/wsj/utils.tsx
similarity index 90%
rename from lib/routes/wsj/utils.ts
rename to lib/routes/wsj/utils.tsx
index 37fdee011..109c3a785 100644
--- a/lib/routes/wsj/utils.ts
+++ b/lib/routes/wsj/utils.tsx
@@ -1,12 +1,11 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { raw } from 'hono/html';
+import { renderToString } from 'hono/jsx/dom/server';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { PRESETS } from '@/utils/header-generator';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
const parseArticle = (item) =>
cache.tryGet(item.link, async () => {
@@ -89,9 +88,7 @@ const parseArticle = (item) =>
$(e).remove();
});
item.article = article.html();
- item.description = art(path.join(__dirname, 'templates/article-description.art'), {
- item,
- });
+ item.description = renderToString( );
return {
title: item.title,
@@ -108,3 +105,10 @@ const parseArticle = (item) =>
});
export { parseArticle };
+
+const WsjDescription = ({ subTitle, article }: { subTitle?: string; article?: string }) => (
+
+ {subTitle ? raw(subTitle) : null}
+ {article ? raw(article) : null}
+
+);
diff --git a/lib/routes/x-mol/news.ts b/lib/routes/x-mol/news.tsx
similarity index 80%
rename from lib/routes/x-mol/news.ts
rename to lib/routes/x-mol/news.tsx
index a05e06d32..7280dacea 100644
--- a/lib/routes/x-mol/news.ts
+++ b/lib/routes/x-mol/news.tsx
@@ -1,12 +1,10 @@
-import path from 'node:path';
-
import { load } from 'cheerio';
+import { renderToString } from 'hono/jsx/dom/server';
import type { Route } from '@/types';
import cache from '@/utils/cache';
import got from '@/utils/got';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
import timezone from '@/utils/timezone';
import utils from './utils';
@@ -55,10 +53,17 @@ async function handler(ctx) {
return {
title: a.text(),
link: new URL(a.attr('href'), utils.host).href,
- description: art(path.join(__dirname, 'templates/description.art'), {
- image: element.find('img').attr('src').split('?')[0],
- text: element.find('.thsis-div a').text().trim(),
- }),
+ description: renderToString(
+ <>
+ {element.find('img').attr('src') ? (
+ <>
+
+
+ >
+ ) : null}
+ {element.find('.thsis-div a').text().trim() ? {element.find('.thsis-div a').text().trim()}
: null}
+ >
+ ),
author,
pubDate: span.next().length ? timezone(parseDate(span.next().text().trim()), 8) : undefined,
};
diff --git a/lib/routes/x-mol/templates/description.art b/lib/routes/x-mol/templates/description.art
deleted file mode 100644
index e18d50115..000000000
--- a/lib/routes/x-mol/templates/description.art
+++ /dev/null
@@ -1,6 +0,0 @@
-{{ if image }}
-
-{{ /if }}
-{{ if text }}
- {{ text }}
-{{ /if }}
diff --git a/lib/routes/x410/news.ts b/lib/routes/x410/news.ts
index 0b81a9f8d..457377d79 100644
--- a/lib/routes/x410/news.ts
+++ b/lib/routes/x410/news.ts
@@ -1,5 +1,3 @@
-import path from 'node:path';
-
import type { Cheerio, CheerioAPI } from 'cheerio';
import { load } from 'cheerio';
import type { Element } from 'domhandler';
@@ -10,7 +8,8 @@ import { ViewType } from '@/types';
import cache from '@/utils/cache';
import ofetch from '@/utils/ofetch';
import { parseDate } from '@/utils/parse-date';
-import { art } from '@/utils/render';
+
+import { renderDescription } from './templates/description';
export const handler = async (ctx: Context): Promise => {
const limit: number = Number.parseInt(ctx.req.query('limit') ?? '30', 10);
@@ -32,7 +31,7 @@ export const handler = async (ctx: Context): Promise => {
const $aEl: Cheerio = $el.find('h4 a');
const title: string = $aEl.text();
- const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), {
+ const description: string | undefined = renderDescription({
description: $el.find('div#cookbook').html(),
});
const pubDateStr: string | undefined = $el.find('span.updated').text();
@@ -81,7 +80,7 @@ export const handler = async (ctx: Context): Promise => {
});
const title: string = $$('.title').text();
- const description: string | undefined = art(path.join(__dirname, 'templates/description.art'), {
+ const description: string | undefined = renderDescription({
description: $$('div#cookbook').html(),
});
const pubDateStr: string | undefined = $$('meta[property="article:published_time"]').attr('content');
diff --git a/lib/routes/x410/templates/description.art b/lib/routes/x410/templates/description.art
deleted file mode 100644
index dfab19230..000000000
--- a/lib/routes/x410/templates/description.art
+++ /dev/null
@@ -1,17 +0,0 @@
-{{ if images }}
- {{ each images image }}
- {{ if image?.src }}
-
-