@mxfb/tools 0.3.0 → 1.0.2
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.
- package/TODO.md +1 -0
- package/agnostic/@publisher/index.d.ts +47 -0
- package/agnostic/@publisher/index.js +361 -0
- package/agnostic/elo/index.d.ts +23 -0
- package/agnostic/elo/index.js +28 -0
- package/agnostic/elo/index.test.d.ts +1 -0
- package/agnostic/elo/index.test.js +67 -0
- package/agnostic/index.d.ts +5 -4
- package/agnostic/index.js +5 -0
- package/agnostic/poetry/generate-rhymes/index.d.ts +1 -0
- package/agnostic/poetry/generate-rhymes/index.js +125 -0
- package/agnostic/poetry/index.d.ts +1 -0
- package/agnostic/poetry/index.js +1 -0
- package/agnostic/scrapers/@meteociel/index.d.ts +36 -0
- package/agnostic/scrapers/@meteociel/index.js +248 -0
- package/agnostic/scrapers/@meteociel/index.test.d.ts +1 -0
- package/agnostic/scrapers/@meteociel/index.test.js +43 -0
- package/agnostic/scrapers/@meteociel/locations/index.d.ts +11 -0
- package/agnostic/scrapers/@meteociel/locations/index.js +10 -0
- package/agnostic/scrapers/@meteociel/types.d.ts +1 -0
- package/agnostic/scrapers/@meteociel/types.js +1 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.d.ts +22 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.js +124 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.d.ts +1 -0
- package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.js +107 -0
- package/agnostic/scrapers/@tumblr/blogs/index.d.ts +8 -0
- package/agnostic/scrapers/@tumblr/blogs/index.js +23 -0
- package/agnostic/scrapers/@tumblr/index.d.ts +73 -0
- package/agnostic/scrapers/@tumblr/index.js +180 -0
- package/agnostic/scrapers/@tumblr/index.test.d.ts +1 -0
- package/agnostic/scrapers/@tumblr/index.test.js +319 -0
- package/agnostic/scrapers/@tumblr/types.d.ts +238 -0
- package/agnostic/scrapers/@tumblr/types.js +38 -0
- package/agnostic/scrapers/@tumblr/utils.d.ts +12 -0
- package/agnostic/scrapers/@tumblr/utils.js +407 -0
- package/agnostic/scrapers/index.d.ts +2 -0
- package/agnostic/scrapers/index.js +2 -0
- package/agnostic/weather/index.d.ts +1 -0
- package/agnostic/weather/index.js +1 -0
- package/agnostic/weather/types.d.ts +181 -0
- package/agnostic/weather/types.js +8 -0
- package/components/index.d.ts +1 -1
- package/components/index.js +1 -0
- package/components/public-classnames.d.ts +5 -0
- package/components/public-classnames.js +5 -0
- package/components/utils/index.d.ts +1 -0
- package/components/utils/index.js +12 -0
- package/components/utils/types.d.ts +3 -0
- package/components/utils/types.js +1 -0
- package/index.d.ts +3 -3
- package/index.js +3 -0
- package/node/headless-browser/index.d.ts +37 -0
- package/node/headless-browser/index.js +126 -0
- package/node/headless-browser/types.d.ts +47 -0
- package/node/headless-browser/types.js +5 -0
- package/node/index.d.ts +1 -2
- package/node/index.js +1 -0
- package/node/index.test.d.ts +1 -0
- package/node/index.test.js +6 -0
- package/node/scrapers/@tumblr/index.d.ts +61 -0
- package/node/scrapers/@tumblr/index.js +182 -0
- package/node/scrapers/@tumblr/index.test.d.ts +1 -0
- package/node/scrapers/@tumblr/index.test.js +185 -0
- package/node/scrapers/@tumblr/types.d.ts +91 -0
- package/node/scrapers/@tumblr/types.js +1 -0
- package/node/scrapers/@tumblr/utils.d.ts +23 -0
- package/node/scrapers/@tumblr/utils.js +328 -0
- package/node/scrapers/index.d.ts +1 -0
- package/node/scrapers/index.js +1 -0
- package/package.json +211 -14
- package/agnostic/_WIP_tennis/index.d.ts +0 -2
- package/agnostic/_WIP_tennis/index.js +0 -62
- package/agnostic/arrays/index.d.ts +0 -11
- package/agnostic/arrays/is-array-of/index.d.ts +0 -5
- package/agnostic/arrays/is-array-of/index.js +0 -23
- package/agnostic/async/index.d.ts +0 -8
- package/agnostic/async/index.js +0 -20
- package/agnostic/async/timeout-call/index.d.ts +0 -1
- package/agnostic/async/timeout-call/index.js +0 -6
- package/agnostic/async/transitions/index.d.ts +0 -41
- package/agnostic/async/transitions/index.js +0 -7
- package/agnostic/async/wait/index.d.ts +0 -1
- package/agnostic/async/wait/index.js +0 -6
- package/agnostic/booleans/index.d.ts +0 -8
- package/agnostic/booleans/index.js +0 -17
- package/agnostic/booleans/is-falsy/index.d.ts +0 -7
- package/agnostic/booleans/is-falsy/index.js +0 -11
- package/agnostic/css/bem/BEM.d.ts +0 -29
- package/agnostic/css/bem/getNamesArr.d.ts +0 -2
- package/agnostic/css/bem/index.d.ts +0 -7
- package/agnostic/css/bem/index.js +0 -9
- package/agnostic/css/generate-nice-color/index.d.ts +0 -2
- package/agnostic/css/generate-nice-color/index.js +0 -8
- package/agnostic/css/index.d.ts +0 -14
- package/agnostic/css/index.js +0 -177
- package/agnostic/css/is-valid-css-class-name/index.d.ts +0 -2
- package/agnostic/css/is-valid-css-class-name/index.js +0 -8
- package/agnostic/css/styles-set/index.d.ts +0 -37
- package/agnostic/errors/index.d.ts +0 -30
- package/agnostic/errors/index.js +0 -6
- package/agnostic/html/get-node-ancestors/index.d.ts +0 -1
- package/agnostic/html/get-node-ancestors/index.js +0 -6
- package/agnostic/html/get-position-inside-parent/index.d.ts +0 -1
- package/agnostic/html/get-position-inside-parent/index.js +0 -6
- package/agnostic/html/index.d.ts +0 -17
- package/agnostic/html/index.js +0 -39
- package/agnostic/html/insert-node/index.d.ts +0 -2
- package/agnostic/html/insert-node/index.js +0 -6
- package/agnostic/html/placeholders/index.d.ts +0 -9
- package/agnostic/html/placeholders/index.js +0 -6
- package/agnostic/html/sanitize/index.d.ts +0 -24
- package/agnostic/html/sanitize/index.js +0 -9
- package/agnostic/html/selector-to-element/index.d.ts +0 -1
- package/agnostic/html/selector-to-element/index.js +0 -9
- package/agnostic/html/string-to-nodes/index.d.ts +0 -7
- package/agnostic/html/string-to-nodes/index.js +0 -10
- package/agnostic/misc/assert/index.d.ts +0 -5
- package/agnostic/misc/assert/index.js +0 -6
- package/agnostic/misc/cast/index.d.ts +0 -9
- package/agnostic/misc/cast/index.js +0 -8
- package/agnostic/misc/crossenv/index.d.ts +0 -13
- package/agnostic/misc/crossenv/index.js +0 -6
- package/agnostic/misc/elo/index.d.ts +0 -31
- package/agnostic/misc/elo/index.js +0 -6
- package/agnostic/misc/get-current-downlink/index.d.ts +0 -1
- package/agnostic/misc/get-current-downlink/index.js +0 -6
- package/agnostic/misc/index.d.ts +0 -6
- package/agnostic/misc/index.js +0 -16
- package/agnostic/misc/is-constructor-function/index.d.ts +0 -2
- package/agnostic/misc/is-constructor-function/index.js +0 -6
- package/agnostic/misc/is-nullish/index.d.ts +0 -4
- package/agnostic/misc/is-nullish/index.js +0 -10
- package/agnostic/misc/logs/index.d.ts +0 -55
- package/agnostic/misc/logs/index.js +0 -6
- package/agnostic/misc/random/index.d.ts +0 -10
- package/agnostic/misc/random/index.js +0 -6
- package/agnostic/misc/tennis/index.d.ts +0 -60
- package/agnostic/misc/tennis/index.js +0 -6
- package/agnostic/numbers/absolute-modulo/index.d.ts +0 -1
- package/agnostic/numbers/absolute-modulo/index.js +0 -6
- package/agnostic/numbers/clamp/index.d.ts +0 -1
- package/agnostic/numbers/clamp/index.js +0 -6
- package/agnostic/numbers/index.d.ts +0 -13
- package/agnostic/numbers/index.js +0 -30
- package/agnostic/numbers/interpolate/index.d.ts +0 -1
- package/agnostic/numbers/interpolate/index.js +0 -6
- package/agnostic/numbers/responsive-harmonics/index.d.ts +0 -10
- package/agnostic/numbers/responsive-harmonics/index.js +0 -9
- package/agnostic/numbers/round/index.d.ts +0 -1
- package/agnostic/numbers/round/index.js +0 -6
- package/agnostic/objects/enums/index.d.ts +0 -4
- package/agnostic/objects/enums/index.js +0 -7
- package/agnostic/objects/enums/is-in-enum/index.d.ts +0 -1
- package/agnostic/objects/enums/is-in-enum/index.js +0 -6
- package/agnostic/objects/flatten-getters/index.d.ts +0 -1
- package/agnostic/objects/flatten-getters/index.js +0 -9
- package/agnostic/objects/index.d.ts +0 -12
- package/agnostic/objects/index.js +0 -32
- package/agnostic/objects/is-object/index.d.ts +0 -1
- package/agnostic/objects/is-object/index.js +0 -6
- package/agnostic/objects/is-record/index.d.ts +0 -1
- package/agnostic/objects/is-record/index.js +0 -6
- package/agnostic/objects/record-format/index.d.ts +0 -11
- package/agnostic/objects/record-format/index.js +0 -16
- package/agnostic/objects/validation/index.d.ts +0 -3
- package/agnostic/objects/validation/index.js +0 -6
- package/agnostic/optim/index.d.ts +0 -7
- package/agnostic/optim/index.js +0 -18
- package/agnostic/optim/memoize/index.d.ts +0 -3
- package/agnostic/optim/memoize/index.js +0 -6
- package/agnostic/optim/throttle-debounce/index.d.ts +0 -30
- package/agnostic/optim/throttle-debounce/index.js +0 -8
- package/agnostic/optim/throttle-debounce/types.d.ts +0 -2
- package/agnostic/regexps/index.d.ts +0 -23
- package/agnostic/regexps/index.js +0 -89
- package/agnostic/strings/char-codes/index.d.ts +0 -17
- package/agnostic/strings/char-codes/index.js +0 -6
- package/agnostic/strings/index.d.ts +0 -14
- package/agnostic/strings/index.js +0 -32
- package/agnostic/strings/matches/index.d.ts +0 -3
- package/agnostic/strings/matches/index.js +0 -10
- package/agnostic/strings/normalize-indent/index.d.ts +0 -1
- package/agnostic/strings/normalize-indent/index.js +0 -6
- package/agnostic/strings/replace-all/index.d.ts +0 -1
- package/agnostic/strings/replace-all/index.js +0 -6
- package/agnostic/strings/to-alphanum/index.d.ts +0 -1
- package/agnostic/strings/to-alphanum/index.js +0 -6
- package/chunks/chunk-32IRF4OP.js +0 -9
- package/chunks/chunk-34U4HX4V.js +0 -73
- package/chunks/chunk-4GDNHUCZ.js +0 -162
- package/chunks/chunk-4QQBKBYH.js +0 -59
- package/chunks/chunk-4VC5PT5T.js +0 -13
- package/chunks/chunk-5LNUCDQ6.js +0 -132
- package/chunks/chunk-7AWTHZLY.js +0 -12
- package/chunks/chunk-7DBNMU6N.js +0 -14
- package/chunks/chunk-COVPTTAD.js +0 -10
- package/chunks/chunk-DNIOWD7K.js +0 -8
- package/chunks/chunk-DQNJQBC6.js +0 -10
- package/chunks/chunk-E6MSDKON.js +0 -13
- package/chunks/chunk-EDVNAV3G.js +0 -72
- package/chunks/chunk-EW46EXFB.js +0 -8
- package/chunks/chunk-FPEW3A27.js +0 -14
- package/chunks/chunk-GJOU3UAL.js +0 -36
- package/chunks/chunk-HC6ZOHCS.js +0 -14
- package/chunks/chunk-HGCG2J77.js +0 -122
- package/chunks/chunk-IFD7GSGK.js +0 -130
- package/chunks/chunk-JQXNEJAP.js +0 -18
- package/chunks/chunk-JWBDZPQG.js +0 -18
- package/chunks/chunk-K37RALUX.js +0 -8
- package/chunks/chunk-KIONYWA7.js +0 -168
- package/chunks/chunk-KQT5CTGF.js +0 -21
- package/chunks/chunk-LEXQ6E6L.js +0 -31
- package/chunks/chunk-LQFKUNVQ.js +0 -19
- package/chunks/chunk-MF56TTC5.js +0 -15
- package/chunks/chunk-MT74WIQR.js +0 -41
- package/chunks/chunk-NRITRUZW.js +0 -10
- package/chunks/chunk-NRJQKVQV.js +0 -31
- package/chunks/chunk-OSAXBA7G.js +0 -10
- package/chunks/chunk-PO3V3I57.js +0 -23
- package/chunks/chunk-QXAJXTXV.js +0 -19
- package/chunks/chunk-RCO57B6F.js +0 -9
- package/chunks/chunk-SQZGZ3VT.js +0 -15
- package/chunks/chunk-TTZHPIH7.js +0 -237
- package/chunks/chunk-UYTXXUU7.js +0 -54
- package/chunks/chunk-VTPRO4NJ.js +0 -10
- package/chunks/chunk-VYW4IADX.js +0 -35
- package/chunks/chunk-VZDUZTW6.js +0 -113
- package/chunks/chunk-W5A2TON3.js +0 -10
- package/chunks/chunk-W7YBGJ4H.js +0 -28
- package/chunks/chunk-WLL3FNVL.js +0 -40
- package/chunks/chunk-WOAYU6LB.js +0 -13
- package/chunks/chunk-XA4HVHJ4.js +0 -18
- package/chunks/chunk-XN3PZIVL.js +0 -38
- package/chunks/chunk-Y3OZHZUF.js +0 -237
- package/chunks/chunk-YDIBNEGA.js +0 -9
- package/chunks/chunk-ZC5RZBNY.js +0 -33
- package/components/Comp/index.d.ts +0 -1
- package/node/files/index.d.ts +0 -9
- package/node/files/index.js +0 -21
- package/node/files/is-in-directory/index.d.ts +0 -1
- package/node/files/is-in-directory/index.js +0 -6
- package/node/files/read-write/index.d.ts +0 -9
- package/node/files/read-write/index.js +0 -6
- package/node/files/subpaths/index.d.ts +0 -34
- package/node/files/subpaths/index.js +0 -7
- package/node/image/index.d.ts +0 -9
- package/node/image/index.js +0 -74
- package/node/mxfb-cli-config/index.d.ts +0 -19
- package/node/mxfb-cli-config/index.js +0 -113
- package/node/process/index.d.ts +0 -8
- package/node/process/index.js +0 -20
- package/node/process/on-exit/index.d.ts +0 -5
- package/node/process/on-exit/index.js +0 -14
- package/shared/errors/index.d.ts +0 -138
- package/shared/index.d.ts +0 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { JSDOM } from 'jsdom';
|
|
3
|
+
import { parser } from './index.js';
|
|
4
|
+
// Trimmed from a real block of the blog, keeping every part the parser reads.
|
|
5
|
+
const videoPostBlock = `<article id="post-182451761040" class="post type-video">
|
|
6
|
+
<div class="post-panel">
|
|
7
|
+
<div class="media loading">
|
|
8
|
+
<iframe width="500" height="281" id="youtube_iframe"
|
|
9
|
+
src="https://www.youtube.com/embed/7UWhdL76FKI?feature=oembed"
|
|
10
|
+
title="Murcof x Vanessa Wagner - Gnossienne No. 3 (Album Version)"></iframe>
|
|
11
|
+
</div>
|
|
12
|
+
<div class="copy"><p>Murcof & Vanessa Wagner — Gnossienne No. 3<br></p></div>
|
|
13
|
+
<div class="meta">
|
|
14
|
+
<ul class="meta-list">
|
|
15
|
+
<li class="date">
|
|
16
|
+
<a href="https://delabienbonnemusique.tumblr.com/post/182451761040/x"
|
|
17
|
+
title="Jeu. janvier 31, 2019 @ 5:39 pm"><span class="icon"></span>il y a 7 ans</a>
|
|
18
|
+
</li>
|
|
19
|
+
</ul>
|
|
20
|
+
<div class="share"><div class="short-link">
|
|
21
|
+
<input type="text" readonly value="https://tmblr.co/ZknzVy2fw_E_G">
|
|
22
|
+
</div></div>
|
|
23
|
+
</div>
|
|
24
|
+
</div>
|
|
25
|
+
</article>`;
|
|
26
|
+
const audioPostBlock = `<article id="post-170586407310" class="post type-audio">
|
|
27
|
+
<div class="post-panel">
|
|
28
|
+
<div class="copy"><p>Salvatore Adamo — Tombe la neige</p></div>
|
|
29
|
+
<div class="copy source">
|
|
30
|
+
<p>Source : <a href="https://open.spotify.com/track/6NVx24kOU2B8IZ0HsY2fUz">Spotify</a></p>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="meta"><ul class="meta-list"><li class="date">
|
|
33
|
+
<a href="#" title="Mar. février 6, 2018 @ 10:38 pm">il y a 8 ans</a>
|
|
34
|
+
</li></ul></div>
|
|
35
|
+
</div>
|
|
36
|
+
</article>`;
|
|
37
|
+
function parse(block) {
|
|
38
|
+
const { document } = new JSDOM(`<body>${block}</body>`).window;
|
|
39
|
+
const postElement = document.body.firstElementChild;
|
|
40
|
+
if (postElement === null)
|
|
41
|
+
throw new Error('no-block');
|
|
42
|
+
return parser.parsePostDom(postElement);
|
|
43
|
+
}
|
|
44
|
+
describe('scrapers/@tumblr/blogs/delabienbonnemusique', () => {
|
|
45
|
+
it('keeps the title line whole rather than guessing an artist out of it', () => {
|
|
46
|
+
const { title } = parse(videoPostBlock);
|
|
47
|
+
expect(title).toBe('Murcof & Vanessa Wagner — Gnossienne No. 3');
|
|
48
|
+
});
|
|
49
|
+
it('reads the only publication date the blog states, as UTC', () => {
|
|
50
|
+
const { publishedOn, detail } = parse(videoPostBlock);
|
|
51
|
+
expect(publishedOn?.toISOString()).toBe('2019-01-31T17:39:00.000Z');
|
|
52
|
+
expect(detail.publishedLabel).toBe('Jeu. janvier 31, 2019 @ 5:39 pm');
|
|
53
|
+
});
|
|
54
|
+
it('reads a morning hour without pushing it into the afternoon', () => {
|
|
55
|
+
const { publishedOn } = parse(videoPostBlock.replace('5:39 pm', '11:42 am'));
|
|
56
|
+
expect(publishedOn?.toISOString()).toBe('2019-01-31T11:42:00.000Z');
|
|
57
|
+
});
|
|
58
|
+
it('reads midnight and noon the way the theme means them', () => {
|
|
59
|
+
expect(parse(videoPostBlock.replace('5:39 pm', '12:30 am')).publishedOn?.toISOString())
|
|
60
|
+
.toBe('2019-01-31T00:30:00.000Z');
|
|
61
|
+
expect(parse(videoPostBlock.replace('5:39 pm', '12:30 pm')).publishedOn?.toISOString())
|
|
62
|
+
.toBe('2019-01-31T12:30:00.000Z');
|
|
63
|
+
});
|
|
64
|
+
it('keeps the post body, the embed, and the embed title a dead video leaves behind', () => {
|
|
65
|
+
const { contentHtml, media, detail } = parse(videoPostBlock);
|
|
66
|
+
expect(contentHtml).toBe('<p>Murcof & Vanessa Wagner — Gnossienne No. 3<br></p>');
|
|
67
|
+
expect(media).toEqual([{
|
|
68
|
+
kind: 'iframe',
|
|
69
|
+
url: 'https://www.youtube.com/embed/7UWhdL76FKI?feature=oembed',
|
|
70
|
+
widthPx: 500,
|
|
71
|
+
heightPx: 281,
|
|
72
|
+
alternativeText: 'Murcof x Vanessa Wagner - Gnossienne No. 3 (Album Version)'
|
|
73
|
+
}]);
|
|
74
|
+
expect(detail.postType).toBe('video');
|
|
75
|
+
expect(detail.embedTitle).toBe('Murcof x Vanessa Wagner - Gnossienne No. 3 (Album Version)');
|
|
76
|
+
});
|
|
77
|
+
it('takes the embed as the source of a video post', () => {
|
|
78
|
+
const { detail } = parse(videoPostBlock);
|
|
79
|
+
expect(detail.sourceUrl).toBe('https://www.youtube.com/embed/7UWhdL76FKI?feature=oembed');
|
|
80
|
+
expect(detail.sourceUrls).toEqual([
|
|
81
|
+
'https://www.youtube.com/embed/7UWhdL76FKI?feature=oembed'
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
it('never takes a share button or a permalink for a source', () => {
|
|
85
|
+
const { detail } = parse(videoPostBlock);
|
|
86
|
+
expect(JSON.stringify(detail.sourceUrls)).not.toContain('tmblr.co');
|
|
87
|
+
expect(JSON.stringify(detail.sourceUrls)).not.toContain('/post/182451761040');
|
|
88
|
+
});
|
|
89
|
+
it('reads the source link of a post that has one, and never reads it as the body', () => {
|
|
90
|
+
const { detail, contentHtml, media } = parse(audioPostBlock);
|
|
91
|
+
expect(detail.sourceUrl).toBe('https://open.spotify.com/track/6NVx24kOU2B8IZ0HsY2fUz');
|
|
92
|
+
expect(contentHtml).toBe('<p>Salvatore Adamo — Tombe la neige</p>');
|
|
93
|
+
expect(media).toEqual([]);
|
|
94
|
+
});
|
|
95
|
+
it('states what it could not find rather than inventing it', () => {
|
|
96
|
+
const { detail, publishedOn, title } = parse('<article class="post"><div></div></article>');
|
|
97
|
+
expect(title).toBeNull();
|
|
98
|
+
expect(publishedOn).toBeNull();
|
|
99
|
+
expect(detail).toEqual({
|
|
100
|
+
postType: null,
|
|
101
|
+
publishedLabel: null,
|
|
102
|
+
sourceUrl: null,
|
|
103
|
+
sourceUrls: [],
|
|
104
|
+
embedTitle: null
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { TumblrBlogParser } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Loads the parser registered for a blog, importing its module on first use.
|
|
4
|
+
*
|
|
5
|
+
* @param blogName - The blog identifier, as `TumblrBlog.name` states it.
|
|
6
|
+
* @returns That blog's parser, or `null` when none is registered.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadBlogParser(blogName: string): Promise<TumblrBlogParser | null>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every blog this package carries a parser for, by the name its host states.
|
|
3
|
+
*
|
|
4
|
+
* Each entry is a dynamic import rather than a static one: a consumer reading one blog
|
|
5
|
+
* has no use for the parsers of the others, and a static import would put all of them in
|
|
6
|
+
* their bundle.
|
|
7
|
+
*/
|
|
8
|
+
const loadersByBlogName = {
|
|
9
|
+
delabienbonnemusique: async () => await import('./delabienbonnemusique/index.js')
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Loads the parser registered for a blog, importing its module on first use.
|
|
13
|
+
*
|
|
14
|
+
* @param blogName - The blog identifier, as `TumblrBlog.name` states it.
|
|
15
|
+
* @returns That blog's parser, or `null` when none is registered.
|
|
16
|
+
*/
|
|
17
|
+
export async function loadBlogParser(blogName) {
|
|
18
|
+
const load = loadersByBlogName[blogName];
|
|
19
|
+
if (load === undefined)
|
|
20
|
+
return null;
|
|
21
|
+
const { parser } = await load();
|
|
22
|
+
return parser;
|
|
23
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { TumblrBlogParserLoader, TumblrPage, TumblrRobots, TumblrSitemap } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetches one page and decodes it as UTF-8, which is what Tumblr serves.
|
|
4
|
+
*
|
|
5
|
+
* @param url - Absolute URL of the page to fetch.
|
|
6
|
+
* @returns The page's raw content.
|
|
7
|
+
* @throws If the response status is not a success, as `fetch-failed-<status>`, so a
|
|
8
|
+
* caller can tell a missing page from an unparseable one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function fetchPage(url: string): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Extracts everything a single Tumblr page reveals about its blog and its posts.
|
|
13
|
+
*
|
|
14
|
+
* Reads only the markup Tumblr itself injects — canonical and icon links, Open Graph and
|
|
15
|
+
* `article:*` metas, and the `/post/<id>` permalink pattern — so it holds across themes
|
|
16
|
+
* rather than against one. Theme markup is never interpreted, only kept: each post
|
|
17
|
+
* carries its block's raw HTML in `rawHtml`, which is what a blog's own parser is handed
|
|
18
|
+
* to extract the fields no theme-agnostic rule can reach.
|
|
19
|
+
*
|
|
20
|
+
* Everything that locates the page is derived from `pageUrl`, never from the canonical
|
|
21
|
+
* link: a modern theme points its canonical at `www.tumblr.com/<blog>`, which names a
|
|
22
|
+
* different host than the one being read and is identical on every listing page.
|
|
23
|
+
*
|
|
24
|
+
* @param pageContent - The page's raw HTML.
|
|
25
|
+
* @param pageUrl - The URL the content was fetched from.
|
|
26
|
+
* @returns The page's blog, posts and pagination, each field `null` or empty where the
|
|
27
|
+
* page stated nothing.
|
|
28
|
+
* @throws If the content declares neither a canonical URL nor an Open Graph site name,
|
|
29
|
+
* which is taken to mean it is not a Tumblr page.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parsePage(pageContent: string, pageUrl: string): TumblrPage;
|
|
32
|
+
/**
|
|
33
|
+
* Fills each post's `detail` with what the blog's own parser reads out of its block.
|
|
34
|
+
*
|
|
35
|
+
* The generic pass stops at the markup: it locates a post's block and keeps it, but a
|
|
36
|
+
* title, a date or a track listing sit wherever a theme decided, which no cross-theme rule
|
|
37
|
+
* reaches. This is where a blog's own parser gets its turn, and it is loaded by blog name
|
|
38
|
+
* on first use — a consumer reading one blog never imports the parsers of the others.
|
|
39
|
+
*
|
|
40
|
+
* A page whose blog has no registered parser comes back untouched, which is the common
|
|
41
|
+
* case.
|
|
42
|
+
*
|
|
43
|
+
* @param page - A page as `parsePage` returned it.
|
|
44
|
+
* @param loadParser - Where to look a parser up, for a consumer registering their own.
|
|
45
|
+
* Defaults to the parsers this package carries.
|
|
46
|
+
* @returns The page, with `detail` filled on every post whose block could be read, and
|
|
47
|
+
* the standard fields the parser completed.
|
|
48
|
+
* @throws Whatever the blog's parser throws — a parser that breaks on a post is a bug in
|
|
49
|
+
* that parser, and a crawl reports the page rather than storing a post that looks complete.
|
|
50
|
+
*/
|
|
51
|
+
export declare function fillPostDetails(page: TumblrPage, loadParser?: TumblrBlogParserLoader): Promise<TumblrPage>;
|
|
52
|
+
/**
|
|
53
|
+
* Reads a `sitemap.xml` document, whether it indexes other sitemaps or lists locations.
|
|
54
|
+
*
|
|
55
|
+
* A blog's sitemap enumerates every post without any theme involvement, which makes it
|
|
56
|
+
* the cheapest and most reliable way to discover what a blog holds.
|
|
57
|
+
*
|
|
58
|
+
* @param sitemapContent - The sitemap's raw XML.
|
|
59
|
+
* @returns The sitemaps it points at, or the locations it lists with the date each was
|
|
60
|
+
* last modified.
|
|
61
|
+
* @throws If the document's root is neither `sitemapindex` nor `urlset`, as
|
|
62
|
+
* `not-a-sitemap`.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseSitemap(sitemapContent: string): TumblrSitemap;
|
|
65
|
+
/**
|
|
66
|
+
* Reads a `robots.txt`, keeping the `Sitemap:` directives and the rules stated for every
|
|
67
|
+
* crawler.
|
|
68
|
+
*
|
|
69
|
+
* @param robotsContent - The file's raw text.
|
|
70
|
+
* @returns The sitemaps it advertises and the `User-agent: *` rules, each list empty when
|
|
71
|
+
* the file states none.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseRobots(robotsContent: string): TumblrRobots;
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
import { loadBlogParser } from './blogs/index.js';
|
|
3
|
+
import { extractBlog, extractCanonicalUrl, extractPageKind, extractPagination, extractPostFromPermalinkPage, extractPostsFromListingPage, readMetaProperty, readSitemapChildUrls, readSitemapLocations, toIntegerOrNull, toOrigin } from './utils.js';
|
|
4
|
+
/**
|
|
5
|
+
* Fetches one page and decodes it as UTF-8, which is what Tumblr serves.
|
|
6
|
+
*
|
|
7
|
+
* @param url - Absolute URL of the page to fetch.
|
|
8
|
+
* @returns The page's raw content.
|
|
9
|
+
* @throws If the response status is not a success, as `fetch-failed-<status>`, so a
|
|
10
|
+
* caller can tell a missing page from an unparseable one.
|
|
11
|
+
*/
|
|
12
|
+
export async function fetchPage(url) {
|
|
13
|
+
const response = await fetch(url);
|
|
14
|
+
if (!response.ok)
|
|
15
|
+
throw new Error(`fetch-failed-${response.status}`);
|
|
16
|
+
const content = await response.text();
|
|
17
|
+
return content;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Extracts everything a single Tumblr page reveals about its blog and its posts.
|
|
21
|
+
*
|
|
22
|
+
* Reads only the markup Tumblr itself injects — canonical and icon links, Open Graph and
|
|
23
|
+
* `article:*` metas, and the `/post/<id>` permalink pattern — so it holds across themes
|
|
24
|
+
* rather than against one. Theme markup is never interpreted, only kept: each post
|
|
25
|
+
* carries its block's raw HTML in `rawHtml`, which is what a blog's own parser is handed
|
|
26
|
+
* to extract the fields no theme-agnostic rule can reach.
|
|
27
|
+
*
|
|
28
|
+
* Everything that locates the page is derived from `pageUrl`, never from the canonical
|
|
29
|
+
* link: a modern theme points its canonical at `www.tumblr.com/<blog>`, which names a
|
|
30
|
+
* different host than the one being read and is identical on every listing page.
|
|
31
|
+
*
|
|
32
|
+
* @param pageContent - The page's raw HTML.
|
|
33
|
+
* @param pageUrl - The URL the content was fetched from.
|
|
34
|
+
* @returns The page's blog, posts and pagination, each field `null` or empty where the
|
|
35
|
+
* page stated nothing.
|
|
36
|
+
* @throws If the content declares neither a canonical URL nor an Open Graph site name,
|
|
37
|
+
* which is taken to mean it is not a Tumblr page.
|
|
38
|
+
*/
|
|
39
|
+
export function parsePage(pageContent, pageUrl) {
|
|
40
|
+
const collectedOn = new Date();
|
|
41
|
+
const { document } = new JSDOM(pageContent).window;
|
|
42
|
+
const canonicalUrl = extractCanonicalUrl(document);
|
|
43
|
+
if (canonicalUrl === null && readMetaProperty(document, 'og:site_name') === null) {
|
|
44
|
+
throw new Error('not-a-tumblr-page');
|
|
45
|
+
}
|
|
46
|
+
const baseUrl = toOrigin(pageUrl);
|
|
47
|
+
const kind = extractPageKind(pageUrl);
|
|
48
|
+
return {
|
|
49
|
+
kind,
|
|
50
|
+
url: pageUrl,
|
|
51
|
+
canonicalUrl,
|
|
52
|
+
blog: extractBlog(document, pageUrl, canonicalUrl, kind),
|
|
53
|
+
posts: kind === 'permalink'
|
|
54
|
+
? [extractPostFromPermalinkPage(document, pageUrl)]
|
|
55
|
+
: extractPostsFromListingPage(document, baseUrl),
|
|
56
|
+
pagination: extractPagination(document, pageUrl, baseUrl),
|
|
57
|
+
collectedOn
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Fills each post's `detail` with what the blog's own parser reads out of its block.
|
|
62
|
+
*
|
|
63
|
+
* The generic pass stops at the markup: it locates a post's block and keeps it, but a
|
|
64
|
+
* title, a date or a track listing sit wherever a theme decided, which no cross-theme rule
|
|
65
|
+
* reaches. This is where a blog's own parser gets its turn, and it is loaded by blog name
|
|
66
|
+
* on first use — a consumer reading one blog never imports the parsers of the others.
|
|
67
|
+
*
|
|
68
|
+
* A page whose blog has no registered parser comes back untouched, which is the common
|
|
69
|
+
* case.
|
|
70
|
+
*
|
|
71
|
+
* @param page - A page as `parsePage` returned it.
|
|
72
|
+
* @param loadParser - Where to look a parser up, for a consumer registering their own.
|
|
73
|
+
* Defaults to the parsers this package carries.
|
|
74
|
+
* @returns The page, with `detail` filled on every post whose block could be read, and
|
|
75
|
+
* the standard fields the parser completed.
|
|
76
|
+
* @throws Whatever the blog's parser throws — a parser that breaks on a post is a bug in
|
|
77
|
+
* that parser, and a crawl reports the page rather than storing a post that looks complete.
|
|
78
|
+
*/
|
|
79
|
+
export async function fillPostDetails(page, loadParser = loadBlogParser) {
|
|
80
|
+
const blogName = page.blog.name;
|
|
81
|
+
if (blogName === null)
|
|
82
|
+
return page;
|
|
83
|
+
const parser = await loadParser(blogName);
|
|
84
|
+
if (parser === null)
|
|
85
|
+
return page;
|
|
86
|
+
return {
|
|
87
|
+
...page,
|
|
88
|
+
posts: page.posts.map(post => {
|
|
89
|
+
const parsed = parsePostBlock(parser, post);
|
|
90
|
+
return parsed === null ? post : applyParsedPost(post, parsed);
|
|
91
|
+
})
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
function parsePostBlock(parser, post) {
|
|
95
|
+
if (post.rawHtml === null)
|
|
96
|
+
return null;
|
|
97
|
+
const { document } = new JSDOM(`<body>${post.rawHtml}</body>`).window;
|
|
98
|
+
const postElement = document.body.firstElementChild;
|
|
99
|
+
if (postElement === null)
|
|
100
|
+
return null;
|
|
101
|
+
return parser.parsePostDom(postElement);
|
|
102
|
+
}
|
|
103
|
+
function applyParsedPost(post, parsed) {
|
|
104
|
+
const { tags = [], media = [] } = parsed;
|
|
105
|
+
return {
|
|
106
|
+
...post,
|
|
107
|
+
detail: parsed.detail,
|
|
108
|
+
title: parsed.title ?? post.title,
|
|
109
|
+
contentHtml: parsed.contentHtml ?? post.contentHtml,
|
|
110
|
+
publishedOn: parsed.publishedOn ?? post.publishedOn,
|
|
111
|
+
tags: tags.length > 0 ? tags : post.tags,
|
|
112
|
+
media: media.length > 0 ? media : post.media
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Reads a `sitemap.xml` document, whether it indexes other sitemaps or lists locations.
|
|
117
|
+
*
|
|
118
|
+
* A blog's sitemap enumerates every post without any theme involvement, which makes it
|
|
119
|
+
* the cheapest and most reliable way to discover what a blog holds.
|
|
120
|
+
*
|
|
121
|
+
* @param sitemapContent - The sitemap's raw XML.
|
|
122
|
+
* @returns The sitemaps it points at, or the locations it lists with the date each was
|
|
123
|
+
* last modified.
|
|
124
|
+
* @throws If the document's root is neither `sitemapindex` nor `urlset`, as
|
|
125
|
+
* `not-a-sitemap`.
|
|
126
|
+
*/
|
|
127
|
+
export function parseSitemap(sitemapContent) {
|
|
128
|
+
const { document } = new JSDOM(sitemapContent, { contentType: 'text/xml' }).window;
|
|
129
|
+
const rootName = document.documentElement.localName;
|
|
130
|
+
if (rootName !== 'sitemapindex' && rootName !== 'urlset')
|
|
131
|
+
throw new Error('not-a-sitemap');
|
|
132
|
+
return {
|
|
133
|
+
kind: rootName === 'sitemapindex' ? 'index' : 'urlset',
|
|
134
|
+
childSitemapUrls: readSitemapChildUrls(document),
|
|
135
|
+
locations: readSitemapLocations(document)
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Reads a `robots.txt`, keeping the `Sitemap:` directives and the rules stated for every
|
|
140
|
+
* crawler.
|
|
141
|
+
*
|
|
142
|
+
* @param robotsContent - The file's raw text.
|
|
143
|
+
* @returns The sitemaps it advertises and the `User-agent: *` rules, each list empty when
|
|
144
|
+
* the file states none.
|
|
145
|
+
*/
|
|
146
|
+
export function parseRobots(robotsContent) {
|
|
147
|
+
const sitemapUrls = [];
|
|
148
|
+
const disallowedPaths = [];
|
|
149
|
+
const allowedPaths = [];
|
|
150
|
+
let crawlDelaySeconds = null;
|
|
151
|
+
let insideEveryCrawlerGroup = false;
|
|
152
|
+
for (const rawLine of robotsContent.split(/\r?\n/v)) {
|
|
153
|
+
const line = rawLine.replace(/#.*$/v, '').trim();
|
|
154
|
+
const separatorIndex = line.indexOf(':');
|
|
155
|
+
if (separatorIndex === -1)
|
|
156
|
+
continue;
|
|
157
|
+
const directive = line.slice(0, separatorIndex).trim().toLowerCase();
|
|
158
|
+
const value = line.slice(separatorIndex + 1).trim();
|
|
159
|
+
if (value === '')
|
|
160
|
+
continue;
|
|
161
|
+
if (directive === 'sitemap')
|
|
162
|
+
sitemapUrls.push(value);
|
|
163
|
+
else if (directive === 'user-agent')
|
|
164
|
+
insideEveryCrawlerGroup = value === '*';
|
|
165
|
+
else if (!insideEveryCrawlerGroup)
|
|
166
|
+
continue;
|
|
167
|
+
else if (directive === 'disallow')
|
|
168
|
+
disallowedPaths.push(value);
|
|
169
|
+
else if (directive === 'allow')
|
|
170
|
+
allowedPaths.push(value);
|
|
171
|
+
else if (directive === 'crawl-delay')
|
|
172
|
+
crawlDelaySeconds = toIntegerOrNull(value);
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
sitemapUrls,
|
|
176
|
+
disallowedPaths,
|
|
177
|
+
allowedPaths,
|
|
178
|
+
crawlDelaySeconds
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|