diff --git a/.editorconfig b/.editorconfig index a727df3..aba2646 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,11 +2,11 @@ root = true [*] -indent_style = tab +indent_style = space end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true -[*.yml] -indent_style = space +[*.{yml,ts,js}] +indent_size = 2 diff --git a/src/answer.ts b/src/answer.ts index 8a9d168..2039af7 100644 --- a/src/answer.ts +++ b/src/answer.ts @@ -2,18 +2,18 @@ import { Question } from "./question"; import { fixImagesAndLinks, createTemplate, extractReference, FetchError } from "./lib"; export type Answer = { - content: string; - excerpt: string; - author: { - name: string; - url: string; - headline: string; - avatar_url: string; - }; - voteup_count: number; - comment_count: number; - question: Question; - created_time: number; + content: string; + excerpt: string; + author: { + name: string; + url: string; + headline: string; + avatar_url: string; + }; + voteup_count: number; + comment_count: number; + question: Question; + created_time: number; } const template = createTemplate` @@ -97,31 +97,31 @@ const questionTemplate = createTemplate` `; export async function answer(id: string, redirect: boolean, env: Env): Promise { - const url = `https://api.zhihu.com/v4/answers/${id}?include=content%2Cexcerpt%2Cauthor%2Cvoteup_count%2Ccomment_count%2Cquestion%2Ccreated_time%2Cquestion.detail`; - const response = await fetch(url); - if (!response.ok) { - throw new FetchError(response.statusText, response); - } - const data = await response.json(); - const createdTime = new Date(data.created_time * 1000); + const url = `https://api.zhihu.com/v4/answers/${id}?include=content%2Cexcerpt%2Cauthor%2Cvoteup_count%2Ccomment_count%2Cquestion%2Ccreated_time%2Cquestion.detail`; + const response = await fetch(url); + if (!response.ok) { + throw new FetchError(response.statusText, response); + } + const data = await response.json(); + const createdTime = new Date(data.created_time * 1000); - return template({ - title: data.question.title, - url: new URL(`${data.question.id}/answer/${id}`, `https://www.zhihu.com/question/`).href, - content: await fixImagesAndLinks(data.content), - reference: await extractReference(data.content), - excerpt: data.excerpt, - author: data.author.name, - created_time: createdTime.toISOString(), - created_time_formatted: createdTime.toDateString(), - voteup_count: data.voteup_count.toString(), - comment_count: data.comment_count.toString(), - question: data.question.detail.trim().length > 0 ? questionTemplate({ - question: await fixImagesAndLinks(data.question.detail), - }) : '', - redirect: redirect ? 'true' : 'false', - author_url: data.author.url.replace("api.", ""), - headline: data.author.headline, - avatar_url: data.author.avatar_url, - }); + return template({ + title: data.question.title, + url: new URL(`${data.question.id}/answer/${id}`, `https://www.zhihu.com/question/`).href, + content: await fixImagesAndLinks(data.content), + reference: await extractReference(data.content), + excerpt: data.excerpt, + author: data.author.name, + created_time: createdTime.toISOString(), + created_time_formatted: createdTime.toDateString(), + voteup_count: data.voteup_count.toString(), + comment_count: data.comment_count.toString(), + question: data.question.detail.trim().length > 0 ? questionTemplate({ + question: await fixImagesAndLinks(data.question.detail), + }) : '', + redirect: redirect ? 'true' : 'false', + author_url: data.author.url.replace("api.", ""), + headline: data.author.headline, + avatar_url: data.author.avatar_url, + }); } diff --git a/src/article.ts b/src/article.ts index b7c1657..5473093 100644 --- a/src/article.ts +++ b/src/article.ts @@ -1,133 +1,132 @@ import { fixImagesAndLinks, createTemplate, extractReference, FetchError } from "./lib"; export type Article = { - title: string; - content: string; - excerpt: string; - author: { - name: string; - url: string; - headline: string; - avatar_url: string; - }; - created: number; - voteup_count: number; - comment_count: number; - image_url: string; - column: { - title: string; - description: string; - }; + title: string; + content: string; + excerpt: string; + author: { + name: string; + url: string; + headline: string; + avatar_url: string; + }; + created: number; + voteup_count: number; + comment_count: number; + image_url: string; + column: { + title: string; + description: string; + }; } const template = createTemplate` - ${"title"} | FxZhihu - - - - - + ${"title"} | FxZhihu + + + + + - - + + - + - - - + display: flex; + gap: 1em; + } + #avatar { + width: 100px; + height: 100px; + } + .author > div { + flex: 1; + } + a[data-draft-type="link-card"] { + display: block; + } + -
+
-

${"title"}

+

${"title"}

- -
- -

${"headline"}

-
-
- -

${"voteup_count"} πŸ‘ / ${"comment_count"} πŸ’¬

-
-
- ${"content"} - ${"reference"} -
-
-

δΈ“ζ οΌš${"column_title"}

-

${"column_description"}

-
-
+ +
+ +

${"headline"}

+
+ + +

${"voteup_count"} πŸ‘ / ${"comment_count"} πŸ’¬

+
+
+ ${"content"} + ${"reference"} +
+
+

δΈ“ζ οΌš${"column_title"}

+

${"column_description"}

+
+
`; export async function article(id: string, redirect: boolean, env: Env): Promise { - const url = new URL(id, `https://api.zhihu.com/article/`); - const response = await fetch(url); - if (!response.ok) { - throw new FetchError(response.statusText, response); - } - const data = await response.json
(); - const createdTime = new Date(data.created * 1000); + const url = new URL(id, `https://api.zhihu.com/article/`); + const response = await fetch(url); + if (!response.ok) { + throw new FetchError(response.statusText, response); + } + const data = await response.json
(); + const createdTime = new Date(data.created * 1000); - return template({ - title: data.title, - url: new URL(id, `https://zhuanlan.zhihu.com/p/`).href, - content: await fixImagesAndLinks(data.content), - reference: await extractReference(data.content), - excerpt: data.excerpt, - author: data.author.name, - created_time: createdTime.toISOString(), - created_time_formatted: createdTime.toDateString(), - voteup_count: data.voteup_count.toString(), - comment_count: data.comment_count.toString(), - column_title: data.column.title, - column_description: data.column.description, - redirect: redirect ? 'true' : 'false', - author_url: data.author.url.replace("api.", ""), - headline: data.author.headline, - avatar_url: data.author.avatar_url, - image_url: data.image_url, - }); + return template({ + title: data.title, + url: new URL(id, `https://zhuanlan.zhihu.com/p/`).href, + content: await fixImagesAndLinks(data.content), + reference: await extractReference(data.content), + excerpt: data.excerpt, + author: data.author.name, + created_time: createdTime.toISOString(), + created_time_formatted: createdTime.toDateString(), + voteup_count: data.voteup_count.toString(), + comment_count: data.comment_count.toString(), + column_title: data.column.title, + column_description: data.column.description, + redirect: redirect ? 'true' : 'false', + author_url: data.author.url.replace("api.", ""), + headline: data.author.headline, + avatar_url: data.author.avatar_url, + image_url: data.image_url, + }); } diff --git a/src/index.ts b/src/index.ts index ffae650..0db04d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,62 +20,62 @@ import { status } from './status'; const GITHUB_REPO = 'https://github.com/frostming/fxzhihu'; export default { - async fetch(request, env, ctx): Promise { - const url = new URL(request.url); - const path = url.pathname; - let redirect = !['false', 'no'].includes(url.searchParams.get('redirect') || ''); - // Redirect unless the request is coming from Telegram - const referer = request.headers.get('Referer') || ''; - if (!referer.toLowerCase().includes('https://t.me')) { - redirect = false; - } + async fetch(request, env, ctx): Promise { + const url = new URL(request.url); + const path = url.pathname; + let redirect = !['false', 'no'].includes(url.searchParams.get('redirect') || ''); + // Redirect unless the request is coming from Telegram + const referer = request.headers.get('Referer') || ''; + if (!referer.toLowerCase().includes('https://t.me')) { + redirect = false; + } - if (path === '/') { - return Response.redirect(GITHUB_REPO, 302); - } + if (path === '/') { + return Response.redirect(GITHUB_REPO, 302); + } - if (path === '/robots.txt') { - return new Response(`User-agent: * + if (path === '/robots.txt') { + return new Response(`User-agent: * Disallow: / Allow: /question/* Allow: /p/* Allow: /answer/* `); - } + } - for (const { urlPattern, pageFunction } of [ - { urlPattern: new URLPattern({ pathname: "/question/:_id(\\d+)/answer/:id(\\d+)" }), pageFunction: answer }, - { urlPattern: new URLPattern({ pathname: "/answer/:id(\\d+)" }), pageFunction: answer }, - { urlPattern: new URLPattern({ pathname: "/p/:id(\\d+)" }), pageFunction: article }, - { urlPattern: new URLPattern({ pathname: "/question/:id(\\d+)" }), pageFunction: question }, - { urlPattern: new URLPattern({ pathname: "/pin/:id(\\d+)" }), pageFunction: status }, - ]) { - let match = urlPattern.test(url); - if (match) { - const id = urlPattern.exec(url)?.pathname.groups?.id!; - try { - return new Response(await pageFunction(id, redirect, env), { - headers: { - 'Content-Type': 'text/html', - }, - }); - } catch (e: any) { - // add traceback - console.error(e); - if (e.response && (e.code as number) === 4041) { - return new Response(errorPage(e), { - headers: { - 'Content-Type': 'text/html', - }, - }); - } - return e.response || new Response(e.message, { status: 500 }); - } - } - } + for (const { urlPattern, pageFunction } of [ + { urlPattern: new URLPattern({ pathname: "/question/:_id(\\d+)/answer/:id(\\d+)" }), pageFunction: answer }, + { urlPattern: new URLPattern({ pathname: "/answer/:id(\\d+)" }), pageFunction: answer }, + { urlPattern: new URLPattern({ pathname: "/p/:id(\\d+)" }), pageFunction: article }, + { urlPattern: new URLPattern({ pathname: "/question/:id(\\d+)" }), pageFunction: question }, + { urlPattern: new URLPattern({ pathname: "/pin/:id(\\d+)" }), pageFunction: status }, + ]) { + let match = urlPattern.test(url); + if (match) { + const id = urlPattern.exec(url)?.pathname.groups?.id!; + try { + return new Response(await pageFunction(id, redirect, env), { + headers: { + 'Content-Type': 'text/html', + }, + }); + } catch (e: any) { + // add traceback + console.error(e); + if (e.response && (e.code as number) === 4041) { + return new Response(errorPage(e), { + headers: { + 'Content-Type': 'text/html', + }, + }); + } + return e.response || new Response(e.message, { status: 500 }); + } + } + } - // Redirect to the same URL under zhihu.com - const zhihuUrl = new URL(path, `https://www.zhihu.com`).href; - return Response.redirect(zhihuUrl, 302); - }, + // Redirect to the same URL under zhihu.com + const zhihuUrl = new URL(path, `https://www.zhihu.com`).href; + return Response.redirect(zhihuUrl, 302); + }, } satisfies ExportedHandler; diff --git a/src/lib.ts b/src/lib.ts index 2595dc8..f2cf3bb 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -1,105 +1,105 @@ export async function fixImagesAndLinks(html: string) { - const htmlResponse = new Response(html) - // Create a new HTMLRewriter instance - const rewriter = new HTMLRewriter() - // Handle img tags - .on('img', { - element(element) { - const actualsrc = element.getAttribute('data-actualsrc') - if (actualsrc) { - element.setAttribute('src', actualsrc) - // Remove the data-actualsrc attribute - element.removeAttribute('data-actualsrc') - } - } - }) - // Handle links - .on('a', { - element(element) { - const href = element.getAttribute('href') - if (href?.startsWith('https://link.zhihu.com/')) { - try { - const url = new URL(href) - const target = decodeURIComponent(url.searchParams.get('target') || '') - if (target) { - element.setAttribute('href', target) - } - } catch (e) { - // Keep original href if URL parsing fails - console.error('Failed to parse URL:', e) - } - } - } - }) - // Handle u tags - .on('u', { - element(element) { - // Remove the u tag but keep its contents - element.remove() - }, - text(text) { - // Ensure the text content is preserved - text.before(text.text) - } - }) - // Transform the HTML content + const htmlResponse = new Response(html) + // Create a new HTMLRewriter instance + const rewriter = new HTMLRewriter() + // Handle img tags + .on('img', { + element(element) { + const actualsrc = element.getAttribute('data-actualsrc') + if (actualsrc) { + element.setAttribute('src', actualsrc) + // Remove the data-actualsrc attribute + element.removeAttribute('data-actualsrc') + } + } + }) + // Handle links + .on('a', { + element(element) { + const href = element.getAttribute('href') + if (href?.startsWith('https://link.zhihu.com/')) { + try { + const url = new URL(href) + const target = decodeURIComponent(url.searchParams.get('target') || '') + if (target) { + element.setAttribute('href', target) + } + } catch (e) { + // Keep original href if URL parsing fails + console.error('Failed to parse URL:', e) + } + } + } + }) + // Handle u tags + .on('u', { + element(element) { + // Remove the u tag but keep its contents + element.remove() + }, + text(text) { + // Ensure the text content is preserved + text.before(text.text) + } + }) + // Transform the HTML content - return await rewriter.transform(htmlResponse).text() + return await rewriter.transform(htmlResponse).text() } export function createTemplate< - K extends readonly string[] + K extends readonly string[] >(strings: TemplateStringsArray, ...keys: K) { - return (dict: Record) => { - const result = [strings[0]]; - keys.forEach((key, i) => { - result.push(dict[key as K[number]], strings[i + 1]); - }); - return result.join(""); - }; + return (dict: Record) => { + const result = [strings[0]]; + keys.forEach((key, i) => { + result.push(dict[key as K[number]], strings[i + 1]); + }); + return result.join(""); + }; } export async function extractReference(html: string) { - const references = new Map(); - const htmlResponse = new Response(html); + const references = new Map(); + const htmlResponse = new Response(html); - // Create HTMLRewriter instance to collect references - const rewriter = new HTMLRewriter() - .on('sup', { - element(element) { - const text = element.getAttribute('data-text') - const url = element.getAttribute('data-url') - const numero = element.getAttribute('data-numero') + // Create HTMLRewriter instance to collect references + const rewriter = new HTMLRewriter() + .on('sup', { + element(element) { + const text = element.getAttribute('data-text') + const url = element.getAttribute('data-url') + const numero = element.getAttribute('data-numero') - if (text && url && numero) { - references.set(numero, { text, url }) - } - } - }) + if (text && url && numero) { + references.set(numero, { text, url }) + } + } + }) - // Process the HTML to collect references - await rewriter.transform(htmlResponse).text() + // Process the HTML to collect references + await rewriter.transform(htmlResponse).text() - // Generate reference list if any references were found - if (references.size > 0) { - const referenceList = Array.from(references.entries()) - .sort(([a], [b]) => parseInt(a) - parseInt(b)) - .map(([index, { text, url }]) => `${index}. ${text} ${url}`) - .join('
') + // Generate reference list if any references were found + if (references.size > 0) { + const referenceList = Array.from(references.entries()) + .sort(([a], [b]) => parseInt(a) - parseInt(b)) + .map(([index, { text, url }]) => `${index}. ${text} ${url}`) + .join('
') - return `

参考

${referenceList}
` - } + return `

参考

${referenceList}
` + } - // Return empty string if no references found - return '' + // Return empty string if no references found + return '' } export class FetchError extends Error { - response?: Response; + response?: Response; - constructor(message: string, response?: Response) { - super(message); - this.response = response; - } + constructor(message: string, response?: Response) { + super(message); + this.response = response; + } } diff --git a/src/question.ts b/src/question.ts index 7ab80af..ea9e996 100644 --- a/src/question.ts +++ b/src/question.ts @@ -1,78 +1,78 @@ import { createTemplate, FetchError } from "./lib"; export type Question = { - type: 'question'; - id: number; - title: string; - detail: string; - excerpt: string; - created: number; - answer_count: number; - author: { - name: string; - }; + type: 'question'; + id: number; + title: string; + detail: string; + excerpt: string; + created: number; + answer_count: number; + author: { + name: string; + }; }; const template = createTemplate` - ${"title"} - @${"author"} | FxZhihu - - - - - - - + ${"title"} - @${"author"} | FxZhihu + + + + + + + - + -
-

${"title"}

- - -

${"answer_count"} δΈͺε›žη­”

-
-
- ${"content"} -
+
+

${"title"}

+ + +

${"answer_count"} δΈͺε›žη­”

+
+
+ ${"content"} +
`; export async function question(id: string, redirect: boolean, env: Env): Promise { - const response = await fetch(`https://api.zhihu.com/questions/${id}?include=detail%2Cexcerpt%2Canswer_count%2Cauthor`, { - headers: { - cookie: `__zse_ck=${env.ZSE_CK}`, - 'user-agent': 'node' - }, - }); - if (!response.ok) { - throw new FetchError(response.statusText, response); - } + const response = await fetch(`https://api.zhihu.com/questions/${id}?include=detail%2Cexcerpt%2Canswer_count%2Cauthor`, { + headers: { + cookie: `__zse_ck=${env.ZSE_CK}`, + 'user-agent': 'node' + }, + }); + if (!response.ok) { + throw new FetchError(response.statusText, response); + } - const data = await response.json(); - const createdTime = new Date(data.created * 1000); + const data = await response.json(); + const createdTime = new Date(data.created * 1000); - return template({ - title: data.title, - author: data.author.name, - created_time: createdTime.toISOString(), - created_time_formatted: createdTime.toDateString(), - answer_count: data.answer_count.toString(), - content: data.detail, - redirect: redirect ? 'true' : 'false', - url: new URL(id, `https://www.zhihu.com/question/`).href, - excerpt: data.excerpt, - }); + return template({ + title: data.title, + author: data.author.name, + created_time: createdTime.toISOString(), + created_time_formatted: createdTime.toDateString(), + answer_count: data.answer_count.toString(), + content: data.detail, + redirect: redirect ? 'true' : 'false', + url: new URL(id, `https://www.zhihu.com/question/`).href, + excerpt: data.excerpt, + }); } diff --git a/src/status.ts b/src/status.ts index d9cce8e..7c3752b 100644 --- a/src/status.ts +++ b/src/status.ts @@ -3,161 +3,160 @@ import { fixImagesAndLinks, createTemplate, extractReference, FetchError } from const ZHIHU_HOST = 'https://www.zhihu.com'; type Status = { - id: number; - title: string; - content_html: string; - excerpt_title: string; - author: { - name: string; - url: string; - url_token: string; - headline: string; - avatar_url: string; - }; - content: ContentData; - created: number; - updated: number; - reaction: { - statistics: { - up_vote_count: number; - comment_count: number; - }; - }; - origin_pin: Status; + id: number; + title: string; + content_html: string; + excerpt_title: string; + author: { + name: string; + url: string; + url_token: string; + headline: string; + avatar_url: string; + }; + content: ContentData; + created: number; + updated: number; + reaction: { + statistics: { + up_vote_count: number; + comment_count: number; + }; + }; + origin_pin: Status; }; type ContentItem = TextContent | VideoContent | ImageContent; interface TextContent { - title: string; - content: string; - fold_type: string; - own_text: string; - type: 'text'; - text_link_type: string; + title: string; + content: string; + fold_type: string; + own_text: string; + type: 'text'; + text_link_type: string; } interface ImageContent { - url: string; - original_url: string; - is_watermark: boolean; - watermark_url: string; - thumbnail: string; - width: number; - height: number; - type: 'image'; - is_gif: boolean; - is_long: boolean; + url: string; + original_url: string; + is_watermark: boolean; + watermark_url: string; + thumbnail: string; + width: number; + height: number; + type: 'image'; + is_gif: boolean; + is_long: boolean; } interface VideoContent { - status: string; - width: number; - playlist: VideoFormat[]; - type: 'video'; + status: string; + width: number; + playlist: VideoFormat[]; + type: 'video'; } interface VideoFormat { - format: string; - url: string; - bitrate: number; - height: number; - width: number; - fps: number; - duration: number; - quality: string; - size: number; + format: string; + url: string; + bitrate: number; + height: number; + width: number; + fps: number; + duration: number; + quality: string; + size: number; } interface ContentData { - content: ContentItem[]; + content: ContentItem[]; - [Symbol.iterator](): IterableIterator; + [Symbol.iterator](): IterableIterator; } function findVideoUrl(contents: ContentData): string | undefined { - return contents - .content - .find(contentItem => contentItem.type === 'video') - ?.playlist - ?.find(videoItem => videoItem.quality === 'hd') - ?.url; + return contents + .content + .find(contentItem => contentItem.type === 'video') + ?.playlist + ?.find(videoItem => videoItem.quality === 'hd') + ?.url; } const template = createTemplate` - ${'title'} | FxZhihu - - - - - - - - - + ${'title'} | FxZhihu + + + + + + + + + - - - + + -
-

${'title'}

-
- -
- -

${'headline'}

-
-
- - -

${'voteup_count'} πŸ‘ / ${'comment_count'} πŸ’¬

-
-
- ${'content'} - ${'reference'} - ${'videoHtml'} -
- ${'originPinResult'} +
+

${'title'}

+
+ +
+ +

${'headline'}

+
+
+ + +

${'voteup_count'} πŸ‘ / ${'comment_count'} πŸ’¬

+
+
+ ${'content'} + ${'reference'} + ${'videoHtml'} +
+ ${'originPinResult'} `; @@ -191,67 +190,67 @@ const videoContentTemplate = createTemplate`