@mxfb/tools 1.0.0 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/TODO.md +1 -0
  2. package/agnostic/@publisher/index.d.ts +47 -0
  3. package/agnostic/@publisher/index.js +361 -0
  4. package/agnostic/elo/index.d.ts +23 -0
  5. package/agnostic/elo/index.js +28 -0
  6. package/agnostic/elo/index.test.js +67 -0
  7. package/agnostic/index.d.ts +5 -0
  8. package/agnostic/index.js +5 -0
  9. package/agnostic/poetry/generate-rhymes/index.d.ts +1 -0
  10. package/agnostic/poetry/generate-rhymes/index.js +125 -0
  11. package/agnostic/poetry/index.d.ts +1 -0
  12. package/agnostic/poetry/index.js +1 -0
  13. package/agnostic/scrapers/@meteociel/index.js +240 -186
  14. package/agnostic/scrapers/@meteociel/index.test.js +43 -0
  15. package/agnostic/scrapers/@meteociel/locations/index.js +9 -5
  16. package/agnostic/scrapers/@meteociel/types.js +1 -0
  17. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.d.ts +22 -0
  18. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.js +124 -0
  19. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.d.ts +1 -0
  20. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.js +107 -0
  21. package/agnostic/scrapers/@tumblr/blogs/index.d.ts +8 -0
  22. package/agnostic/scrapers/@tumblr/blogs/index.js +23 -0
  23. package/agnostic/scrapers/@tumblr/index.d.ts +73 -0
  24. package/agnostic/scrapers/@tumblr/index.js +180 -0
  25. package/agnostic/scrapers/@tumblr/index.test.d.ts +1 -0
  26. package/agnostic/scrapers/@tumblr/index.test.js +319 -0
  27. package/agnostic/scrapers/@tumblr/types.d.ts +238 -0
  28. package/agnostic/scrapers/@tumblr/types.js +38 -0
  29. package/agnostic/scrapers/@tumblr/utils.d.ts +12 -0
  30. package/agnostic/scrapers/@tumblr/utils.js +407 -0
  31. package/agnostic/scrapers/index.d.ts +2 -0
  32. package/agnostic/scrapers/index.js +2 -0
  33. package/agnostic/weather/index.d.ts +1 -0
  34. package/agnostic/weather/index.js +1 -0
  35. package/agnostic/weather/types.d.ts +2 -2
  36. package/agnostic/weather/types.js +8 -0
  37. package/components/index.d.ts +1 -0
  38. package/components/index.js +1 -0
  39. package/components/public-classnames.d.ts +5 -0
  40. package/components/public-classnames.js +5 -0
  41. package/components/utils/index.d.ts +1 -0
  42. package/components/utils/index.js +12 -0
  43. package/components/utils/types.d.ts +3 -0
  44. package/components/utils/types.js +1 -0
  45. package/index.d.ts +3 -0
  46. package/index.js +3 -0
  47. package/node/headless-browser/index.d.ts +37 -0
  48. package/node/headless-browser/index.js +126 -0
  49. package/node/headless-browser/types.d.ts +47 -0
  50. package/node/headless-browser/types.js +5 -0
  51. package/node/index.js +1 -0
  52. package/node/index.test.d.ts +1 -0
  53. package/node/index.test.js +6 -0
  54. package/node/scrapers/@tumblr/index.d.ts +61 -0
  55. package/node/scrapers/@tumblr/index.js +182 -0
  56. package/node/scrapers/@tumblr/index.test.d.ts +1 -0
  57. package/node/scrapers/@tumblr/index.test.js +185 -0
  58. package/node/scrapers/@tumblr/types.d.ts +91 -0
  59. package/node/scrapers/@tumblr/types.js +1 -0
  60. package/node/scrapers/@tumblr/utils.d.ts +23 -0
  61. package/node/scrapers/@tumblr/utils.js +328 -0
  62. package/node/scrapers/index.d.ts +1 -0
  63. package/node/scrapers/index.js +1 -0
  64. package/package.json +197 -31
  65. package/chunks/chunk-JQLUR5LO.js +0 -12
  66. package/components/Comp/index.js +0 -0
  67. /package/{components/Comp/index.d.ts → agnostic/elo/index.test.d.ts} +0 -0
@@ -0,0 +1,319 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
+ import { fetchPage, fillPostDetails, parsePage, parseRobots, parseSitemap } from './index.js';
3
+ const blogUrl = 'https://somebody.tumblr.com';
4
+ const listingPage = `<!doctype html>
5
+ <html>
6
+ <head>
7
+ <link rel="canonical" href="https://somebody.tumblr.com/page/2" />
8
+ <link rel="icon" href="/avatar.png" />
9
+ <meta property="og:site_name" content="Somebody's blog" />
10
+ <meta name="description" content="A blog about nothing" />
11
+ </head>
12
+ <body>
13
+ <nav><a href="/about">About</a></nav>
14
+ <a href="/post/1001/first">first</a>
15
+ <a href="/post/1001/first#notes">first again</a>
16
+ <a href="/post/1002/second">second</a>
17
+ <a href="https://elsewhere.tumblr.com/post/9999/foreign">foreign</a>
18
+ <a href="/page/1">previous</a>
19
+ <a href="/page/3">next</a>
20
+ </body>
21
+ </html>`;
22
+ const permalinkPage = `<!doctype html>
23
+ <html>
24
+ <head>
25
+ <link rel="canonical" href="https://somebody.tumblr.com/post/1001/first" />
26
+ <meta property="og:site_name" content="Somebody's blog" />
27
+ <meta property="og:type" content="video.other" />
28
+ <meta property="og:title" content="A video post" />
29
+ <meta property="og:description" content="What this one post is about" />
30
+ <meta property="og:image" content="https://media.tumblr.com/poster.jpg" />
31
+ <meta property="og:image:width" content="640" />
32
+ <meta property="og:image:height" content="480" />
33
+ <meta property="article:published_time" content="2024-03-07T09:30:00Z" />
34
+ <meta property="article:tag" content="cinema" />
35
+ <meta property="article:tag" content="super 8" />
36
+ </head>
37
+ <body></body>
38
+ </html>`;
39
+ const listingPageWithBlocks = `<!doctype html>
40
+ <html>
41
+ <head>
42
+ <link rel="canonical" href="https://somebody.tumblr.com/" />
43
+ <meta property="og:site_name" content="Somebody's blog" />
44
+ </head>
45
+ <body>
46
+ <header><a href="/post/1001/first">featured</a></header>
47
+ <main>
48
+ <div id="post-1001">
49
+ <h2><a href="/post/1001/first">First</a></h2>
50
+ <p>Body of the first</p>
51
+ </div>
52
+ <li>
53
+ <h2><a href="/post/1002/second">Second</a></h2>
54
+ <p>Body of the second</p>
55
+ </li>
56
+ </main>
57
+ <footer><a href="/archive">Archive</a></footer>
58
+ </body>
59
+ </html>`;
60
+ const permalinkPageWithBlock = `<!doctype html>
61
+ <html>
62
+ <head>
63
+ <link rel="canonical" href="https://somebody.tumblr.com/post/1001/first" />
64
+ <meta property="og:site_name" content="Somebody's blog" />
65
+ </head>
66
+ <body>
67
+ <nav><a href="/post/1002/second">Next post</a></nav>
68
+ <article>
69
+ <h1>First</h1>
70
+ <p>Body of the first</p>
71
+ <a href="/post/1001/first">12 notes</a>
72
+ </article>
73
+ </body>
74
+ </html>`;
75
+ describe('scrapers/@tumblr/parsePage', () => {
76
+ it('throws when the content is not a Tumblr page', () => {
77
+ expect(() => parsePage('<html></html>', 'https://somebody.tumblr.com/'))
78
+ .toThrow('not-a-tumblr-page');
79
+ });
80
+ it('reads the blog off a listing page', () => {
81
+ const { blog } = parsePage(listingPage, `${blogUrl}/page/2`);
82
+ expect(blog.name).toBe('somebody');
83
+ expect(blog.title).toBe("Somebody's blog");
84
+ expect(blog.description).toBe('A blog about nothing');
85
+ expect(blog.baseUrl).toBe('https://somebody.tumblr.com');
86
+ expect(blog.avatarUrl).toBe('https://somebody.tumblr.com/avatar.png');
87
+ expect(blog.navLinks).toEqual([
88
+ { label: 'About', url: 'https://somebody.tumblr.com/about' }
89
+ ]);
90
+ });
91
+ it('identifies every post of a listing page once, and only its own', () => {
92
+ const { kind, posts } = parsePage(listingPage, `${blogUrl}/page/2`);
93
+ expect(kind).toBe('index');
94
+ expect(posts.map(post => post.id)).toEqual(['1001', '1002']);
95
+ expect(posts.map(post => post.url)).toEqual([
96
+ 'https://somebody.tumblr.com/post/1001/first',
97
+ 'https://somebody.tumblr.com/post/1002/second'
98
+ ]);
99
+ expect(posts.every(post => post.type === 'unknown')).toBe(true);
100
+ });
101
+ it('walks pagination from the page links it finds', () => {
102
+ const { pagination } = parsePage(listingPage, `${blogUrl}/page/2`);
103
+ expect(pagination.currentPageNumber).toBe(2);
104
+ expect(pagination.previousPageUrl).toBe('https://somebody.tumblr.com/page/1');
105
+ expect(pagination.nextPageUrl).toBe('https://somebody.tumblr.com/page/3');
106
+ });
107
+ it('reads the single post of a permalink page', () => {
108
+ const { kind, posts } = parsePage(permalinkPage, `${blogUrl}/post/1001/first`);
109
+ expect(kind).toBe('permalink');
110
+ expect(posts).toHaveLength(1);
111
+ const [post] = posts;
112
+ expect(post?.id).toBe('1001');
113
+ expect(post?.type).toBe('video');
114
+ expect(post?.title).toBe('A video post');
115
+ expect(post?.tags).toEqual(['cinema', 'super 8']);
116
+ expect(post?.publishedOn?.toISOString()).toBe('2024-03-07T09:30:00.000Z');
117
+ expect(post?.media).toEqual([{
118
+ kind: 'image',
119
+ url: 'https://media.tumblr.com/poster.jpg',
120
+ widthPx: 640,
121
+ heightPx: 480,
122
+ alternativeText: null
123
+ }]);
124
+ });
125
+ it('keeps the raw markup of each block a listing page carries', () => {
126
+ const { posts } = parsePage(listingPageWithBlocks, blogUrl);
127
+ const [first, second] = posts;
128
+ expect(first?.rawHtml).toContain('Body of the first');
129
+ expect(first?.rawHtml).not.toContain('Body of the second');
130
+ expect(first?.rawHtml?.startsWith('<div id="post-1001">')).toBe(true);
131
+ expect(second?.rawHtml).toContain('Body of the second');
132
+ expect(second?.rawHtml?.startsWith('<li>')).toBe(true);
133
+ expect(posts.map(post => post.rawHtmlSource)).toEqual(['listing', 'listing']);
134
+ });
135
+ it('leaves contentHtml to a blog parser rather than guessing a body', () => {
136
+ const { posts } = parsePage(listingPageWithBlocks, blogUrl);
137
+ expect(posts.every(post => post.contentHtml === null)).toBe(true);
138
+ });
139
+ it('keeps the raw markup of the post a permalink page is about', () => {
140
+ const { posts } = parsePage(permalinkPageWithBlock, `${blogUrl}/post/1001/first`);
141
+ const [post] = posts;
142
+ expect(post?.rawHtml?.startsWith('<article>')).toBe(true);
143
+ expect(post?.rawHtml).toContain('Body of the first');
144
+ expect(post?.rawHtml).not.toContain('Next post');
145
+ expect(post?.rawHtmlSource).toBe('permalink');
146
+ });
147
+ it('reports no raw markup when no block could be located', () => {
148
+ const { posts } = parsePage(permalinkPage, `${blogUrl}/post/1001/first`);
149
+ expect(posts[0]?.rawHtml).toBeNull();
150
+ expect(posts[0]?.rawHtmlSource).toBeNull();
151
+ });
152
+ it('never attributes a post description to the blog', () => {
153
+ const { blog } = parsePage(permalinkPage, `${blogUrl}/post/1001/first`);
154
+ expect(blog.description).toBeNull();
155
+ });
156
+ });
157
+ describe('scrapers/@tumblr/fillPostDetails', () => {
158
+ const loadParser = async (blogName) => blogName !== 'somebody'
159
+ ? null
160
+ : {
161
+ parsePostDom: postElement => ({
162
+ detail: {
163
+ heading: postElement.querySelector('h2')?.textContent?.trim() ?? null,
164
+ blockId: postElement.id
165
+ },
166
+ title: postElement.querySelector('h2')?.textContent?.trim() ?? null
167
+ })
168
+ };
169
+ it('fills detail with what the blog parser read out of each block', async () => {
170
+ const page = await fillPostDetails(parsePage(listingPageWithBlocks, blogUrl), loadParser);
171
+ expect(page.posts.map(post => post.detail)).toEqual([
172
+ { heading: 'First', blockId: 'post-1001' },
173
+ { heading: 'Second', blockId: '' }
174
+ ]);
175
+ });
176
+ it('lets the parser complete the standard fields the generic pass left empty', async () => {
177
+ const page = await fillPostDetails(parsePage(listingPageWithBlocks, blogUrl), loadParser);
178
+ expect(page.posts.map(post => post.title)).toEqual(['First', 'Second']);
179
+ });
180
+ it('leaves a page alone when no parser is registered for its blog', async () => {
181
+ const parsed = parsePage(listingPageWithBlocks, 'https://nobody.tumblr.com/');
182
+ const page = await fillPostDetails(parsed, loadParser);
183
+ expect(page).toBe(parsed);
184
+ expect(page.posts.every(post => post.detail === null)).toBe(true);
185
+ });
186
+ it('reports no detail for a post whose block was never located', async () => {
187
+ const page = await fillPostDetails(parsePage(permalinkPage, `${blogUrl}/post/1001/first`), loadParser);
188
+ expect(page.posts[0]?.detail).toBeNull();
189
+ });
190
+ it('carries no parser for any blog yet, so detail stays null by default', async () => {
191
+ const page = await fillPostDetails(parsePage(listingPageWithBlocks, blogUrl));
192
+ expect(page.posts.every(post => post.detail === null)).toBe(true);
193
+ });
194
+ });
195
+ describe('scrapers/@tumblr/fetchPage', () => {
196
+ const originalFetch = global.fetch;
197
+ afterEach(() => {
198
+ global.fetch = originalFetch;
199
+ });
200
+ it('returns the response body as text', async () => {
201
+ global.fetch = vi.fn(async () => ({
202
+ ok: true,
203
+ text: async () => '<html>ok</html>'
204
+ }));
205
+ await expect(fetchPage('https://somebody.tumblr.com')).resolves.toBe('<html>ok</html>');
206
+ });
207
+ it('throws with the status when the response is not a success', async () => {
208
+ global.fetch = vi.fn(async () => ({
209
+ ok: false,
210
+ status: 404,
211
+ text: async () => ''
212
+ }));
213
+ await expect(fetchPage('https://somebody.tumblr.com/page/999'))
214
+ .rejects
215
+ .toThrow('fetch-failed-404');
216
+ });
217
+ });
218
+ describe('scrapers/@tumblr/parseSitemap', () => {
219
+ it('reads a sitemap index', () => {
220
+ const sitemap = parseSitemap(`<?xml version="1.0" encoding="UTF-8"?>
221
+ <sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
222
+ <sitemap><loc>https://somebody.tumblr.com/sitemap-posts1.xml</loc></sitemap>
223
+ <sitemap><loc>https://somebody.tumblr.com/sitemap-posts2.xml</loc></sitemap>
224
+ </sitemapindex>`);
225
+ expect(sitemap.kind).toBe('index');
226
+ expect(sitemap.childSitemapUrls).toEqual([
227
+ 'https://somebody.tumblr.com/sitemap-posts1.xml',
228
+ 'https://somebody.tumblr.com/sitemap-posts2.xml'
229
+ ]);
230
+ expect(sitemap.locations).toEqual([]);
231
+ });
232
+ it('reads a location set', () => {
233
+ const sitemap = parseSitemap(`<?xml version="1.0" encoding="UTF-8"?>
234
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
235
+ <url>
236
+ <loc>https://somebody.tumblr.com/post/1001/first</loc>
237
+ <changefreq>hourly</changefreq>
238
+ <lastmod>2019-01-31T16:39:53+00:00</lastmod>
239
+ </url>
240
+ <url><loc>https://somebody.tumblr.com/post/1002/second</loc></url>
241
+ </urlset>`);
242
+ expect(sitemap.kind).toBe('urlset');
243
+ expect(sitemap.childSitemapUrls).toEqual([]);
244
+ expect(sitemap.locations.map(location => location.url)).toEqual([
245
+ 'https://somebody.tumblr.com/post/1001/first',
246
+ 'https://somebody.tumblr.com/post/1002/second'
247
+ ]);
248
+ });
249
+ it('keeps the date a location was last modified, when it is stated', () => {
250
+ const sitemap = parseSitemap(`<?xml version="1.0" encoding="UTF-8"?>
251
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
252
+ <url>
253
+ <loc>https://somebody.tumblr.com/post/1001/first</loc>
254
+ <lastmod>2019-01-31T16:39:53+00:00</lastmod>
255
+ </url>
256
+ <url><loc>https://somebody.tumblr.com/post/1002/second</loc></url>
257
+ </urlset>`);
258
+ expect(sitemap.locations[0]?.lastModifiedOn?.toISOString()).toBe('2019-01-31T16:39:53.000Z');
259
+ expect(sitemap.locations[1]?.lastModifiedOn).toBeNull();
260
+ });
261
+ it('throws when the document is not a sitemap', () => {
262
+ expect(() => parseSitemap('<html><body>nope</body></html>')).toThrow('not-a-sitemap');
263
+ });
264
+ });
265
+ describe('scrapers/@tumblr/parseRobots', () => {
266
+ it('keeps the sitemaps and the rules stated for every crawler', () => {
267
+ const robots = parseRobots([
268
+ '# a comment',
269
+ 'Sitemap: https://somebody.tumblr.com/sitemap.xml',
270
+ 'User-agent: *',
271
+ 'Disallow: /search',
272
+ 'Allow: /search/public',
273
+ 'Crawl-delay: 2'
274
+ ].join('\n'));
275
+ expect(robots.sitemapUrls).toEqual(['https://somebody.tumblr.com/sitemap.xml']);
276
+ expect(robots.disallowedPaths).toEqual(['/search']);
277
+ expect(robots.allowedPaths).toEqual(['/search/public']);
278
+ expect(robots.crawlDelaySeconds).toBe(2);
279
+ });
280
+ it('ignores the rules of a named crawler', () => {
281
+ const robots = parseRobots([
282
+ 'User-agent: SomeBot',
283
+ 'Disallow: /',
284
+ 'User-agent: *',
285
+ 'Disallow: /archive'
286
+ ].join('\n'));
287
+ expect(robots.disallowedPaths).toEqual(['/archive']);
288
+ });
289
+ it('returns empty lists for an empty file', () => {
290
+ const robots = parseRobots('');
291
+ expect(robots.sitemapUrls).toEqual([]);
292
+ expect(robots.disallowedPaths).toEqual([]);
293
+ expect(robots.allowedPaths).toEqual([]);
294
+ expect(robots.crawlDelaySeconds).toBeNull();
295
+ });
296
+ });
297
+ describe('scrapers/@tumblr/parsePage, against a canonical that names another host', () => {
298
+ const modernPage = `<!doctype html>
299
+ <html>
300
+ <head>
301
+ <link rel="canonical" href="https://www.tumblr.com/somebody" />
302
+ <meta property="og:site_name" content="Somebody's blog" />
303
+ </head>
304
+ <body><a href="/post/1001/first">first</a></body>
305
+ </html>`;
306
+ it('locates the page from the URL it was fetched from, not from the canonical', () => {
307
+ const page = parsePage(modernPage, `${blogUrl}/page/2`);
308
+ expect(page.url).toBe(`${blogUrl}/page/2`);
309
+ expect(page.canonicalUrl).toBe('https://www.tumblr.com/somebody');
310
+ expect(page.kind).toBe('index');
311
+ expect(page.blog.baseUrl).toBe(blogUrl);
312
+ expect(page.pagination.currentPageNumber).toBe(2);
313
+ expect(page.posts.map(post => post.id)).toEqual(['1001']);
314
+ });
315
+ it('reads the blog name from the canonical path rather than the hostname', () => {
316
+ const page = parsePage(modernPage, `${blogUrl}/page/2`);
317
+ expect(page.blog.name).toBe('somebody');
318
+ });
319
+ });
@@ -0,0 +1,238 @@
1
+ /** Tumblr's own post taxonomy, as the platform names it. */
2
+ export declare const tumblrPostTypes: readonly ["text", "photo", "quote", "link", "chat", "audio", "video", "answer", "unknown"];
3
+ /** The kind of content a post carries. */
4
+ export type TumblrPostType = typeof tumblrPostTypes[number];
5
+ /** The kinds of page a blog serves, as told apart by their path. */
6
+ export declare const tumblrPageKinds: readonly ["index", "permalink", "archive", "tagged", "custom", "unknown"];
7
+ /** What a parsed page turned out to be. */
8
+ export type TumblrPageKind = typeof tumblrPageKinds[number];
9
+ /** The media families a post can embed. */
10
+ export declare const tumblrMediaKinds: readonly ["image", "video", "audio", "iframe"];
11
+ /** Which media family a single item belongs to. */
12
+ export type TumblrMediaKind = typeof tumblrMediaKinds[number];
13
+ /** The kinds of page a post's raw markup can have been read off. */
14
+ export declare const tumblrRawHtmlSources: readonly ["listing", "permalink"];
15
+ /** Which kind of page a post's raw markup came from. */
16
+ export type TumblrRawHtmlSource = typeof tumblrRawHtmlSources[number];
17
+ /**
18
+ * One media item attached to a post.
19
+ *
20
+ * @property kind - Which media family the item belongs to.
21
+ * @property url - Absolute URL of the media file.
22
+ * @property widthPx - Intrinsic width, when the page declares one.
23
+ * @property heightPx - Intrinsic height, when the page declares one.
24
+ * @property alternativeText - Textual alternative, when the page declares one.
25
+ */
26
+ export type TumblrMedia = {
27
+ kind: TumblrMediaKind;
28
+ url: string;
29
+ widthPx: number | null;
30
+ heightPx: number | null;
31
+ alternativeText: string | null;
32
+ };
33
+ /**
34
+ * A link a theme exposes in its navigation.
35
+ *
36
+ * @property label - The link's visible text, whitespace-normalized.
37
+ * @property url - Absolute URL the link points at.
38
+ */
39
+ export type TumblrNavLink = {
40
+ label: string;
41
+ url: string;
42
+ };
43
+ /**
44
+ * A single post, as much of it as the parsed page revealed.
45
+ *
46
+ * Every field but `type`, `tags` and `media` is `null` when the page did not carry it —
47
+ * which is the common case on an index page, where posts are only identified.
48
+ *
49
+ * @property id - The numeric post id, as a string.
50
+ * @property url - Absolute permalink.
51
+ * @property type - The post's content type, `'unknown'` when nothing stated it.
52
+ * @property publishedOn - Publication date.
53
+ * @property title - The post's own title, not the blog's.
54
+ * @property contentHtml - The post body as markup, which only a blog's own parser fills:
55
+ * telling a body apart from the theme chrome around it is not something that can be done
56
+ * across themes.
57
+ * @property rawHtml - The post block's markup, exactly as the page carried it, attributes
58
+ * and theme wrappers included. It is the raw material a parser is handed later, and URLs
59
+ * inside it stay relative to the page it came from.
60
+ * @property rawHtmlSource - Which kind of page {@link TumblrPost.rawHtml} was read off.
61
+ * A listing block is whatever the theme chose to show there, so it may well be a teaser of
62
+ * the post a permalink page carries in full.
63
+ * @property detail - What this blog's own parser read out of `rawHtml`, `null` when no
64
+ * parser is registered for the blog or when there was no block to read.
65
+ * @property tags - Tags attached to the post, empty when none were found.
66
+ * @property media - Media items attached to the post, empty when none were found.
67
+ * @property notesCount - Number of notes (likes and reblogs combined).
68
+ * @property reblogOfUrl - Permalink of the post this one reblogs.
69
+ * @property lastModifiedOn - When the post last changed, which a sitemap states for every
70
+ * post of a blog. On a post that was never edited this is also its publication date, but
71
+ * the two are not the same claim and are kept apart.
72
+ */
73
+ export type TumblrPost = {
74
+ id: string | null;
75
+ url: string | null;
76
+ type: TumblrPostType;
77
+ publishedOn: Date | null;
78
+ title: string | null;
79
+ contentHtml: string | null;
80
+ rawHtml: string | null;
81
+ rawHtmlSource: TumblrRawHtmlSource | null;
82
+ detail: TumblrPostDetail | null;
83
+ tags: string[];
84
+ media: TumblrMedia[];
85
+ notesCount: number | null;
86
+ reblogOfUrl: string | null;
87
+ lastModifiedOn: Date | null;
88
+ };
89
+ /**
90
+ * What a blog's own parser read out of one post block.
91
+ *
92
+ * Deliberately open: a theme states things no other theme does — a track's artist, a
93
+ * release year, a label — and inventing a field on {@link TumblrPost} for each of them
94
+ * would make the shape mean less with every blog added. What a given blog puts in here is
95
+ * documented by that blog's own parser.
96
+ */
97
+ export type TumblrPostDetail = Record<string, unknown>;
98
+ /**
99
+ * What a blog's parser read out of one post block.
100
+ *
101
+ * Besides its own `detail`, a parser may complete the standard fields it reads better
102
+ * than the generic pass — which is most of them, a theme being the only thing that knows
103
+ * where it put a date or a title. A field left out, or `null`, keeps whatever the generic
104
+ * pass found.
105
+ *
106
+ * @property detail - What this theme states that no other does.
107
+ * @property title - The post's own title.
108
+ * @property contentHtml - The post body as markup, the one field the generic pass can
109
+ * never fill.
110
+ * @property publishedOn - Publication date.
111
+ * @property tags - Tags attached to the post. An empty list keeps the generic pass's.
112
+ * @property media - Media items attached to the post. An empty list keeps the generic
113
+ * pass's.
114
+ */
115
+ export type TumblrParsedPost = {
116
+ detail: TumblrPostDetail;
117
+ title?: string | null;
118
+ contentHtml?: string | null;
119
+ publishedOn?: Date | null;
120
+ tags?: string[];
121
+ media?: TumblrMedia[];
122
+ };
123
+ /**
124
+ * A single blog's parser, the one thing a theme-specific module must provide.
125
+ *
126
+ * @property parsePostDom - Reads one post block, as located by the generic pass.
127
+ */
128
+ export type TumblrBlogParser = {
129
+ parsePostDom: (postElement: Element) => TumblrParsedPost;
130
+ };
131
+ /**
132
+ * Finds the parser for a blog, by the name `extractBlog` read off the page.
133
+ *
134
+ * @returns That blog's parser, or `null` when none is registered — which is the common
135
+ * case, and means a post keeps only what the generic pass found.
136
+ */
137
+ export type TumblrBlogParserLoader = (blogName: string) => Promise<TumblrBlogParser | null>;
138
+ /**
139
+ * The blog a page belongs to, as much of it as the page revealed.
140
+ *
141
+ * @property name - The blog identifier — the `staff` of `staff.tumblr.com`, `null` on a
142
+ * custom domain.
143
+ * @property title - The blog's display title.
144
+ * @property description - The blog's own description, never a post's.
145
+ * @property baseUrl - Origin the blog is served from.
146
+ * @property avatarUrl - Best guess at the blog avatar, read from the page's icon links.
147
+ * @property navLinks - Links found in the page's navigation, empty when the theme
148
+ * exposes none semantically.
149
+ */
150
+ export type TumblrBlog = {
151
+ name: string | null;
152
+ title: string | null;
153
+ description: string | null;
154
+ baseUrl: string | null;
155
+ avatarUrl: string | null;
156
+ navLinks: TumblrNavLink[];
157
+ };
158
+ /**
159
+ * Where a page sits in the blog's pagination, and where to go next.
160
+ *
161
+ * @property currentPageNumber - 1 for the blog root, the `N` of `/page/N`, `null`
162
+ * elsewhere.
163
+ * @property nextPageUrl - Absolute URL of the following page.
164
+ * @property previousPageUrl - Absolute URL of the preceding page.
165
+ */
166
+ export type TumblrPagination = {
167
+ currentPageNumber: number | null;
168
+ nextPageUrl: string | null;
169
+ previousPageUrl: string | null;
170
+ };
171
+ /**
172
+ * Everything one Tumblr page yielded.
173
+ *
174
+ * @property kind - What the page turned out to be.
175
+ * @property url - The URL the page was fetched from, which is what locates it. Modern
176
+ * Tumblr themes declare the same canonical URL on every listing page, so this is the only
177
+ * field that tells two of them apart.
178
+ * @property canonicalUrl - The URL the page declares for itself, which often points at
179
+ * `www.tumblr.com` rather than the blog's own host.
180
+ * @property blog - The blog the page belongs to.
181
+ * @property posts - One post on a permalink page, every post identified on an index
182
+ * page, empty when none were found.
183
+ * @property pagination - Where the page sits among its siblings.
184
+ * @property collectedOn - When the parse ran.
185
+ */
186
+ export type TumblrPage = {
187
+ kind: TumblrPageKind;
188
+ url: string;
189
+ canonicalUrl: string | null;
190
+ blog: TumblrBlog;
191
+ posts: TumblrPost[];
192
+ pagination: TumblrPagination;
193
+ collectedOn: Date;
194
+ };
195
+ /** The two shapes a sitemap document can take. */
196
+ export declare const tumblrSitemapKinds: readonly ["index", "urlset"];
197
+ /** Whether a sitemap lists other sitemaps or actual locations. */
198
+ export type TumblrSitemapKind = typeof tumblrSitemapKinds[number];
199
+ /**
200
+ * One location a sitemap lists.
201
+ *
202
+ * @property url - The location itself.
203
+ * @property lastModifiedOn - When it last changed, when the sitemap says so.
204
+ */
205
+ export type TumblrSitemapLocation = {
206
+ url: string;
207
+ lastModifiedOn: Date | null;
208
+ };
209
+ /**
210
+ * A parsed sitemap document.
211
+ *
212
+ * @property kind - `'index'` when the document lists other sitemaps, `'urlset'` when it
213
+ * lists locations.
214
+ * @property childSitemapUrls - Sitemaps this one points at, empty on a `'urlset'`.
215
+ * @property locations - Locations this one lists, empty on an `'index'`.
216
+ */
217
+ export type TumblrSitemap = {
218
+ kind: TumblrSitemapKind;
219
+ childSitemapUrls: string[];
220
+ locations: TumblrSitemapLocation[];
221
+ };
222
+ /**
223
+ * The directives a blog's `robots.txt` states for every crawler.
224
+ *
225
+ * Only the `User-agent: *` group is read — a blog singling out a named crawler is not
226
+ * something this parser reports on.
227
+ *
228
+ * @property sitemapUrls - Every `Sitemap:` directive, which are group-independent.
229
+ * @property disallowedPaths - Paths closed to every crawler.
230
+ * @property allowedPaths - Paths explicitly reopened inside a disallowed one.
231
+ * @property crawlDelaySeconds - Requested delay between two requests, when stated.
232
+ */
233
+ export type TumblrRobots = {
234
+ sitemapUrls: string[];
235
+ disallowedPaths: string[];
236
+ allowedPaths: string[];
237
+ crawlDelaySeconds: number | null;
238
+ };
@@ -0,0 +1,38 @@
1
+ /** Tumblr's own post taxonomy, as the platform names it. */
2
+ export const tumblrPostTypes = [
3
+ 'text',
4
+ 'photo',
5
+ 'quote',
6
+ 'link',
7
+ 'chat',
8
+ 'audio',
9
+ 'video',
10
+ 'answer',
11
+ 'unknown'
12
+ ];
13
+ /** The kinds of page a blog serves, as told apart by their path. */
14
+ export const tumblrPageKinds = [
15
+ 'index',
16
+ 'permalink',
17
+ 'archive',
18
+ 'tagged',
19
+ 'custom',
20
+ 'unknown'
21
+ ];
22
+ /** The media families a post can embed. */
23
+ export const tumblrMediaKinds = [
24
+ 'image',
25
+ 'video',
26
+ 'audio',
27
+ 'iframe'
28
+ ];
29
+ /** The kinds of page a post's raw markup can have been read off. */
30
+ export const tumblrRawHtmlSources = [
31
+ 'listing',
32
+ 'permalink'
33
+ ];
34
+ /** The two shapes a sitemap document can take. */
35
+ export const tumblrSitemapKinds = [
36
+ 'index',
37
+ 'urlset'
38
+ ];
@@ -0,0 +1,12 @@
1
+ import type { TumblrBlog, TumblrPageKind, TumblrPagination, TumblrPost, TumblrSitemapLocation } from './types.js';
2
+ export declare function toIntegerOrNull(raw: string | null): number | null;
3
+ export declare function readSitemapChildUrls(document: Document): string[];
4
+ export declare function readSitemapLocations(document: Document): TumblrSitemapLocation[];
5
+ export declare function toOrigin(url: string | null): string | null;
6
+ export declare function readMetaProperty(document: Document, property: string): string | null;
7
+ export declare function extractCanonicalUrl(document: Document): string | null;
8
+ export declare function extractPageKind(pageUrl: string): TumblrPageKind;
9
+ export declare function extractBlog(document: Document, pageUrl: string, canonicalUrl: string | null, pageKind: TumblrPageKind): TumblrBlog;
10
+ export declare function extractPostFromPermalinkPage(document: Document, pageUrl: string): TumblrPost;
11
+ export declare function extractPostsFromListingPage(document: Document, baseUrl: string | null): TumblrPost[];
12
+ export declare function extractPagination(document: Document, pageUrl: string, baseUrl: string | null): TumblrPagination;