@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.
Files changed (255) 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.d.ts +1 -0
  7. package/agnostic/elo/index.test.js +67 -0
  8. package/agnostic/index.d.ts +5 -4
  9. package/agnostic/index.js +5 -0
  10. package/agnostic/poetry/generate-rhymes/index.d.ts +1 -0
  11. package/agnostic/poetry/generate-rhymes/index.js +125 -0
  12. package/agnostic/poetry/index.d.ts +1 -0
  13. package/agnostic/poetry/index.js +1 -0
  14. package/agnostic/scrapers/@meteociel/index.d.ts +36 -0
  15. package/agnostic/scrapers/@meteociel/index.js +248 -0
  16. package/agnostic/scrapers/@meteociel/index.test.d.ts +1 -0
  17. package/agnostic/scrapers/@meteociel/index.test.js +43 -0
  18. package/agnostic/scrapers/@meteociel/locations/index.d.ts +11 -0
  19. package/agnostic/scrapers/@meteociel/locations/index.js +10 -0
  20. package/agnostic/scrapers/@meteociel/types.d.ts +1 -0
  21. package/agnostic/scrapers/@meteociel/types.js +1 -0
  22. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.d.ts +22 -0
  23. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.js +124 -0
  24. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.d.ts +1 -0
  25. package/agnostic/scrapers/@tumblr/blogs/delabienbonnemusique/index.test.js +107 -0
  26. package/agnostic/scrapers/@tumblr/blogs/index.d.ts +8 -0
  27. package/agnostic/scrapers/@tumblr/blogs/index.js +23 -0
  28. package/agnostic/scrapers/@tumblr/index.d.ts +73 -0
  29. package/agnostic/scrapers/@tumblr/index.js +180 -0
  30. package/agnostic/scrapers/@tumblr/index.test.d.ts +1 -0
  31. package/agnostic/scrapers/@tumblr/index.test.js +319 -0
  32. package/agnostic/scrapers/@tumblr/types.d.ts +238 -0
  33. package/agnostic/scrapers/@tumblr/types.js +38 -0
  34. package/agnostic/scrapers/@tumblr/utils.d.ts +12 -0
  35. package/agnostic/scrapers/@tumblr/utils.js +407 -0
  36. package/agnostic/scrapers/index.d.ts +2 -0
  37. package/agnostic/scrapers/index.js +2 -0
  38. package/agnostic/weather/index.d.ts +1 -0
  39. package/agnostic/weather/index.js +1 -0
  40. package/agnostic/weather/types.d.ts +181 -0
  41. package/agnostic/weather/types.js +8 -0
  42. package/components/index.d.ts +1 -1
  43. package/components/index.js +1 -0
  44. package/components/public-classnames.d.ts +5 -0
  45. package/components/public-classnames.js +5 -0
  46. package/components/utils/index.d.ts +1 -0
  47. package/components/utils/index.js +12 -0
  48. package/components/utils/types.d.ts +3 -0
  49. package/components/utils/types.js +1 -0
  50. package/index.d.ts +3 -3
  51. package/index.js +3 -0
  52. package/node/headless-browser/index.d.ts +37 -0
  53. package/node/headless-browser/index.js +126 -0
  54. package/node/headless-browser/types.d.ts +47 -0
  55. package/node/headless-browser/types.js +5 -0
  56. package/node/index.d.ts +1 -2
  57. package/node/index.js +1 -0
  58. package/node/index.test.d.ts +1 -0
  59. package/node/index.test.js +6 -0
  60. package/node/scrapers/@tumblr/index.d.ts +61 -0
  61. package/node/scrapers/@tumblr/index.js +182 -0
  62. package/node/scrapers/@tumblr/index.test.d.ts +1 -0
  63. package/node/scrapers/@tumblr/index.test.js +185 -0
  64. package/node/scrapers/@tumblr/types.d.ts +91 -0
  65. package/node/scrapers/@tumblr/types.js +1 -0
  66. package/node/scrapers/@tumblr/utils.d.ts +23 -0
  67. package/node/scrapers/@tumblr/utils.js +328 -0
  68. package/node/scrapers/index.d.ts +1 -0
  69. package/node/scrapers/index.js +1 -0
  70. package/package.json +211 -14
  71. package/agnostic/_WIP_tennis/index.d.ts +0 -2
  72. package/agnostic/_WIP_tennis/index.js +0 -62
  73. package/agnostic/arrays/index.d.ts +0 -11
  74. package/agnostic/arrays/is-array-of/index.d.ts +0 -5
  75. package/agnostic/arrays/is-array-of/index.js +0 -23
  76. package/agnostic/async/index.d.ts +0 -8
  77. package/agnostic/async/index.js +0 -20
  78. package/agnostic/async/timeout-call/index.d.ts +0 -1
  79. package/agnostic/async/timeout-call/index.js +0 -6
  80. package/agnostic/async/transitions/index.d.ts +0 -41
  81. package/agnostic/async/transitions/index.js +0 -7
  82. package/agnostic/async/wait/index.d.ts +0 -1
  83. package/agnostic/async/wait/index.js +0 -6
  84. package/agnostic/booleans/index.d.ts +0 -8
  85. package/agnostic/booleans/index.js +0 -17
  86. package/agnostic/booleans/is-falsy/index.d.ts +0 -7
  87. package/agnostic/booleans/is-falsy/index.js +0 -11
  88. package/agnostic/css/bem/BEM.d.ts +0 -29
  89. package/agnostic/css/bem/getNamesArr.d.ts +0 -2
  90. package/agnostic/css/bem/index.d.ts +0 -7
  91. package/agnostic/css/bem/index.js +0 -9
  92. package/agnostic/css/generate-nice-color/index.d.ts +0 -2
  93. package/agnostic/css/generate-nice-color/index.js +0 -8
  94. package/agnostic/css/index.d.ts +0 -14
  95. package/agnostic/css/index.js +0 -177
  96. package/agnostic/css/is-valid-css-class-name/index.d.ts +0 -2
  97. package/agnostic/css/is-valid-css-class-name/index.js +0 -8
  98. package/agnostic/css/styles-set/index.d.ts +0 -37
  99. package/agnostic/errors/index.d.ts +0 -30
  100. package/agnostic/errors/index.js +0 -6
  101. package/agnostic/html/get-node-ancestors/index.d.ts +0 -1
  102. package/agnostic/html/get-node-ancestors/index.js +0 -6
  103. package/agnostic/html/get-position-inside-parent/index.d.ts +0 -1
  104. package/agnostic/html/get-position-inside-parent/index.js +0 -6
  105. package/agnostic/html/index.d.ts +0 -17
  106. package/agnostic/html/index.js +0 -39
  107. package/agnostic/html/insert-node/index.d.ts +0 -2
  108. package/agnostic/html/insert-node/index.js +0 -6
  109. package/agnostic/html/placeholders/index.d.ts +0 -9
  110. package/agnostic/html/placeholders/index.js +0 -6
  111. package/agnostic/html/sanitize/index.d.ts +0 -24
  112. package/agnostic/html/sanitize/index.js +0 -9
  113. package/agnostic/html/selector-to-element/index.d.ts +0 -1
  114. package/agnostic/html/selector-to-element/index.js +0 -9
  115. package/agnostic/html/string-to-nodes/index.d.ts +0 -7
  116. package/agnostic/html/string-to-nodes/index.js +0 -10
  117. package/agnostic/misc/assert/index.d.ts +0 -5
  118. package/agnostic/misc/assert/index.js +0 -6
  119. package/agnostic/misc/cast/index.d.ts +0 -9
  120. package/agnostic/misc/cast/index.js +0 -8
  121. package/agnostic/misc/crossenv/index.d.ts +0 -13
  122. package/agnostic/misc/crossenv/index.js +0 -6
  123. package/agnostic/misc/elo/index.d.ts +0 -31
  124. package/agnostic/misc/elo/index.js +0 -6
  125. package/agnostic/misc/get-current-downlink/index.d.ts +0 -1
  126. package/agnostic/misc/get-current-downlink/index.js +0 -6
  127. package/agnostic/misc/index.d.ts +0 -6
  128. package/agnostic/misc/index.js +0 -16
  129. package/agnostic/misc/is-constructor-function/index.d.ts +0 -2
  130. package/agnostic/misc/is-constructor-function/index.js +0 -6
  131. package/agnostic/misc/is-nullish/index.d.ts +0 -4
  132. package/agnostic/misc/is-nullish/index.js +0 -10
  133. package/agnostic/misc/logs/index.d.ts +0 -55
  134. package/agnostic/misc/logs/index.js +0 -6
  135. package/agnostic/misc/random/index.d.ts +0 -10
  136. package/agnostic/misc/random/index.js +0 -6
  137. package/agnostic/misc/tennis/index.d.ts +0 -60
  138. package/agnostic/misc/tennis/index.js +0 -6
  139. package/agnostic/numbers/absolute-modulo/index.d.ts +0 -1
  140. package/agnostic/numbers/absolute-modulo/index.js +0 -6
  141. package/agnostic/numbers/clamp/index.d.ts +0 -1
  142. package/agnostic/numbers/clamp/index.js +0 -6
  143. package/agnostic/numbers/index.d.ts +0 -13
  144. package/agnostic/numbers/index.js +0 -30
  145. package/agnostic/numbers/interpolate/index.d.ts +0 -1
  146. package/agnostic/numbers/interpolate/index.js +0 -6
  147. package/agnostic/numbers/responsive-harmonics/index.d.ts +0 -10
  148. package/agnostic/numbers/responsive-harmonics/index.js +0 -9
  149. package/agnostic/numbers/round/index.d.ts +0 -1
  150. package/agnostic/numbers/round/index.js +0 -6
  151. package/agnostic/objects/enums/index.d.ts +0 -4
  152. package/agnostic/objects/enums/index.js +0 -7
  153. package/agnostic/objects/enums/is-in-enum/index.d.ts +0 -1
  154. package/agnostic/objects/enums/is-in-enum/index.js +0 -6
  155. package/agnostic/objects/flatten-getters/index.d.ts +0 -1
  156. package/agnostic/objects/flatten-getters/index.js +0 -9
  157. package/agnostic/objects/index.d.ts +0 -12
  158. package/agnostic/objects/index.js +0 -32
  159. package/agnostic/objects/is-object/index.d.ts +0 -1
  160. package/agnostic/objects/is-object/index.js +0 -6
  161. package/agnostic/objects/is-record/index.d.ts +0 -1
  162. package/agnostic/objects/is-record/index.js +0 -6
  163. package/agnostic/objects/record-format/index.d.ts +0 -11
  164. package/agnostic/objects/record-format/index.js +0 -16
  165. package/agnostic/objects/validation/index.d.ts +0 -3
  166. package/agnostic/objects/validation/index.js +0 -6
  167. package/agnostic/optim/index.d.ts +0 -7
  168. package/agnostic/optim/index.js +0 -18
  169. package/agnostic/optim/memoize/index.d.ts +0 -3
  170. package/agnostic/optim/memoize/index.js +0 -6
  171. package/agnostic/optim/throttle-debounce/index.d.ts +0 -30
  172. package/agnostic/optim/throttle-debounce/index.js +0 -8
  173. package/agnostic/optim/throttle-debounce/types.d.ts +0 -2
  174. package/agnostic/regexps/index.d.ts +0 -23
  175. package/agnostic/regexps/index.js +0 -89
  176. package/agnostic/strings/char-codes/index.d.ts +0 -17
  177. package/agnostic/strings/char-codes/index.js +0 -6
  178. package/agnostic/strings/index.d.ts +0 -14
  179. package/agnostic/strings/index.js +0 -32
  180. package/agnostic/strings/matches/index.d.ts +0 -3
  181. package/agnostic/strings/matches/index.js +0 -10
  182. package/agnostic/strings/normalize-indent/index.d.ts +0 -1
  183. package/agnostic/strings/normalize-indent/index.js +0 -6
  184. package/agnostic/strings/replace-all/index.d.ts +0 -1
  185. package/agnostic/strings/replace-all/index.js +0 -6
  186. package/agnostic/strings/to-alphanum/index.d.ts +0 -1
  187. package/agnostic/strings/to-alphanum/index.js +0 -6
  188. package/chunks/chunk-32IRF4OP.js +0 -9
  189. package/chunks/chunk-34U4HX4V.js +0 -73
  190. package/chunks/chunk-4GDNHUCZ.js +0 -162
  191. package/chunks/chunk-4QQBKBYH.js +0 -59
  192. package/chunks/chunk-4VC5PT5T.js +0 -13
  193. package/chunks/chunk-5LNUCDQ6.js +0 -132
  194. package/chunks/chunk-7AWTHZLY.js +0 -12
  195. package/chunks/chunk-7DBNMU6N.js +0 -14
  196. package/chunks/chunk-COVPTTAD.js +0 -10
  197. package/chunks/chunk-DNIOWD7K.js +0 -8
  198. package/chunks/chunk-DQNJQBC6.js +0 -10
  199. package/chunks/chunk-E6MSDKON.js +0 -13
  200. package/chunks/chunk-EDVNAV3G.js +0 -72
  201. package/chunks/chunk-EW46EXFB.js +0 -8
  202. package/chunks/chunk-FPEW3A27.js +0 -14
  203. package/chunks/chunk-GJOU3UAL.js +0 -36
  204. package/chunks/chunk-HC6ZOHCS.js +0 -14
  205. package/chunks/chunk-HGCG2J77.js +0 -122
  206. package/chunks/chunk-IFD7GSGK.js +0 -130
  207. package/chunks/chunk-JQXNEJAP.js +0 -18
  208. package/chunks/chunk-JWBDZPQG.js +0 -18
  209. package/chunks/chunk-K37RALUX.js +0 -8
  210. package/chunks/chunk-KIONYWA7.js +0 -168
  211. package/chunks/chunk-KQT5CTGF.js +0 -21
  212. package/chunks/chunk-LEXQ6E6L.js +0 -31
  213. package/chunks/chunk-LQFKUNVQ.js +0 -19
  214. package/chunks/chunk-MF56TTC5.js +0 -15
  215. package/chunks/chunk-MT74WIQR.js +0 -41
  216. package/chunks/chunk-NRITRUZW.js +0 -10
  217. package/chunks/chunk-NRJQKVQV.js +0 -31
  218. package/chunks/chunk-OSAXBA7G.js +0 -10
  219. package/chunks/chunk-PO3V3I57.js +0 -23
  220. package/chunks/chunk-QXAJXTXV.js +0 -19
  221. package/chunks/chunk-RCO57B6F.js +0 -9
  222. package/chunks/chunk-SQZGZ3VT.js +0 -15
  223. package/chunks/chunk-TTZHPIH7.js +0 -237
  224. package/chunks/chunk-UYTXXUU7.js +0 -54
  225. package/chunks/chunk-VTPRO4NJ.js +0 -10
  226. package/chunks/chunk-VYW4IADX.js +0 -35
  227. package/chunks/chunk-VZDUZTW6.js +0 -113
  228. package/chunks/chunk-W5A2TON3.js +0 -10
  229. package/chunks/chunk-W7YBGJ4H.js +0 -28
  230. package/chunks/chunk-WLL3FNVL.js +0 -40
  231. package/chunks/chunk-WOAYU6LB.js +0 -13
  232. package/chunks/chunk-XA4HVHJ4.js +0 -18
  233. package/chunks/chunk-XN3PZIVL.js +0 -38
  234. package/chunks/chunk-Y3OZHZUF.js +0 -237
  235. package/chunks/chunk-YDIBNEGA.js +0 -9
  236. package/chunks/chunk-ZC5RZBNY.js +0 -33
  237. package/components/Comp/index.d.ts +0 -1
  238. package/node/files/index.d.ts +0 -9
  239. package/node/files/index.js +0 -21
  240. package/node/files/is-in-directory/index.d.ts +0 -1
  241. package/node/files/is-in-directory/index.js +0 -6
  242. package/node/files/read-write/index.d.ts +0 -9
  243. package/node/files/read-write/index.js +0 -6
  244. package/node/files/subpaths/index.d.ts +0 -34
  245. package/node/files/subpaths/index.js +0 -7
  246. package/node/image/index.d.ts +0 -9
  247. package/node/image/index.js +0 -74
  248. package/node/mxfb-cli-config/index.d.ts +0 -19
  249. package/node/mxfb-cli-config/index.js +0 -113
  250. package/node/process/index.d.ts +0 -8
  251. package/node/process/index.js +0 -20
  252. package/node/process/on-exit/index.d.ts +0 -5
  253. package/node/process/on-exit/index.js +0 -14
  254. package/shared/errors/index.d.ts +0 -138
  255. package/shared/index.d.ts +0 -0
@@ -0,0 +1,91 @@
1
+ import type { TumblrBlog, TumblrPageKind } from '../../../agnostic/scrapers/@tumblr/types.js';
2
+ /**
3
+ * How one crawl of a blog should behave.
4
+ *
5
+ * @property dataDirPath - Directory the crawl writes into, created when missing. Left
6
+ * out, a fresh temporary directory is made and reported back as
7
+ * {@link CrawlBlogResult.dataDirPath}, which is the path to hand back to resume later.
8
+ * @property followedKinds - Which page kinds the crawl enqueues when it meets them.
9
+ * Defaults to `['index', 'permalink']`: listings enumerate, permalinks extract.
10
+ * @property pages - Seeds `/page/from` to `/page/to` instead of discovering listings from
11
+ * the blog root.
12
+ * @property limit - Hard cap on the number of pages fetched, a safety valve rather than
13
+ * the stop condition. Defaults to 10000.
14
+ * @property delayMs - Delay between two fetches. Defaults to 250, and is raised to the
15
+ * blog's own `Crawl-delay` when it asks for more.
16
+ * @property saveRawPages - Keeps each page's raw HTML under `raw/`, so a later parser fix
17
+ * can be replayed without fetching anything again. Defaults to `true`.
18
+ * @property resume - Picks up where a previous crawl of the same directory stopped.
19
+ * @property useSitemap - Reads `robots.txt` and `sitemap.xml` to enumerate posts without
20
+ * walking listings. Defaults to `true`.
21
+ * @property onPageCrawled - Called once per page, successes and failures alike.
22
+ * @property fetcher - Reads one URL as text, or answers `null` when it could not. Left
23
+ * out, a headless Chrome is started for the crawl and closed with it, which is what a
24
+ * Tumblr blog host requires: it answers a plain request with a 403 and a JavaScript
25
+ * challenge.
26
+ */
27
+ export type CrawlBlogOptions = {
28
+ dataDirPath?: string;
29
+ followedKinds?: TumblrPageKind[];
30
+ pages?: {
31
+ from: number;
32
+ to: number;
33
+ };
34
+ limit?: number;
35
+ delayMs?: number | (() => number);
36
+ saveRawPages?: boolean;
37
+ resume?: boolean;
38
+ useSitemap?: boolean;
39
+ onPageCrawled?: (report: CrawledPageReport) => void;
40
+ fetcher?: (url: string) => Promise<string | null>;
41
+ };
42
+ /**
43
+ * What became of one crawled page.
44
+ *
45
+ * @property url - The URL that was fetched.
46
+ * @property kind - What the page turned out to be, `null` when it could not be read.
47
+ * @property postCount - Posts the page carried.
48
+ * @property newPostCount - Posts the crawl had not seen before, which is what tells a
49
+ * listing worth following from one that only repeats itself.
50
+ * @property error - Why the page was skipped, `null` on success.
51
+ */
52
+ export type CrawledPageReport = {
53
+ url: string;
54
+ kind: TumblrPageKind | null;
55
+ postCount: number;
56
+ newPostCount: number;
57
+ error: string | null;
58
+ };
59
+ /**
60
+ * What one crawl achieved.
61
+ *
62
+ * @property dataDirPath - Where the crawl wrote, whether it was given or created. This
63
+ * is what a later run needs to resume.
64
+ * @property blog - The blog as the first readable page described it, `null` when no page
65
+ * could be read.
66
+ * @property pageCount - Pages successfully parsed.
67
+ * @property postCount - Distinct posts seen across them.
68
+ * @property failures - Every page that could not be fetched or parsed.
69
+ */
70
+ export type CrawlBlogResult = {
71
+ dataDirPath: string;
72
+ blog: TumblrBlog | null;
73
+ pageCount: number;
74
+ postCount: number;
75
+ failures: CrawledPageReport[];
76
+ };
77
+ /**
78
+ * A crawl's progress, as `state.json` holds it between two runs.
79
+ *
80
+ * @property baseUrl - Origin the crawl was run against.
81
+ * @property processedUrls - URLs already fetched, which a resumed crawl will not fetch
82
+ * again.
83
+ * @property waitlistUrls - URLs still owed a fetch when the crawl stopped.
84
+ * @property knownPostIds - Every post id seen so far.
85
+ */
86
+ export type CrawlState = {
87
+ baseUrl: string;
88
+ processedUrls: string[];
89
+ waitlistUrls: string[];
90
+ knownPostIds: string[];
91
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,23 @@
1
+ import type { TumblrPage, TumblrPageKind, TumblrPost, TumblrRobots } from '../../../agnostic/scrapers/@tumblr/types.js';
2
+ import type { CrawlState } from './types.js';
3
+ type PageFetcher = (url: string) => Promise<string | null>;
4
+ export declare function toOriginOrNull(rawUrl: string): string | null;
5
+ export declare function normalizeUrl(rawUrl: string, baseUrl: string | null): string | null;
6
+ export declare function isListingKind(kind: TumblrPageKind): boolean;
7
+ export declare function buildListingPageUrls(baseUrl: string, from: number, to: number): string[];
8
+ export declare function toErrorMessage(error: unknown): string;
9
+ export declare function fetchRobots(baseUrl: string, fetcher: PageFetcher): Promise<TumblrRobots | null>;
10
+ export declare function collectSitemapLocations(baseUrl: string, robots: TumblrRobots | null, fetcher: PageFetcher): Promise<Map<string, Date | null>>;
11
+ export declare function fillLastModifiedFromSitemap(page: TumblrPage, lastModifiedByUrl: Map<string, Date | null>, baseUrl: string): TumblrPage;
12
+ export declare function createTemporaryDataDir(): Promise<string>;
13
+ export declare function ensureDataDirectories(dataDirPath: string, saveRawPages: boolean): Promise<void>;
14
+ export declare function appendPageRecord(dataDirPath: string, page: TumblrPage): Promise<void>;
15
+ export declare function writeRawPage(dataDirPath: string, url: string, pageContent: string): Promise<void>;
16
+ export declare function writeCrawlState(dataDirPath: string, state: CrawlState): Promise<void>;
17
+ export declare function readCrawlState(dataDirPath: string, baseUrl: string): Promise<CrawlState | null>;
18
+ export declare function rebuildCrawlStateFromRecords(dataDirPath: string, baseUrl: string): Promise<CrawlState | null>;
19
+ export declare function readPageRecords(dataDirPath: string): AsyncGenerator<TumblrPage>;
20
+ export declare function mergeArchivedPosts(existing: TumblrPost, incoming: TumblrPost): TumblrPost;
21
+ export declare function resolveDelayMs(robots: TumblrRobots | null, defaultDelayMs: number): number;
22
+ export declare function discoverUrlsToFollow(page: TumblrPage, followedKinds: TumblrPageKind[], newPostCount: number, baseUrl: string): string[];
23
+ export {};
@@ -0,0 +1,328 @@
1
+ import { createHash, randomBytes } from 'node:crypto';
2
+ import { createReadStream, promises as fs } from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { createInterface } from 'node:readline';
6
+ import { parseRobots, parseSitemap } from '../../../agnostic/scrapers/@tumblr/index.js';
7
+ const PAGES_FILE_NAME = 'pages.ndjson';
8
+ const STATE_FILE_NAME = 'state.json';
9
+ const RAW_DIR_NAME = 'raw';
10
+ const DEFAULT_SITEMAP_PATHNAME = '/sitemap.xml';
11
+ const LISTING_PAGE_KINDS = ['index', 'archive', 'tagged'];
12
+ export function toOriginOrNull(rawUrl) {
13
+ try {
14
+ return new URL(rawUrl).origin;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ export function normalizeUrl(rawUrl, baseUrl) {
21
+ try {
22
+ const url = new URL(rawUrl, baseUrl ?? undefined);
23
+ url.hash = '';
24
+ if (url.pathname !== '/' && url.pathname.endsWith('/')) {
25
+ url.pathname = url.pathname.slice(0, -1);
26
+ }
27
+ return url.href;
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ }
33
+ export function isListingKind(kind) {
34
+ return LISTING_PAGE_KINDS.includes(kind);
35
+ }
36
+ export function buildListingPageUrls(baseUrl, from, to) {
37
+ const urls = [];
38
+ for (let pageNumber = from; pageNumber <= to; pageNumber = pageNumber + 1) {
39
+ urls.push(`${baseUrl}/page/${pageNumber}`);
40
+ }
41
+ return urls;
42
+ }
43
+ export function toErrorMessage(error) {
44
+ return error instanceof Error ? error.message : 'unknown-error';
45
+ }
46
+ export async function fetchRobots(baseUrl, fetcher) {
47
+ const content = await fetchOrNull(fetcher, `${baseUrl}/robots.txt`);
48
+ if (content === null)
49
+ return null;
50
+ try {
51
+ return parseRobots(content);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export async function collectSitemapLocations(baseUrl, robots, fetcher) {
58
+ const advertisedUrls = robots?.sitemapUrls ?? [];
59
+ const rootSitemapUrls = advertisedUrls.length > 0
60
+ ? advertisedUrls
61
+ : [`${baseUrl}${DEFAULT_SITEMAP_PATHNAME}`];
62
+ const lastModifiedByUrl = new Map();
63
+ const visitedSitemapUrls = new Set();
64
+ const pendingSitemapUrls = [...rootSitemapUrls];
65
+ while (pendingSitemapUrls.length > 0) {
66
+ const sitemapUrl = pendingSitemapUrls.shift();
67
+ if (sitemapUrl === undefined || visitedSitemapUrls.has(sitemapUrl))
68
+ continue;
69
+ visitedSitemapUrls.add(sitemapUrl);
70
+ const sitemap = await readSitemapOrNull(sitemapUrl, fetcher);
71
+ if (sitemap === null)
72
+ continue;
73
+ for (const childUrl of sitemap.childSitemapUrls)
74
+ pendingSitemapUrls.push(childUrl);
75
+ for (const location of sitemap.locations) {
76
+ const normalizedUrl = normalizeUrl(location.url, baseUrl);
77
+ if (normalizedUrl === null || lastModifiedByUrl.has(normalizedUrl))
78
+ continue;
79
+ lastModifiedByUrl.set(normalizedUrl, location.lastModifiedOn);
80
+ }
81
+ }
82
+ return lastModifiedByUrl;
83
+ }
84
+ export function fillLastModifiedFromSitemap(page, lastModifiedByUrl, baseUrl) {
85
+ if (lastModifiedByUrl.size === 0)
86
+ return page;
87
+ return {
88
+ ...page,
89
+ posts: page.posts.map(post => {
90
+ if (post.url === null || post.lastModifiedOn !== null)
91
+ return post;
92
+ const normalizedUrl = normalizeUrl(post.url, baseUrl);
93
+ const lastModifiedOn = normalizedUrl === null
94
+ ? null
95
+ : lastModifiedByUrl.get(normalizedUrl) ?? null;
96
+ return lastModifiedOn === null ? post : { ...post, lastModifiedOn };
97
+ })
98
+ };
99
+ }
100
+ export async function createTemporaryDataDir() {
101
+ const dirPath = path.join(os.tmpdir(), randomBytes(8).toString('hex'));
102
+ await fs.mkdir(dirPath, { recursive: true });
103
+ return dirPath;
104
+ }
105
+ export async function ensureDataDirectories(dataDirPath, saveRawPages) {
106
+ await fs.mkdir(dataDirPath, { recursive: true });
107
+ if (saveRawPages)
108
+ await fs.mkdir(path.join(dataDirPath, RAW_DIR_NAME), { recursive: true });
109
+ }
110
+ export async function appendPageRecord(dataDirPath, page) {
111
+ const line = `${JSON.stringify(page)}\n`;
112
+ await fs.appendFile(path.join(dataDirPath, PAGES_FILE_NAME), line, { encoding: 'utf-8' });
113
+ }
114
+ export async function writeRawPage(dataDirPath, url, pageContent) {
115
+ const filePath = path.join(dataDirPath, RAW_DIR_NAME, toRawFileName(url));
116
+ await fs.writeFile(filePath, pageContent, { encoding: 'utf-8' });
117
+ }
118
+ export async function writeCrawlState(dataDirPath, state) {
119
+ const line = JSON.stringify(state, null, 2);
120
+ await fs.writeFile(path.join(dataDirPath, STATE_FILE_NAME), line, { encoding: 'utf-8' });
121
+ }
122
+ export async function readCrawlState(dataDirPath, baseUrl) {
123
+ const stateContent = await readFileOrNull(path.join(dataDirPath, STATE_FILE_NAME));
124
+ const parsedState = stateContent === null ? null : toCrawlStateOrNull(parseJson(stateContent));
125
+ if (parsedState !== null)
126
+ return parsedState;
127
+ return await rebuildCrawlStateFromRecords(dataDirPath, baseUrl);
128
+ }
129
+ export async function rebuildCrawlStateFromRecords(dataDirPath, baseUrl) {
130
+ const recordsContent = await readFileOrNull(path.join(dataDirPath, PAGES_FILE_NAME));
131
+ if (recordsContent === null)
132
+ return null;
133
+ const processedUrls = new Set();
134
+ const knownPostIds = new Set();
135
+ for (const line of recordsContent.split('\n')) {
136
+ if (line.trim() === '')
137
+ continue;
138
+ const record = toPageRecordDigest(parseJson(line));
139
+ if (record.url !== null)
140
+ processedUrls.add(record.url);
141
+ for (const postId of record.postIds)
142
+ knownPostIds.add(postId);
143
+ }
144
+ return {
145
+ baseUrl,
146
+ processedUrls: Array.from(processedUrls),
147
+ waitlistUrls: [],
148
+ knownPostIds: Array.from(knownPostIds)
149
+ };
150
+ }
151
+ export async function* readPageRecords(dataDirPath) {
152
+ const filePath = path.join(dataDirPath, PAGES_FILE_NAME);
153
+ const lines = createInterface({
154
+ input: createReadStream(filePath, { encoding: 'utf-8' }),
155
+ crlfDelay: Number.POSITIVE_INFINITY
156
+ });
157
+ for await (const line of lines) {
158
+ if (line.trim() === '')
159
+ continue;
160
+ const page = toArchivedPage(parseJson(line));
161
+ if (page !== null)
162
+ yield page;
163
+ }
164
+ }
165
+ export function mergeArchivedPosts(existing, incoming) {
166
+ const incomingWins = incoming.rawHtmlSource === 'permalink'
167
+ && existing.rawHtmlSource !== 'permalink';
168
+ const [preferred, other] = incomingWins ? [incoming, existing] : [existing, incoming];
169
+ return {
170
+ id: preferred.id ?? other.id,
171
+ url: preferred.url ?? other.url,
172
+ type: preferred.type === 'unknown' ? other.type : preferred.type,
173
+ publishedOn: preferred.publishedOn ?? other.publishedOn,
174
+ title: preferred.title ?? other.title,
175
+ contentHtml: preferred.contentHtml ?? other.contentHtml,
176
+ rawHtml: preferred.rawHtml ?? other.rawHtml,
177
+ rawHtmlSource: preferred.rawHtml === null ? other.rawHtmlSource : preferred.rawHtmlSource,
178
+ detail: preferred.detail ?? other.detail,
179
+ tags: preferred.tags.length > 0 ? preferred.tags : other.tags,
180
+ media: preferred.media.length > 0 ? preferred.media : other.media,
181
+ notesCount: preferred.notesCount ?? other.notesCount,
182
+ reblogOfUrl: preferred.reblogOfUrl ?? other.reblogOfUrl,
183
+ lastModifiedOn: preferred.lastModifiedOn ?? other.lastModifiedOn
184
+ };
185
+ }
186
+ export function resolveDelayMs(robots, defaultDelayMs) {
187
+ const requestedDelayMs = (robots?.crawlDelaySeconds ?? 0) * 1000;
188
+ return Math.max(defaultDelayMs, requestedDelayMs);
189
+ }
190
+ export function discoverUrlsToFollow(page, followedKinds, newPostCount, baseUrl) {
191
+ const urls = [];
192
+ if (!isListingKind(page.kind))
193
+ return urls;
194
+ if (followedKinds.includes('permalink')) {
195
+ for (const post of page.posts) {
196
+ const postUrl = post.url === null ? null : normalizeUrl(post.url, baseUrl);
197
+ if (postUrl !== null)
198
+ urls.push(postUrl);
199
+ }
200
+ }
201
+ const keepsWalking = followedKinds.includes(page.kind)
202
+ && newPostCount > 0
203
+ && page.pagination.nextPageUrl !== null;
204
+ if (keepsWalking) {
205
+ const nextPageUrl = normalizeUrl(page.pagination.nextPageUrl ?? '', baseUrl);
206
+ if (nextPageUrl !== null)
207
+ urls.push(nextPageUrl);
208
+ }
209
+ return urls;
210
+ }
211
+ async function readSitemapOrNull(sitemapUrl, fetcher) {
212
+ const content = await fetchOrNull(fetcher, sitemapUrl);
213
+ if (content === null)
214
+ return null;
215
+ try {
216
+ return parseSitemap(content);
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ async function fetchOrNull(fetcher, url) {
223
+ try {
224
+ return await fetcher(url);
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ }
230
+ async function readFileOrNull(filePath) {
231
+ try {
232
+ return await fs.readFile(filePath, { encoding: 'utf-8' });
233
+ }
234
+ catch {
235
+ return null;
236
+ }
237
+ }
238
+ function parseJson(raw) {
239
+ try {
240
+ return JSON.parse(raw);
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ }
246
+ function toStringArray(value) {
247
+ if (!Array.isArray(value))
248
+ return [];
249
+ return value.filter((item) => typeof item === 'string');
250
+ }
251
+ function toCrawlStateOrNull(value) {
252
+ if (typeof value !== 'object' || value === null)
253
+ return null;
254
+ /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */
255
+ const record = value;
256
+ const baseUrl = record.baseUrl;
257
+ if (typeof baseUrl !== 'string')
258
+ return null;
259
+ return {
260
+ baseUrl,
261
+ processedUrls: toStringArray(record.processedUrls),
262
+ waitlistUrls: toStringArray(record.waitlistUrls),
263
+ knownPostIds: toStringArray(record.knownPostIds)
264
+ };
265
+ }
266
+ function toPageRecordDigest(value) {
267
+ if (typeof value !== 'object' || value === null)
268
+ return { url: null, postIds: [] };
269
+ /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */
270
+ const record = value;
271
+ const pageUrl = record.url;
272
+ const posts = Array.isArray(record.posts) ? record.posts : [];
273
+ const postIds = [];
274
+ for (const post of posts) {
275
+ if (typeof post !== 'object' || post === null)
276
+ continue;
277
+ /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */
278
+ const postRecord = post;
279
+ const postId = postRecord.id;
280
+ if (typeof postId === 'string')
281
+ postIds.push(postId);
282
+ }
283
+ return {
284
+ url: typeof pageUrl === 'string' ? pageUrl : null,
285
+ postIds
286
+ };
287
+ }
288
+ function toDateOrNull(value) {
289
+ if (typeof value !== 'string')
290
+ return null;
291
+ const parsed = new Date(value);
292
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
293
+ }
294
+ // JSON knows no dates, so what a record holds are the strings `JSON.stringify` wrote.
295
+ function toArchivedPage(value) {
296
+ if (typeof value !== 'object' || value === null)
297
+ return null;
298
+ /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */
299
+ const record = value;
300
+ const collectedOn = toDateOrNull(record.collectedOn);
301
+ if (typeof record.url !== 'string' || !Array.isArray(record.posts))
302
+ return null;
303
+ if (collectedOn === null)
304
+ return null;
305
+ /* eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion */
306
+ const page = value;
307
+ return {
308
+ ...page,
309
+ collectedOn,
310
+ posts: page.posts.map(post => ({
311
+ ...post,
312
+ publishedOn: toDateOrNull(post.publishedOn),
313
+ lastModifiedOn: toDateOrNull(post.lastModifiedOn)
314
+ }))
315
+ };
316
+ }
317
+ function toRawFileName(url) {
318
+ const digest = createHash('sha1').update(url).digest('hex').slice(0, 6);
319
+ let pathname;
320
+ try {
321
+ pathname = new URL(url).pathname;
322
+ }
323
+ catch {
324
+ pathname = url;
325
+ }
326
+ const slug = pathname.replace(/[^a-zA-Z0-9]+/gv, '-').replace(/^-+|-+$/gv, '');
327
+ return `${slug === '' ? 'home' : slug}-${digest}.html`;
328
+ }
@@ -0,0 +1 @@
1
+ export * as tumblr from './@tumblr/index.js'
@@ -0,0 +1 @@
1
+ export * as tumblr from './@tumblr/index.js'