forked from hplush/slowreader
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathjson-feed.ts
More file actions
185 lines (163 loc) · 4.68 KB
/
json-feed.ts
File metadata and controls
185 lines (163 loc) · 4.68 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
import { getEnvironment } from '../environment.ts'
import { createDownloadTask, type TextResponse } from '../lib/download.ts'
import { type OriginPost, type PostMedia, stringifyMedia } from '../post.ts'
import { createPostsList } from '../posts-list.ts'
import {
findAnchorHrefs,
findDocumentLinks,
findHeaderLinks,
findMediaInText,
isHTML,
type Loader,
toTime
} from './common.ts'
// https://www.jsonfeed.org/version/1.1/
interface JsonFeed {
/** deprecated from 1.1 version */
author?: JsonFeedAuthor
authors?: JsonFeedAuthor[]
description?: string
favicon?: string
feed_url?: string
home_page_url?: string
icon?: string
items: JsonFeedItem[]
next_url?: string
title: string
user_comment?: string
version: string
}
interface JsonFeedAuthor {
avatar?: string
name?: string
url?: string
}
interface JsonFeedItem {
attachments?: JSONFeedAttachment[]
/** deprecated from 1.1 version */
author?: JsonFeedAuthor
authors?: JsonFeedAuthor[]
banner_image?: string
content_html?: string
content_text?: string
date_modified?: string
date_published?: string
external_url?: string
id: string
image?: string
summary?: string
tags?: string[]
title?: string
url?: string
}
interface JSONFeedAttachment {
mime_type: string
url: string
}
interface ValidationRules {
[key: string]: (value: unknown) => boolean
}
const JSON_FEED_VERSIONS = ['1', '1.1']
const JSON_FEED_VALIDATORS = {
items: value => Array.isArray(value),
title: value => typeof value === 'string',
version: value => {
if (typeof value !== 'string' || !value.includes('jsonfeed')) return false
let version = value.split('/').pop()
return JSON_FEED_VERSIONS.includes(version!)
}
} satisfies ValidationRules
function isObject(value: unknown): value is Record<string, unknown> {
return value !== null && typeof value === 'object' && !Array.isArray(value)
}
function stringify(value: unknown): string {
return typeof value === 'object' ? JSON.stringify(value) : String(value)
}
function validate<ValidatedType>(
value: unknown,
rules: ValidationRules
): value is ValidatedType {
if (!isObject(value)) {
return false
}
for (let field in rules) {
if (!(field in value) || !rules[field]!(value[field])) {
getEnvironment().warn(
`JSON feed field '${field}' is not valid with value ` +
stringify(value[field])
)
return false
}
}
return true
}
function parsePostSources(text: TextResponse): JsonFeedItem[] {
let parsedJson = text.parseJson()
if (!validate<JsonFeed>(parsedJson, JSON_FEED_VALIDATORS)) return []
return parsedJson.items
}
function parsePosts(text: TextResponse): OriginPost[] {
return parsePostSources(text).map(item => {
let full = (item.content_html || item.content_text) ?? undefined
let textMedia = findMediaInText(item.content_html)
let postMedia: PostMedia[] = []
if (item.image) {
postMedia.push({ type: 'image', url: item.image })
}
if (item.banner_image) {
postMedia.push({ type: 'image', url: item.banner_image })
}
for (let attachment of item.attachments ?? []) {
if (attachment.url && attachment.mime_type) {
postMedia.push({ type: attachment.mime_type, url: attachment.url })
}
}
return {
full,
intro: item.summary ?? undefined,
media: stringifyMedia([...postMedia, ...textMedia]),
originId: item.id,
publishedAt: toTime(item.date_published) ?? undefined,
title: item.title,
url: item.url ?? undefined
}
})
}
export const jsonFeed: Loader = {
getMineLinksFromText(text) {
let type = 'application/feed+json'
let headerLinks = findHeaderLinks(text, type)
if (!isHTML(text)) return headerLinks
let linksByType = [...headerLinks, ...findDocumentLinks(text, type)]
if (linksByType.length === 0) {
linksByType = findDocumentLinks(text, 'application/json')
}
return [...linksByType, ...findAnchorHrefs(text, /feed\.json/i)]
},
getPosts(task, url, text) {
if (text) {
return createPostsList(parsePosts(text), undefined)
} else {
return createPostsList(undefined, async () => {
return [parsePosts(await task.text(url)), undefined]
})
}
},
async getPostSource(feed, originId) {
let json = await createDownloadTask().text(feed.url)
return parsePostSources(json).find(i => i.id === originId)
},
getSuggestedLinksFromText(text) {
return [new URL('/feed.json', new URL(text.url).origin).href]
},
isMineText(text) {
let parsedJson = text.parseJson()
if (validate<JsonFeed>(parsedJson, JSON_FEED_VALIDATORS)) {
return parsedJson.title
}
return false
},
isMineUrl() {
return undefined
}
}