@falkenslab/moodle-driver 0.0.0-stage → 0.3.1

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 (297) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +133 -2
  3. package/dist/areas/activities/build.d.ts +98 -0
  4. package/dist/areas/activities/build.js +246 -0
  5. package/dist/areas/activities/build.js.map +1 -0
  6. package/dist/areas/activities/content.d.ts +53 -0
  7. package/dist/areas/activities/content.js +113 -0
  8. package/dist/areas/activities/content.js.map +1 -0
  9. package/dist/areas/assignments/grader.d.ts +68 -0
  10. package/dist/areas/assignments/grader.js +169 -0
  11. package/dist/areas/assignments/grader.js.map +1 -0
  12. package/dist/areas/assignments/grading.d.ts +86 -0
  13. package/dist/areas/assignments/grading.js +162 -0
  14. package/dist/areas/assignments/grading.js.map +1 -0
  15. package/dist/areas/assignments/student.d.ts +57 -0
  16. package/dist/areas/assignments/student.js +115 -0
  17. package/dist/areas/assignments/student.js.map +1 -0
  18. package/dist/areas/calendar/calendar.d.ts +43 -0
  19. package/dist/areas/calendar/calendar.js +84 -0
  20. package/dist/areas/calendar/calendar.js.map +1 -0
  21. package/dist/areas/completion/completion.d.ts +50 -0
  22. package/dist/areas/completion/completion.js +112 -0
  23. package/dist/areas/completion/completion.js.map +1 -0
  24. package/dist/areas/courses/list.d.ts +59 -0
  25. package/dist/areas/courses/list.js +74 -0
  26. package/dist/areas/courses/list.js.map +1 -0
  27. package/dist/areas/courses/overview.d.ts +83 -0
  28. package/dist/areas/courses/overview.js +137 -0
  29. package/dist/areas/courses/overview.js.map +1 -0
  30. package/dist/areas/courses/state.d.ts +51 -0
  31. package/dist/areas/courses/state.js +45 -0
  32. package/dist/areas/courses/state.js.map +1 -0
  33. package/dist/areas/courses/structure.d.ts +89 -0
  34. package/dist/areas/courses/structure.js +104 -0
  35. package/dist/areas/courses/structure.js.map +1 -0
  36. package/dist/areas/forums/discussions.d.ts +57 -0
  37. package/dist/areas/forums/discussions.js +119 -0
  38. package/dist/areas/forums/discussions.js.map +1 -0
  39. package/dist/areas/forums/posts.d.ts +83 -0
  40. package/dist/areas/forums/posts.js +114 -0
  41. package/dist/areas/forums/posts.js.map +1 -0
  42. package/dist/areas/grades/gradebook.d.ts +104 -0
  43. package/dist/areas/grades/gradebook.js +250 -0
  44. package/dist/areas/grades/gradebook.js.map +1 -0
  45. package/dist/areas/grades/user-report.d.ts +26 -0
  46. package/dist/areas/grades/user-report.js +71 -0
  47. package/dist/areas/grades/user-report.js.map +1 -0
  48. package/dist/areas/h5p/h5p.d.ts +33 -0
  49. package/dist/areas/h5p/h5p.js +104 -0
  50. package/dist/areas/h5p/h5p.js.map +1 -0
  51. package/dist/areas/lessons/lessons.d.ts +60 -0
  52. package/dist/areas/lessons/lessons.js +145 -0
  53. package/dist/areas/lessons/lessons.js.map +1 -0
  54. package/dist/areas/messages/messages.d.ts +68 -0
  55. package/dist/areas/messages/messages.js +84 -0
  56. package/dist/areas/messages/messages.js.map +1 -0
  57. package/dist/areas/participants/participants.d.ts +57 -0
  58. package/dist/areas/participants/participants.js +135 -0
  59. package/dist/areas/participants/participants.js.map +1 -0
  60. package/dist/areas/questions/banks.d.ts +83 -0
  61. package/dist/areas/questions/banks.js +239 -0
  62. package/dist/areas/questions/banks.js.map +1 -0
  63. package/dist/areas/quizzes/attempts.d.ts +123 -0
  64. package/dist/areas/quizzes/attempts.js +300 -0
  65. package/dist/areas/quizzes/attempts.js.map +1 -0
  66. package/dist/areas/quizzes/results.d.ts +36 -0
  67. package/dist/areas/quizzes/results.js +93 -0
  68. package/dist/areas/quizzes/results.js.map +1 -0
  69. package/dist/areas/quizzes/structure.d.ts +42 -0
  70. package/dist/areas/quizzes/structure.js +100 -0
  71. package/dist/areas/quizzes/structure.js.map +1 -0
  72. package/dist/areas/site/info.d.ts +25 -0
  73. package/dist/areas/site/info.js +45 -0
  74. package/dist/areas/site/info.js.map +1 -0
  75. package/dist/areas/site/users.d.ts +56 -0
  76. package/dist/areas/site/users.js +89 -0
  77. package/dist/areas/site/users.js.map +1 -0
  78. package/dist/areas/surveys/choice.d.ts +38 -0
  79. package/dist/areas/surveys/choice.js +105 -0
  80. package/dist/areas/surveys/choice.js.map +1 -0
  81. package/dist/areas/surveys/surveys.d.ts +51 -0
  82. package/dist/areas/surveys/surveys.js +201 -0
  83. package/dist/areas/surveys/surveys.js.map +1 -0
  84. package/dist/browser/chromium.d.ts +15 -0
  85. package/dist/browser/chromium.js +53 -0
  86. package/dist/browser/chromium.js.map +1 -0
  87. package/dist/browser/cookies.d.ts +11 -0
  88. package/dist/browser/cookies.js +31 -0
  89. package/dist/browser/cookies.js.map +1 -0
  90. package/dist/browser/manager.d.ts +57 -0
  91. package/dist/browser/manager.js +177 -0
  92. package/dist/browser/manager.js.map +1 -0
  93. package/dist/browser.d.ts +53 -0
  94. package/dist/browser.js +167 -0
  95. package/dist/browser.js.map +1 -0
  96. package/dist/categories.d.ts +28 -0
  97. package/dist/categories.js +222 -0
  98. package/dist/categories.js.map +1 -0
  99. package/dist/chromiumInstaller.d.ts +15 -0
  100. package/dist/chromiumInstaller.js +52 -0
  101. package/dist/chromiumInstaller.js.map +1 -0
  102. package/dist/cli/args.d.ts +33 -0
  103. package/dist/cli/args.js +194 -0
  104. package/dist/cli/args.js.map +1 -0
  105. package/dist/cli/commands.generated.d.ts +4 -0
  106. package/dist/cli/commands.generated.js +2203 -0
  107. package/dist/cli/commands.generated.js.map +1 -0
  108. package/dist/cli/config.d.ts +46 -0
  109. package/dist/cli/config.js +107 -0
  110. package/dist/cli/config.js.map +1 -0
  111. package/dist/cli/main.d.ts +2 -0
  112. package/dist/cli/main.js +19 -0
  113. package/dist/cli/main.js.map +1 -0
  114. package/dist/cli/output.d.ts +4 -0
  115. package/dist/cli/output.js +71 -0
  116. package/dist/cli/output.js.map +1 -0
  117. package/dist/cli/run.d.ts +15 -0
  118. package/dist/cli/run.js +194 -0
  119. package/dist/cli/run.js.map +1 -0
  120. package/dist/cli/spec.d.ts +42 -0
  121. package/dist/cli/spec.js +4 -0
  122. package/dist/cli/spec.js.map +1 -0
  123. package/dist/client.d.ts +217 -0
  124. package/dist/client.js +179 -0
  125. package/dist/client.js.map +1 -0
  126. package/dist/core/ajax.d.ts +25 -0
  127. package/dist/core/ajax.js +85 -0
  128. package/dist/core/ajax.js.map +1 -0
  129. package/dist/core/editor.d.ts +18 -0
  130. package/dist/core/editor.js +51 -0
  131. package/dist/core/editor.js.map +1 -0
  132. package/dist/core/errors.d.ts +12 -0
  133. package/dist/core/errors.js +10 -0
  134. package/dist/core/errors.js.map +1 -0
  135. package/dist/core/files.d.ts +16 -0
  136. package/dist/core/files.js +58 -0
  137. package/dist/core/files.js.map +1 -0
  138. package/dist/core/forms.d.ts +45 -0
  139. package/dist/core/forms.js +200 -0
  140. package/dist/core/forms.js.map +1 -0
  141. package/dist/core/http.d.ts +18 -0
  142. package/dist/core/http.js +22 -0
  143. package/dist/core/http.js.map +1 -0
  144. package/dist/core/inPage.d.ts +8 -0
  145. package/dist/core/inPage.js +10 -0
  146. package/dist/core/inPage.js.map +1 -0
  147. package/dist/core/login.d.ts +40 -0
  148. package/dist/core/login.js +83 -0
  149. package/dist/core/login.js.map +1 -0
  150. package/dist/core/pace.d.ts +4 -0
  151. package/dist/core/pace.js +21 -0
  152. package/dist/core/pace.js.map +1 -0
  153. package/dist/core/session.d.ts +55 -0
  154. package/dist/core/session.js +157 -0
  155. package/dist/core/session.js.map +1 -0
  156. package/dist/core/site.d.ts +8 -0
  157. package/dist/core/site.js +36 -0
  158. package/dist/core/site.js.map +1 -0
  159. package/dist/core/text.d.ts +21 -0
  160. package/dist/core/text.js +113 -0
  161. package/dist/core/text.js.map +1 -0
  162. package/dist/home.d.ts +19 -0
  163. package/dist/home.js +54 -0
  164. package/dist/home.js.map +1 -0
  165. package/dist/index.d.ts +40 -0
  166. package/dist/index.js +51 -0
  167. package/dist/index.js.map +1 -0
  168. package/dist/internals.d.ts +11 -0
  169. package/dist/internals.js +15 -0
  170. package/dist/internals.js.map +1 -0
  171. package/dist/moodle/activities.d.ts +94 -0
  172. package/dist/moodle/activities.js +240 -0
  173. package/dist/moodle/activities.js.map +1 -0
  174. package/dist/moodle/ajax.d.ts +22 -0
  175. package/dist/moodle/ajax.js +91 -0
  176. package/dist/moodle/ajax.js.map +1 -0
  177. package/dist/moodle/assign.d.ts +65 -0
  178. package/dist/moodle/assign.js +162 -0
  179. package/dist/moodle/assign.js.map +1 -0
  180. package/dist/moodle/calendar.d.ts +41 -0
  181. package/dist/moodle/calendar.js +80 -0
  182. package/dist/moodle/calendar.js.map +1 -0
  183. package/dist/moodle/choice.d.ts +33 -0
  184. package/dist/moodle/choice.js +100 -0
  185. package/dist/moodle/choice.js.map +1 -0
  186. package/dist/moodle/completion.d.ts +48 -0
  187. package/dist/moodle/completion.js +108 -0
  188. package/dist/moodle/completion.js.map +1 -0
  189. package/dist/moodle/content.d.ts +43 -0
  190. package/dist/moodle/content.js +105 -0
  191. package/dist/moodle/content.js.map +1 -0
  192. package/dist/moodle/course.d.ts +51 -0
  193. package/dist/moodle/course.js +45 -0
  194. package/dist/moodle/course.js.map +1 -0
  195. package/dist/moodle/courses.d.ts +57 -0
  196. package/dist/moodle/courses.js +70 -0
  197. package/dist/moodle/courses.js.map +1 -0
  198. package/dist/moodle/editor.d.ts +18 -0
  199. package/dist/moodle/editor.js +50 -0
  200. package/dist/moodle/editor.js.map +1 -0
  201. package/dist/moodle/files.d.ts +16 -0
  202. package/dist/moodle/files.js +57 -0
  203. package/dist/moodle/files.js.map +1 -0
  204. package/dist/moodle/forms.d.ts +44 -0
  205. package/dist/moodle/forms.js +199 -0
  206. package/dist/moodle/forms.js.map +1 -0
  207. package/dist/moodle/forum.d.ts +55 -0
  208. package/dist/moodle/forum.js +115 -0
  209. package/dist/moodle/forum.js.map +1 -0
  210. package/dist/moodle/forumPosts.d.ts +83 -0
  211. package/dist/moodle/forumPosts.js +113 -0
  212. package/dist/moodle/forumPosts.js.map +1 -0
  213. package/dist/moodle/gradebook.d.ts +102 -0
  214. package/dist/moodle/gradebook.js +246 -0
  215. package/dist/moodle/gradebook.js.map +1 -0
  216. package/dist/moodle/grades.d.ts +24 -0
  217. package/dist/moodle/grades.js +67 -0
  218. package/dist/moodle/grades.js.map +1 -0
  219. package/dist/moodle/grading.d.ts +92 -0
  220. package/dist/moodle/grading.js +161 -0
  221. package/dist/moodle/grading.js.map +1 -0
  222. package/dist/moodle/h5p.d.ts +31 -0
  223. package/dist/moodle/h5p.js +100 -0
  224. package/dist/moodle/h5p.js.map +1 -0
  225. package/dist/moodle/inPage.d.ts +8 -0
  226. package/dist/moodle/inPage.js +10 -0
  227. package/dist/moodle/inPage.js.map +1 -0
  228. package/dist/moodle/lesson.d.ts +57 -0
  229. package/dist/moodle/lesson.js +140 -0
  230. package/dist/moodle/lesson.js.map +1 -0
  231. package/dist/moodle/login.d.ts +37 -0
  232. package/dist/moodle/login.js +77 -0
  233. package/dist/moodle/login.js.map +1 -0
  234. package/dist/moodle/messages.d.ts +66 -0
  235. package/dist/moodle/messages.js +80 -0
  236. package/dist/moodle/messages.js.map +1 -0
  237. package/dist/moodle/overview.d.ts +83 -0
  238. package/dist/moodle/overview.js +137 -0
  239. package/dist/moodle/overview.js.map +1 -0
  240. package/dist/moodle/pace.d.ts +4 -0
  241. package/dist/moodle/pace.js +19 -0
  242. package/dist/moodle/pace.js.map +1 -0
  243. package/dist/moodle/participants.d.ts +55 -0
  244. package/dist/moodle/participants.js +131 -0
  245. package/dist/moodle/participants.js.map +1 -0
  246. package/dist/moodle/questionBank.d.ts +81 -0
  247. package/dist/moodle/questionBank.js +235 -0
  248. package/dist/moodle/questionBank.js.map +1 -0
  249. package/dist/moodle/quiz.d.ts +117 -0
  250. package/dist/moodle/quiz.js +291 -0
  251. package/dist/moodle/quiz.js.map +1 -0
  252. package/dist/moodle/quizResults.d.ts +36 -0
  253. package/dist/moodle/quizResults.js +93 -0
  254. package/dist/moodle/quizResults.js.map +1 -0
  255. package/dist/moodle/quizStructure.d.ts +42 -0
  256. package/dist/moodle/quizStructure.js +100 -0
  257. package/dist/moodle/quizStructure.js.map +1 -0
  258. package/dist/moodle/session.d.ts +39 -0
  259. package/dist/moodle/session.js +99 -0
  260. package/dist/moodle/session.js.map +1 -0
  261. package/dist/moodle/site.d.ts +8 -0
  262. package/dist/moodle/site.js +36 -0
  263. package/dist/moodle/site.js.map +1 -0
  264. package/dist/moodle/siteInfo.d.ts +25 -0
  265. package/dist/moodle/siteInfo.js +45 -0
  266. package/dist/moodle/siteInfo.js.map +1 -0
  267. package/dist/moodle/structure.d.ts +94 -0
  268. package/dist/moodle/structure.js +103 -0
  269. package/dist/moodle/structure.js.map +1 -0
  270. package/dist/moodle/submission.d.ts +48 -0
  271. package/dist/moodle/submission.js +109 -0
  272. package/dist/moodle/submission.js.map +1 -0
  273. package/dist/moodle/survey.d.ts +47 -0
  274. package/dist/moodle/survey.js +197 -0
  275. package/dist/moodle/survey.js.map +1 -0
  276. package/dist/moodle/text.d.ts +21 -0
  277. package/dist/moodle/text.js +113 -0
  278. package/dist/moodle/text.js.map +1 -0
  279. package/dist/moodle/user.d.ts +54 -0
  280. package/dist/moodle/user.js +78 -0
  281. package/dist/moodle/user.js.map +1 -0
  282. package/dist/options.d.ts +30 -0
  283. package/dist/options.js +35 -0
  284. package/dist/options.js.map +1 -0
  285. package/dist/sessionCookies.d.ts +11 -0
  286. package/dist/sessionCookies.js +31 -0
  287. package/dist/sessionCookies.js.map +1 -0
  288. package/dist/testing/fakeMoodle.d.ts +55 -0
  289. package/dist/testing/fakeMoodle.js +103 -0
  290. package/dist/testing/fakeMoodle.js.map +1 -0
  291. package/dist/testing/gradingPanel.d.ts +6 -0
  292. package/dist/testing/gradingPanel.js +22 -0
  293. package/dist/testing/gradingPanel.js.map +1 -0
  294. package/dist/testing/index.d.ts +5 -0
  295. package/dist/testing/index.js +18 -0
  296. package/dist/testing/index.js.map +1 -0
  297. package/package.json +77 -4
@@ -0,0 +1,16 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ /** Local files given to upload (a submission, an attachment, a resource): absolute, and there. */
3
+ export declare function checkLocalFiles(files: string[]): Promise<void>;
4
+ /** A file name from Content-Disposition, else the URL's last segment; never a path. */
5
+ export declare function fileNameFor(url: URL, disposition: string | undefined): string;
6
+ export interface DownloadedFile {
7
+ path: string;
8
+ name: string;
9
+ size: number;
10
+ contentType?: string;
11
+ }
12
+ /**
13
+ * Saves a file of the site into `destDir` with the person's session, never overwriting. Only URLs of
14
+ * the site's origin: the session's cookies go nowhere else (its media outside Moodle's path included).
15
+ */
16
+ export declare function downloadFile(context: BrowserContext, siteUrl: string, url: string, destDir: string): Promise<DownloadedFile>;
@@ -0,0 +1,58 @@
1
+ import { access, mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { NotLoggedInError } from "./session.js";
4
+ import { MoodleError } from "./errors.js";
5
+ /** Local files given to upload (a submission, an attachment, a resource): absolute, and there. */
6
+ export async function checkLocalFiles(files) {
7
+ for (const file of files) {
8
+ if (!path.isAbsolute(file))
9
+ throw new MoodleError(`Give files as absolute paths: "${file}".`, "invalid-input");
10
+ await access(file).catch(() => {
11
+ throw new MoodleError(`There's no file at ${file}.`, "not-found");
12
+ });
13
+ }
14
+ }
15
+ /** A file name from Content-Disposition, else the URL's last segment; never a path. */
16
+ export function fileNameFor(url, disposition) {
17
+ const encoded = disposition && /filename\*=UTF-8''([^;]+)/i.exec(disposition)?.[1];
18
+ const plain = disposition && /filename="?([^";]+)"?/i.exec(disposition)?.[1];
19
+ const raw = encoded ? decodeURIComponent(encoded) : (plain ?? decodeURIComponent(url.pathname.split("/").pop() || "download"));
20
+ // The last segment of whatever path it names, then without characters a file name can't have.
21
+ const name = (raw.split(/[\\/]/).pop() ?? "").replace(/[:*?"<>|]+/g, "_").trim();
22
+ return name === "" || name === "." || name === ".." ? "download" : name;
23
+ }
24
+ /** `dir/name`, or `dir/name (2).ext`… when it exists: a download never overwrites. */
25
+ async function freePath(dir, name) {
26
+ const { name: stem, ext } = path.parse(name);
27
+ for (let n = 1;; n++) {
28
+ const candidate = path.join(dir, n === 1 ? name : `${stem} (${n})${ext}`);
29
+ if (!(await access(candidate).then(() => true, () => false)))
30
+ return candidate;
31
+ }
32
+ }
33
+ /**
34
+ * Saves a file of the site into `destDir` with the person's session, never overwriting. Only URLs of
35
+ * the site's origin: the session's cookies go nowhere else (its media outside Moodle's path included).
36
+ */
37
+ export async function downloadFile(context, siteUrl, url, destDir) {
38
+ const target = new URL(url);
39
+ const origin = new URL(siteUrl).origin;
40
+ if (target.origin !== origin)
41
+ throw new MoodleError(`Only files of ${origin} can be downloaded with the session; ${target.origin} isn't the site.`, "outside-site");
42
+ if (!path.isAbsolute(destDir))
43
+ throw new MoodleError(`Give the destination folder as an absolute path: "${destDir}".`, "invalid-input");
44
+ // The context's request API carries its cookies without opening a tab.
45
+ const response = await context.request.get(target.href, { maxRedirects: 5 });
46
+ const contentType = response.headers()["content-type"]?.split(";")[0]?.trim();
47
+ // A file the session can't reach comes back as the login page.
48
+ if (contentType === "text/html" && new URL(response.url()).pathname.includes("/login/"))
49
+ throw new NotLoggedInError();
50
+ if (!response.ok())
51
+ throw new MoodleError(`Moodle answered ${response.status()} for ${target.pathname}: the file doesn't exist or you can't see it.`, "not-found");
52
+ await mkdir(destDir, { recursive: true });
53
+ const body = await response.body();
54
+ const saved = await freePath(destDir, fileNameFor(target, response.headers()["content-disposition"]));
55
+ await writeFile(saved, body);
56
+ return { path: saved, name: path.basename(saved), size: body.length, contentType };
57
+ }
58
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/core/files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ import { MoodleError } from "./errors.js";
3
+ /** A form field's value: text, a number, a checkbox, or a multiple select's values. */
4
+ export type FieldValue = string | number | boolean | string[];
5
+ /** Moodle refused the form, or a field to set isn't in it. */
6
+ export declare class FormError extends MoodleError {
7
+ readonly fields: {
8
+ field: string;
9
+ message: string;
10
+ }[];
11
+ name: string;
12
+ constructor(message: string, fields: {
13
+ field: string;
14
+ message: string;
15
+ }[]);
16
+ }
17
+ /** Opens a Moodle form page and waits for its editors to be ready. */
18
+ export declare function openForm(context: BrowserContext, siteUrl: string, pathAndQuery: string): Promise<Page>;
19
+ /** A hidden field's value in the form (a draft area id, the course id…). */
20
+ export declare function formValue(page: Page, name: string): Promise<string | undefined>;
21
+ /**
22
+ * Sets fields by their form name: text inputs and textareas (through TinyMCE when it edits them),
23
+ * selects (several values for a multiple one), checkboxes and radios, in the order given (a field can
24
+ * enable the next one). Throws `FormError` naming the fields the form doesn't have, and those it has
25
+ * disabled: Moodle wouldn't send them, and the change would be lost unnoticed.
26
+ */
27
+ export declare function setFields(page: Page, values: Record<string, FieldValue>, formSelector?: string): Promise<void>;
28
+ /**
29
+ * Uploads local files to a form's draft area, as the file picker's "Upload a file" does: the
30
+ * upload repository's id comes from the page's file picker settings, the context from `M.cfg`.
31
+ */
32
+ export declare function uploadToDraft(page: Page, draftItemId: string, filePaths: string[]): Promise<void>;
33
+ /**
34
+ * Submits the form with one of its buttons and waits for the next page. Throws `FormError` with
35
+ * Moodle's messages by field (`#id_error_<field>`) when the form comes back with errors.
36
+ */
37
+ export declare function submitForm(page: Page, button: string): Promise<void>;
38
+ /**
39
+ * The values of a Moodle date selector's parts (`name[day]`, `[month]`, `[year]`, `[hour]`,
40
+ * `[minute]`) for `date`, in the person's time zone (`M.cfg.usertimezone`), which is how Moodle
41
+ * reads them. Its "enable" checkbox, when it has one, is the caller's to set first.
42
+ */
43
+ export declare function dateSelectorValues(page: Page, name: string, date: Date): Promise<Record<string, FieldValue>>;
44
+ /** The date a Moodle date selector holds (`name[day]`… in the person's time zone), or undefined when it's off. */
45
+ export declare function dateSelectorDate(page: Page, name: string): Promise<Date | undefined>;
@@ -0,0 +1,200 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { openSitePage } from "./session.js";
4
+ import { MoodleError } from "./errors.js";
5
+ /** Moodle refused the form, or a field to set isn't in it. */
6
+ export class FormError extends MoodleError {
7
+ fields;
8
+ name = "FormError";
9
+ constructor(message, fields) {
10
+ super(`${message} ${fields.map((f) => `${f.field}: ${f.message}`).join("; ")}`, "form");
11
+ this.fields = fields;
12
+ }
13
+ }
14
+ /** Opens a Moodle form page and waits for its editors to be ready. */
15
+ export async function openForm(context, siteUrl, pathAndQuery) {
16
+ const page = await openSitePage(context, siteUrl, pathAndQuery);
17
+ try {
18
+ if ((await page.locator("form.mform").count()) === 0)
19
+ throw new FormError("This page has no Moodle form.", [{ field: "page", message: pathAndQuery }]);
20
+ await page.waitForLoadState("load");
21
+ // TinyMCE starts after load; its content must be set through it, or the save takes the old one.
22
+ await page
23
+ .waitForFunction(() => {
24
+ const tiny = globalThis.tinymce;
25
+ return !tiny || tiny.get().every((editor) => editor.initialized);
26
+ }, undefined, { timeout: 15_000 })
27
+ .catch(() => undefined);
28
+ // Moodle enables and disables fields as others change (disabledIf) once its form dependencies
29
+ // start, after load: a field set before then stays disabled (confirmed empirically, 5.2).
30
+ const dependent = await page.evaluate(() => globalThis.document.documentElement.innerHTML.includes("M.form.initFormDependencies"));
31
+ if (dependent) {
32
+ await page
33
+ .waitForFunction(() => {
34
+ const g = globalThis;
35
+ const id = g.document.querySelector("form.mform")?.id ?? "";
36
+ return Boolean(g.M?.form?.dependencyManagers?.[id]);
37
+ }, undefined, { timeout: 15_000 })
38
+ .catch(() => undefined);
39
+ }
40
+ return page;
41
+ }
42
+ catch (error) {
43
+ await page.close().catch(() => undefined);
44
+ throw error;
45
+ }
46
+ }
47
+ /** A hidden field's value in the form (a draft area id, the course id…). */
48
+ export function formValue(page, name) {
49
+ return page.evaluate((name) => {
50
+ const field = globalThis.document.querySelector(`form.mform [name="${name}"]`);
51
+ return field?.value;
52
+ }, name);
53
+ }
54
+ /**
55
+ * Sets fields by their form name: text inputs and textareas (through TinyMCE when it edits them),
56
+ * selects (several values for a multiple one), checkboxes and radios, in the order given (a field can
57
+ * enable the next one). Throws `FormError` naming the fields the form doesn't have, and those it has
58
+ * disabled: Moodle wouldn't send them, and the change would be lost unnoticed.
59
+ */
60
+ export async function setFields(page, values, formSelector = "form.mform") {
61
+ const missing = [];
62
+ const disabled = [];
63
+ // One field at a time: Moodle enables dependent fields in handlers that run after the change event
64
+ // (choosing whether to rescale enables a maximum grade, confirmed empirically, 5.2).
65
+ for (const [name, value] of Object.entries(values)) {
66
+ const outcome = await setField(page, name, value, formSelector);
67
+ if (outcome === "missing")
68
+ missing.push(name);
69
+ if (outcome === "disabled")
70
+ disabled.push(name);
71
+ }
72
+ if (missing.length > 0) {
73
+ throw new FormError("The form has no such field:", missing.map((field) => ({ field, message: "not in this form; check its name in Moodle's form" })));
74
+ }
75
+ if (disabled.length > 0) {
76
+ throw new FormError("The form has these fields disabled, so Moodle wouldn't save them:", disabled.map((field) => ({ field, message: "disabled; another field enables it (set that one first, in the same call)" })));
77
+ }
78
+ }
79
+ function setField(page, name, value, formSelector) {
80
+ return page.evaluate(({ name, value, formSelector }) => {
81
+ const g = globalThis;
82
+ const form = g.document.querySelector(formSelector);
83
+ const all = Array.from(form?.querySelectorAll(`[name="${name}"]`) ?? []);
84
+ // A Moodle checkbox comes with a hidden twin holding its unchecked value.
85
+ const fields = all.length > 1 ? all.filter((field) => field.type !== "hidden") : all;
86
+ if (fields.length === 0)
87
+ return "missing";
88
+ // Checked now, after the fields before it: their change events may have enabled it.
89
+ if (fields.every((field) => field.disabled))
90
+ return "disabled";
91
+ for (const field of fields) {
92
+ if (field.type === "checkbox")
93
+ field.checked = value === true || value === 1 || value === "1";
94
+ else if (field.type === "radio")
95
+ field.checked = field.value === String(value);
96
+ else if (field.multiple && field.options) {
97
+ const wanted = (Array.isArray(value) ? value : [value]).map(String);
98
+ for (const option of Array.from(field.options))
99
+ option.selected = wanted.includes(option.value);
100
+ }
101
+ else {
102
+ field.value = String(value);
103
+ const editor = field.id ? g.tinymce?.get(field.id) : null;
104
+ if (editor) {
105
+ editor.setContent(String(value));
106
+ editor.save();
107
+ }
108
+ }
109
+ field.dispatchEvent(new g.Event("change", { bubbles: true }));
110
+ }
111
+ return "set";
112
+ }, { name, value, formSelector });
113
+ }
114
+ /**
115
+ * Uploads local files to a form's draft area, as the file picker's "Upload a file" does: the
116
+ * upload repository's id comes from the page's file picker settings, the context from `M.cfg`.
117
+ */
118
+ export async function uploadToDraft(page, draftItemId, filePaths) {
119
+ const files = await Promise.all(filePaths.map(async (filePath) => ({ name: path.basename(filePath), base64: (await readFile(filePath)).toString("base64") })));
120
+ const errors = await page.evaluate(async ({ draftItemId, files }) => {
121
+ const g = globalThis;
122
+ const repo = /"id":"(\d+)","name":"(?:[^"\\]|\\.)*","type":"upload"/.exec(g.document.documentElement.innerHTML)?.[1];
123
+ if (!repo)
124
+ return ["the site has no upload repository enabled"];
125
+ const failed = [];
126
+ for (const file of files) {
127
+ const body = new g.FormData();
128
+ body.append("repo_upload_file", new g.Blob([Uint8Array.from(g.atob(file.base64), (c) => c.charCodeAt(0))]), file.name);
129
+ for (const [key, value] of Object.entries({ sesskey: g.M.cfg.sesskey, repo_id: repo, itemid: draftItemId, ctx_id: String(g.M.cfg.contextid), savepath: "/", title: file.name, env: "filemanager", author: "" })) {
130
+ body.append(key, value);
131
+ }
132
+ const response = await g.fetch(`${g.M.cfg.wwwroot}/repository/repository_ajax.php?action=upload`, { method: "POST", body, credentials: "same-origin" });
133
+ const answer = await response.json().catch(() => ({ error: "the answer isn't JSON" }));
134
+ if (answer.error)
135
+ failed.push(`${file.name}: ${answer.error}`);
136
+ }
137
+ return failed;
138
+ }, { draftItemId, files });
139
+ if (errors.length > 0)
140
+ throw new FormError("Uploading failed:", errors.map((message) => ({ field: "files", message })));
141
+ }
142
+ /**
143
+ * Submits the form with one of its buttons and waits for the next page. Throws `FormError` with
144
+ * Moodle's messages by field (`#id_error_<field>`) when the form comes back with errors.
145
+ */
146
+ export async function submitForm(page, button) {
147
+ // The navigation's timeout, not the click's: saving can take Moodle long (regrading after a new
148
+ // maximum grade took over 30 seconds, confirmed empirically on the sandbox, 5.2).
149
+ await Promise.all([page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.click(`form.mform [name="${button}"]`, { noWaitAfter: true })]);
150
+ const errors = await page.evaluate(() => {
151
+ const doc = globalThis.document;
152
+ return Array.from(doc.querySelectorAll('form.mform [id^="id_error_"]'))
153
+ .map((element) => ({ field: element.id.slice("id_error_".length), message: (element.textContent ?? "").replace(/\s+/g, " ").trim() }))
154
+ .filter((error) => error.message !== "");
155
+ });
156
+ if (errors.length > 0)
157
+ throw new FormError("Moodle refused the form:", errors);
158
+ }
159
+ /**
160
+ * The values of a Moodle date selector's parts (`name[day]`, `[month]`, `[year]`, `[hour]`,
161
+ * `[minute]`) for `date`, in the person's time zone (`M.cfg.usertimezone`), which is how Moodle
162
+ * reads them. Its "enable" checkbox, when it has one, is the caller's to set first.
163
+ */
164
+ export async function dateSelectorValues(page, name, date) {
165
+ const timeZone = await page.evaluate(() => globalThis.M?.cfg?.usertimezone ?? "UTC");
166
+ let parts;
167
+ try {
168
+ parts = new Intl.DateTimeFormat("en-GB", { timeZone, year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", hourCycle: "h23" }).formatToParts(date);
169
+ }
170
+ catch {
171
+ throw new FormError(`The person's time zone in Moodle ("${timeZone}") isn't one the driver knows, so a date can't be set.`, [{ field: name, message: "unknown time zone" }]);
172
+ }
173
+ const part = (type) => Number(parts.find((p) => p.type === type)?.value);
174
+ return { [`${name}[day]`]: part("day"), [`${name}[month]`]: part("month"), [`${name}[year]`]: part("year"), [`${name}[hour]`]: part("hour"), [`${name}[minute]`]: part("minute") };
175
+ }
176
+ /** The date a Moodle date selector holds (`name[day]`… in the person's time zone), or undefined when it's off. */
177
+ export async function dateSelectorDate(page, name) {
178
+ const read = await page.evaluate((name) => {
179
+ const g = globalThis;
180
+ const enabled = g.document.querySelector(`form.mform [name="${name}[enabled]"][type="checkbox"]`);
181
+ if (enabled && !enabled.checked)
182
+ return undefined;
183
+ // Anonymous callbacks only: see the conventions on page.evaluate.
184
+ const [year, month, day, hour, minute] = ["year", "month", "day", "hour", "minute"].map((p) => Number(g.document.querySelector(`form.mform [name="${name}[${p}]"]`)?.value) || 0);
185
+ return { timeZone: g.M?.cfg?.usertimezone ?? "UTC", year: year, month: month, day: day, hour: hour, minute: minute };
186
+ }, name);
187
+ if (!read || !read.year)
188
+ return undefined;
189
+ // The wall time in that zone: start from it as if UTC, then take away the zone's offset at that moment (twice, across a DST change).
190
+ const wall = Date.UTC(read.year, read.month - 1, read.day, read.hour, read.minute);
191
+ let time = wall;
192
+ for (let i = 0; i < 2; i++) {
193
+ const parts = new Intl.DateTimeFormat("en-GB", { timeZone: read.timeZone, year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", hourCycle: "h23" }).formatToParts(new Date(time));
194
+ const part = (type) => Number(parts.find((p) => p.type === type)?.value);
195
+ const shown = Date.UTC(part("year"), part("month") - 1, part("day"), part("hour"), part("minute"));
196
+ time += wall - shown;
197
+ }
198
+ return new Date(time);
199
+ }
200
+ //# sourceMappingURL=forms.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"forms.js","sourceRoot":"","sources":["../../src/core/forms.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ import type { BrowserContext } from "playwright-core";
2
+ export interface SiteRequest {
3
+ method?: "GET" | "POST";
4
+ headers?: Record<string, string>;
5
+ body?: string;
6
+ }
7
+ /** What the site answered: the status, the URL after redirects, the body as text. */
8
+ export interface SiteResponse {
9
+ status: number;
10
+ url: string;
11
+ body: string;
12
+ }
13
+ type Transport = (url: string, request: SiteRequest) => Promise<SiteResponse>;
14
+ /** Sends this context's requests through `transport` instead of `context.request` (for the fake Moodle). */
15
+ export declare function setTransport(context: BrowserContext, transport: Transport): void;
16
+ /** A request to `url` with the context's cookies; redirects are followed, and any status is returned. */
17
+ export declare function siteFetch(context: BrowserContext, url: string, request?: SiteRequest): Promise<SiteResponse>;
18
+ export {};
@@ -0,0 +1,22 @@
1
+ // A context's own way to make requests, when it has one: the fake Moodle answers through its routes,
2
+ // which `context.request` doesn't go through (confirmed empirically, playwright-core 1.64).
3
+ const transports = new WeakMap();
4
+ /** Sends this context's requests through `transport` instead of `context.request` (for the fake Moodle). */
5
+ export function setTransport(context, transport) {
6
+ transports.set(context, transport);
7
+ }
8
+ /** A request to `url` with the context's cookies; redirects are followed, and any status is returned. */
9
+ export async function siteFetch(context, url, request = {}) {
10
+ const transport = transports.get(context);
11
+ if (transport)
12
+ return transport(url, request);
13
+ const response = await context.request.fetch(url, {
14
+ method: request.method ?? "GET",
15
+ headers: request.headers,
16
+ data: request.body,
17
+ maxRedirects: 10,
18
+ failOnStatusCode: false,
19
+ });
20
+ return { status: response.status(), url: response.url(), body: await response.text() };
21
+ }
22
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAmBA,qGAAqG;AACrG,4FAA4F;AAC5F,MAAM,UAAU,GAAG,IAAI,OAAO,EAA6B,CAAC;AAE5D,4GAA4G;AAC5G,MAAM,UAAU,YAAY,CAAC,OAAuB,EAAE,SAAoB;IACxE,UAAU,CAAC,GAAG,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;AACrC,CAAC;AAED,yGAAyG;AACzG,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,OAAuB,EAAE,GAAW,EAAE,UAAuB,EAAE;IAC7F,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,CAAC,GAAG,EAAE;QAChD,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,KAAK;QAC/B,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,YAAY,EAAE,EAAE;QAChB,gBAAgB,EAAE,KAAK;KACxB,CAAC,CAAC;IACH,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE,QAAQ,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC;AACzF,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Page } from "playwright-core";
2
+ /**
3
+ * Runs `fn(arg)` in the page, like `page.evaluate`, but with a `__name` helper defined first: tsx
4
+ * (the tests) compiles with esbuild's keepNames, which wraps named inner functions in `__name(...)`
5
+ * calls the page doesn't have (confirmed empirically). The bundle doesn't, and the helper is unused
6
+ * there. `arg` must be JSON.
7
+ */
8
+ export declare function inPage<A, R>(page: Page, fn: (arg: A) => R | Promise<R>, arg: A): Promise<R>;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Runs `fn(arg)` in the page, like `page.evaluate`, but with a `__name` helper defined first: tsx
3
+ * (the tests) compiles with esbuild's keepNames, which wraps named inner functions in `__name(...)`
4
+ * calls the page doesn't have (confirmed empirically). The bundle doesn't, and the helper is unused
5
+ * there. `arg` must be JSON.
6
+ */
7
+ export function inPage(page, fn, arg) {
8
+ return page.evaluate(`(() => { const __name = (f) => f; return (${fn.toString()})(${JSON.stringify(arg)}); })()`);
9
+ }
10
+ //# sourceMappingURL=inPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inPage.js","sourceRoot":"","sources":["../../src/core/inPage.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,UAAU,MAAM,CAAO,IAAU,EAAE,EAA8B,EAAE,GAAM;IAC7E,OAAO,IAAI,CAAC,QAAQ,CAAC,6CAA6C,EAAE,CAAC,QAAQ,EAAE,KAAK,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,SAAS,CAAe,CAAC;AAClI,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { BrowserContext, Page } from "playwright-core";
2
+ import type { BrowserManager } from "../browser/manager.js";
3
+ import { MoodleError } from "./errors.js";
4
+ export declare class LoginTimeoutError extends MoodleError {
5
+ name: string;
6
+ constructor(message: string);
7
+ }
8
+ export declare class LoginWindowClosedError extends MoodleError {
9
+ name: string;
10
+ constructor();
11
+ }
12
+ /** The logged-in user, read from their own profile page (`user/profile.php`). */
13
+ export declare function readOwnProfile(context: BrowserContext, siteUrl: string): Promise<{
14
+ userId: number;
15
+ fullName: string;
16
+ }>;
17
+ /**
18
+ * Waits until `page` shows the site with a user logged in. The person may go through other domains
19
+ * (SSO) on the way; reads fail while a page navigates, and are retried.
20
+ */
21
+ export declare function waitForLogin(page: Page, siteUrl: string, { timeoutMs, pollMs, onWait }: {
22
+ timeoutMs: number;
23
+ pollMs?: number;
24
+ onWait?: (elapsedMs: number) => void;
25
+ }): Promise<void>;
26
+ export interface LoginResult {
27
+ status: "already-logged-in" | "logged-in";
28
+ userId: number;
29
+ fullName: string;
30
+ }
31
+ /**
32
+ * Makes sure there's a session: the user at once if there is one; otherwise a browser window at the
33
+ * site's login page, waiting (calling `onWait`) until the person logs in there themselves. The session
34
+ * is saved (ADR-003), and the window closes again unless the browser is shown on demand. It waits
35
+ * `timeoutMs`, 5 minutes by default.
36
+ */
37
+ export declare function logIn(browser: BrowserManager, siteUrl: string, { timeoutMs, onWait }?: {
38
+ timeoutMs?: number;
39
+ onWait?: (elapsedMs: number) => void;
40
+ }): Promise<LoginResult>;
@@ -0,0 +1,83 @@
1
+ import { NotLoggedInError, openSitePage, pageIsLoggedIn } from "./session.js";
2
+ import { MoodleError } from "./errors.js";
3
+ const POLL_MS = 1000;
4
+ const LOGIN_TIMEOUT_MS = 5 * 60_000;
5
+ export class LoginTimeoutError extends MoodleError {
6
+ name = "LoginTimeoutError";
7
+ constructor(message) {
8
+ super(message, "login-timeout");
9
+ }
10
+ }
11
+ export class LoginWindowClosedError extends MoodleError {
12
+ name = "LoginWindowClosedError";
13
+ constructor() {
14
+ super("The login window was closed before the person logged in.", "login-window-closed");
15
+ }
16
+ }
17
+ /** The logged-in user, read from their own profile page (`user/profile.php`). */
18
+ export async function readOwnProfile(context, siteUrl) {
19
+ const page = await openSitePage(context, siteUrl, "user/profile.php");
20
+ try {
21
+ return await page.evaluate(() => {
22
+ const g = globalThis;
23
+ // The page heading is the user's full name, in any interface language.
24
+ const heading = g.document.querySelector("#page-header h1, .page-header-headings h1, h1");
25
+ return { userId: g.M.cfg.userId, fullName: heading?.textContent?.trim() ?? "" };
26
+ });
27
+ }
28
+ finally {
29
+ await page.close().catch(() => undefined);
30
+ }
31
+ }
32
+ /**
33
+ * Waits until `page` shows the site with a user logged in. The person may go through other domains
34
+ * (SSO) on the way; reads fail while a page navigates, and are retried.
35
+ */
36
+ export async function waitForLogin(page, siteUrl, { timeoutMs, pollMs = POLL_MS, onWait }) {
37
+ const start = Date.now();
38
+ for (;;) {
39
+ if (page.isClosed())
40
+ throw new LoginWindowClosedError();
41
+ if (await pageIsLoggedIn(page, siteUrl).catch(() => false))
42
+ return;
43
+ const elapsed = Date.now() - start;
44
+ if (elapsed >= timeoutMs)
45
+ throw new LoginTimeoutError(`Nobody logged in within ${Math.round(timeoutMs / 1000)} seconds; the window was closed.`);
46
+ onWait?.(elapsed);
47
+ await new Promise((resolve) => setTimeout(resolve, pollMs));
48
+ }
49
+ }
50
+ /**
51
+ * Makes sure there's a session: the user at once if there is one; otherwise a browser window at the
52
+ * site's login page, waiting (calling `onWait`) until the person logs in there themselves. The session
53
+ * is saved (ADR-003), and the window closes again unless the browser is shown on demand. It waits
54
+ * `timeoutMs`, 5 minutes by default.
55
+ */
56
+ export async function logIn(browser, siteUrl, { timeoutMs = LOGIN_TIMEOUT_MS, onWait } = {}) {
57
+ try {
58
+ return { status: "already-logged-in", ...(await readOwnProfile(await browser.context(), siteUrl)) };
59
+ }
60
+ catch (error) {
61
+ if (!(error instanceof NotLoggedInError))
62
+ throw error;
63
+ }
64
+ const context = await browser.context({ headed: true });
65
+ const page = context.pages()[0] ?? (await context.newPage());
66
+ await page.goto(new URL("login/index.php", siteUrl).href, { waitUntil: "domcontentloaded" });
67
+ await page.bringToFront();
68
+ try {
69
+ await waitForLogin(page, siteUrl, { timeoutMs, onWait });
70
+ }
71
+ catch (error) {
72
+ if (error instanceof LoginTimeoutError)
73
+ await browser.close();
74
+ throw error;
75
+ }
76
+ const user = await readOwnProfile(context, siteUrl);
77
+ await browser.saveSession();
78
+ // Back to the chosen mode: the next call reopens the browser without a window.
79
+ if (!browser.visible())
80
+ await browser.close();
81
+ return { status: "logged-in", ...user };
82
+ }
83
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/core/login.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAC9E,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE1C,MAAM,OAAO,GAAG,IAAI,CAAC;AACrB,MAAM,gBAAgB,GAAG,CAAC,GAAG,MAAM,CAAC;AAEpC,MAAM,OAAO,iBAAkB,SAAQ,WAAW;IACvC,IAAI,GAAG,mBAAmB,CAAC;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,EAAE,eAAe,CAAC,CAAC;IAClC,CAAC;CACF;AAED,MAAM,OAAO,sBAAuB,SAAQ,WAAW;IAC5C,IAAI,GAAG,wBAAwB,CAAC;IACzC;QACE,KAAK,CAAC,0DAA0D,EAAE,qBAAqB,CAAC,CAAC;IAC3F,CAAC;CACF;AAED,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAuB,EAAE,OAAe;IAC3E,MAAM,IAAI,GAAG,MAAM,YAAY,CAAC,OAAO,EAAE,OAAO,EAAE,kBAAkB,CAAC,CAAC;IACtE,IAAI,CAAC;QACH,OAAO,MAAM,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;YAC9B,MAAM,CAAC,GAAG,UAGT,CAAC;YACF,uEAAuE;YACvE,MAAM,OAAO,GAAG,CAAC,CAAC,QAAQ,CAAC,aAAa,CAAC,+CAA+C,CAAC,CAAC;YAC1F,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC;QAClF,CAAC,CAAC,CAAC;IACL,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;IAC5C,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAU,EACV,OAAe,EACf,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,EAAE,MAAM,EAAgF;IAErH,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACzB,SAAS,CAAC;QACR,IAAI,IAAI,CAAC,QAAQ,EAAE;YAAE,MAAM,IAAI,sBAAsB,EAAE,CAAC;QACxD,IAAI,MAAM,cAAc,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC;YAAE,OAAO;QACnE,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC;QACnC,IAAI,OAAO,IAAI,SAAS;YAAE,MAAM,IAAI,iBAAiB,CAAC,2BAA2B,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,IAAI,CAAC,kCAAkC,CAAC,CAAC;QACjJ,MAAM,EAAE,CAAC,OAAO,CAAC,CAAC;QAClB,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,CAAC;AACH,CAAC;AAQD;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,KAAK,CACzB,OAAuB,EACvB,OAAe,EACf,EAAE,SAAS,GAAG,gBAAgB,EAAE,MAAM,KAAmE,EAAE;IAE3G,IAAI,CAAC;QACH,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,GAAG,CAAC,MAAM,cAAc,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC;IACtG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,gBAAgB,CAAC;YAAE,MAAM,KAAK,CAAC;IACxD,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7D,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,CAAC,CAAC;IAC7F,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IAC3D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,KAAK,YAAY,iBAAiB;YAAE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;QAC9D,MAAM,KAAK,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,cAAc,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACpD,MAAM,OAAO,CAAC,WAAW,EAAE,CAAC;IAC5B,+EAA+E;IAC/E,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE;QAAE,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IAC9C,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,IAAI,EAAE,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,4 @@
1
+ /** How long a person would take to read `text`, in seconds. */
2
+ export declare function readingSeconds(text: string): number;
3
+ /** Waits `seconds`, calling `onProgress` every few seconds so clients keep waiting. */
4
+ export declare function pause(seconds: number, onProgress?: (elapsedSeconds: number) => void): Promise<void>;
@@ -0,0 +1,21 @@
1
+ // Reading at a person's pace (ADR-005): about 240 words a minute, bounded per page.
2
+ const WORDS_PER_SECOND = 4;
3
+ const MIN_SECONDS = 3;
4
+ const MAX_SECONDS = 90;
5
+ const PROGRESS_EVERY_MS = 5000;
6
+ /** How long a person would take to read `text`, in seconds. */
7
+ export function readingSeconds(text) {
8
+ const words = text.split(/\s+/).filter(Boolean).length;
9
+ return Math.min(MAX_SECONDS, Math.max(MIN_SECONDS, Math.round(words / WORDS_PER_SECOND)));
10
+ }
11
+ /** Waits `seconds`, calling `onProgress` every few seconds so clients keep waiting. */
12
+ export async function pause(seconds, onProgress) {
13
+ const end = Date.now() + seconds * 1000;
14
+ // A timer can fire a millisecond before Date.now() says it's due: what's left below that is done,
15
+ // or the loop would go round once more and report the same second twice.
16
+ while (end - Date.now() > 5) {
17
+ await new Promise((resolve) => setTimeout(resolve, Math.min(PROGRESS_EVERY_MS, end - Date.now())));
18
+ onProgress?.(Math.round(seconds - (end - Date.now()) / 1000));
19
+ }
20
+ }
21
+ //# sourceMappingURL=pace.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pace.js","sourceRoot":"","sources":["../../src/core/pace.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,WAAW,GAAG,EAAE,CAAC;AACvB,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B,+DAA+D;AAC/D,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC;IACvD,OAAO,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,gBAAgB,CAAC,CAAC,CAAC,CAAC;AAC5F,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,OAAe,EAAE,UAA6C;IACxF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,GAAG,IAAI,CAAC;IACxC,kGAAkG;IAClG,yEAAyE;IACzE,OAAO,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;QACnG,UAAU,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;IAChE,CAAC;AACH,CAAC"}