This commit is contained in:
truewhile
2026-10-01 23:29:49 +08:00
parent 9036ad04e7
commit 6584519bd8
20 changed files with 1072 additions and 79 deletions
+30
View File
@@ -0,0 +1,30 @@
import { isRenderableCover } from './coverUrl.ts'
function check(name: string, condition: boolean) {
if (!condition) throw new Error(`coverUrl: ${name}`)
}
check('empty is not renderable', !isRenderableCover(''))
check('null is not renderable', !isRenderableCover(null))
check('undefined is not renderable', !isRenderableCover(undefined))
check('blank string is not renderable', !isRenderableCover(' '))
check('https cover is renderable', isRenderableCover('https://img.example.com/a.jpg'))
check('http cover is renderable', isRenderableCover('http://img.example.com/a.jpg'))
check('leading spaces are tolerated', isRenderableCover(' https://img.example.com/a.jpg '))
check('uppercase scheme is renderable', isRenderableCover('HTTPS://img.example.com/a.jpg'))
check('inline image data uri is renderable', isRenderableCover('data:image/png;base64,iVBORw0KGgo='))
check('local asset path is renderable', isRenderableCover('/api/reader/local/asset?b=1&p=2&s=3'))
check('protocol relative cover is renderable', isRenderableCover('//img.example.com/a.jpg'))
// 聚合书源把搜索参数信封当封面返回的真实取值
check(
'search envelope data uri is not renderable',
!isRenderableCover(
'data:;base64,eyJrZXkiOiLlhajnkIPpq5jmraYiLCJ0YWIiOiLlsI/or7QiLCJzb3VyY2VzS2V5Ijoi5YWo6YOoIiwicGFnZSI6MSwiZGlzYWJsZWRfc291cmNlcyI6IjAifQ==',
),
)
check('text data uri is not renderable', !isRenderableCover('data:text/plain;base64,aGk='))
check('source name is not renderable', !isRenderableCover('光遇聚合'))
check('bare relative path is not renderable', !isRenderableCover('files/a.jpg'))
check('javascript url is not renderable', !isRenderableCover('javascript:alert(1)'))
console.log('coverUrl.test.ts ok')
+20
View File
@@ -0,0 +1,20 @@
// 判断一个封面地址能否真正被浏览器渲染成图片。
//
// 书源在「这本书没有封面」时不一定返回空值:聚合类书源(如「光遇聚合」)的
// 搜索请求地址本身是 data:;base64,... 参数信封,规则取值为空时会把它当地址
// 兜底返回,于是 cover_url 变成一串 data: 文本。这类地址 media type 为空,
// 浏览器按 text/plain 解码,必然显示破图。
export function isRenderableCover(url?: string | null): boolean {
const value = (url ?? '').trim()
if (!value) return false
const lower = value.toLowerCase()
if (lower.startsWith('http://') || lower.startsWith('https://')) return true
if (lower.startsWith('data:image/')) return true
// 应用自身生成的相对地址(本地书封面 /api/reader/local/asset?...)以及
// //host/path 这种协议相对地址。书源给出的相对地址同样落在这里,加载失败
// 时由 <img> 的 onError 兜底回退成占位图。
if (value.startsWith('/')) return true
return false
}
+20
View File
@@ -0,0 +1,20 @@
import { splitKindTags } from './kindTags.ts'
function check(name: string, condition: boolean) {
if (!condition) throw new Error(`kindTags: ${name}`)
}
const eq = (a: string[], b: string[]) => a.length === b.length && a.every((v, i) => v === b[i])
check('空值返回空数组', splitKindTags(undefined).length === 0)
check('空串返回空数组', splitKindTags('').length === 0)
check('全空段返回空数组', splitKindTags(',,,').length === 0)
// 聚合源的真实取值:只有状态/分类有值,其余为空
check('丢掉空段', eq(splitKindTags('已完结,,,'), ['已完结']))
check('保留有效段顺序', eq(splitKindTags('已完结,玄幻,'), ['已完结', '玄幻']))
check('去掉段内空白', eq(splitKindTags(' 连载 , 都市 '), ['连载', '都市']))
check('无逗号时整体作一段', eq(splitKindTags('有声小说'), ['有声小说']))
// 更新时间段也是合法标签(legado 同样展示)
check('保留更新时间段', eq(splitKindTags(',,有声小说,2018-12-11 22:01:22'), ['有声小说', '2018-12-11 22:01:22']))
console.log('kindTags.test.ts ok')
+11
View File
@@ -0,0 +1,11 @@
// 书源的 kind 规则普遍是逗号拼接的多段,例如聚合源的
// `{{$.status}},{{$.score}},{{$.tags}},{{$.last_chapter_update_time}}`。
//
// 取不到值的段会是空串(如 "已完结,,,"),直接展示会变成 ",,有声小说," 这种噪音,
// 所以按逗号切分后丢掉空段,只保留有效标签(legado 也是按段展示)。
export function splitKindTags(kind?: string | null): string[] {
return (kind ?? '')
.split(',')
.map((s) => s.trim())
.filter(Boolean)
}