domma-cms 1.8.32 → 1.8.34

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 (110) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/docs/DommaFeatures.md +13 -1
  3. package/package.json +1 -1
  4. package/plugins/_lib/admin/games/games.css +1 -1
  5. package/plugins/_lib/admin/games/kit.js +329 -124
  6. package/plugins/_lib/games/http.js +11 -1
  7. package/plugins/_lib/games/plays.js +28 -7
  8. package/plugins/crossword/CLAUDE.md +37 -0
  9. package/plugins/crossword/admin/css/index.css +18 -1
  10. package/plugins/crossword/admin/views/crossword.js +337 -205
  11. package/plugins/crossword/docs/guide.md +53 -0
  12. package/plugins/crossword/inject/body.html +1 -1
  13. package/plugins/crossword/plugin.js +38 -5
  14. package/plugins/crossword/plugin.json +4 -3
  15. package/plugins/crossword/public/crossword.css +36 -1
  16. package/plugins/crossword/public/crossword.js +468 -6
  17. package/plugins/docs/CLAUDE.md +318 -0
  18. package/plugins/docs/admin/css/docs.css +783 -0
  19. package/plugins/docs/admin/lib/api.js +340 -0
  20. package/plugins/docs/admin/lib/convert.js +482 -0
  21. package/plugins/docs/admin/lib/drafts.js +115 -0
  22. package/plugins/docs/admin/lib/editor.js +851 -0
  23. package/plugins/docs/admin/lib/find-replace.js +426 -0
  24. package/plugins/docs/admin/lib/folders.js +203 -0
  25. package/plugins/docs/admin/lib/panel-guard.js +73 -0
  26. package/plugins/docs/admin/lib/pdf-view.js +126 -0
  27. package/plugins/docs/admin/lib/send.js +156 -0
  28. package/plugins/docs/admin/lib/settings.js +142 -0
  29. package/plugins/docs/admin/lib/share.js +323 -0
  30. package/plugins/docs/admin/lib/shortcuts.js +44 -0
  31. package/plugins/docs/admin/lib/templates.js +194 -0
  32. package/plugins/docs/admin/lib/versions.js +192 -0
  33. package/plugins/docs/admin/templates/docs.html +92 -0
  34. package/plugins/docs/admin/templates/editor.html +86 -0
  35. package/plugins/docs/admin/views/docs.js +777 -0
  36. package/plugins/docs/admin/views/editor.js +978 -0
  37. package/plugins/docs/config.js +24 -0
  38. package/plugins/docs/docs/guide.md +270 -0
  39. package/plugins/docs/lib/extensions.js +213 -0
  40. package/plugins/docs/lib/importer.js +72 -0
  41. package/plugins/docs/lib/pdf-worker.js +48 -0
  42. package/plugins/docs/lib/pdf.js +222 -0
  43. package/plugins/docs/lib/store.js +748 -0
  44. package/plugins/docs/plugin.js +1108 -0
  45. package/plugins/docs/plugin.json +89 -0
  46. package/plugins/docs/plugin.public.js +114 -0
  47. package/plugins/docs/templates/public-doc.html +36 -0
  48. package/plugins/docs/tests/_env.js +10 -0
  49. package/plugins/docs/tests/_pdf.js +73 -0
  50. package/plugins/docs/tests/convert.test.js +95 -0
  51. package/plugins/docs/tests/drafts.test.js +82 -0
  52. package/plugins/docs/tests/pdf.test.js +86 -0
  53. package/plugins/docs/tests/plugin.test.js +816 -0
  54. package/plugins/docs/tests/shortcuts.test.js +30 -0
  55. package/plugins/free-tier.lock.json +147 -1
  56. package/plugins/garage/CLAUDE.md +51 -0
  57. package/plugins/garage/admin/css/garage.css +73 -0
  58. package/plugins/garage/admin/lib/garage.js +236 -0
  59. package/plugins/garage/admin/templates/garage.html +86 -0
  60. package/plugins/garage/admin/views/garage.js +581 -0
  61. package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
  62. package/plugins/garage/config.js +18 -0
  63. package/plugins/garage/docs/guide.md +118 -0
  64. package/plugins/garage/plugin.js +471 -0
  65. package/plugins/garage/plugin.json +68 -0
  66. package/plugins/garage/tests/access.test.js +134 -0
  67. package/plugins/garage/tests/garage.test.js +106 -0
  68. package/plugins/messages/CLAUDE.md +230 -0
  69. package/plugins/messages/admin/css/messages.css +117 -0
  70. package/plugins/messages/admin/lib/messages.js +452 -0
  71. package/plugins/messages/admin/templates/messages.html +76 -0
  72. package/plugins/messages/admin/views/messages.js +1219 -0
  73. package/plugins/messages/collections/user-message-threads/schema.json +22 -0
  74. package/plugins/messages/collections/user-messages/schema.json +23 -0
  75. package/plugins/messages/config.js +14 -0
  76. package/plugins/messages/docs/guide.md +176 -0
  77. package/plugins/messages/lib/call.js +138 -0
  78. package/plugins/messages/plugin.js +994 -0
  79. package/plugins/messages/plugin.json +59 -0
  80. package/plugins/messages/plugin.public.js +52 -0
  81. package/plugins/messages/templates/call.html +6 -0
  82. package/plugins/messages/tests/after-send.test.js +22 -0
  83. package/plugins/messages/tests/api.test.js +187 -0
  84. package/plugins/messages/tests/call.test.js +497 -0
  85. package/plugins/messages/tests/drafts.test.js +25 -0
  86. package/plugins/messages/tests/messages.test.js +167 -0
  87. package/plugins/quiz/CLAUDE.md +43 -0
  88. package/plugins/quiz/admin/css/index.css +13 -1
  89. package/plugins/quiz/admin/views/quiz.js +423 -245
  90. package/plugins/quiz/docs/guide.md +61 -0
  91. package/plugins/quiz/inject/body.html +1 -1
  92. package/plugins/quiz/plugin.js +39 -5
  93. package/plugins/quiz/plugin.json +4 -3
  94. package/plugins/quiz/public/quiz.css +36 -1
  95. package/plugins/quiz/public/quiz.js +298 -6
  96. package/plugins/theme-roller/CLAUDE.md +65 -0
  97. package/plugins/theme-roller/admin/css/theme-roller.css +43 -0
  98. package/plugins/theme-roller/admin/lib/prompt.js +22 -0
  99. package/plugins/theme-roller/admin/lib/themes.js +196 -0
  100. package/plugins/theme-roller/admin/lib/view.js +88 -0
  101. package/plugins/theme-roller/admin/templates/theme-roller.html +73 -0
  102. package/plugins/theme-roller/admin/views/theme-roller-view.js +639 -0
  103. package/plugins/theme-roller/config.js +1 -0
  104. package/plugins/theme-roller/docs/guide.md +104 -0
  105. package/plugins/theme-roller/plugin.js +263 -0
  106. package/plugins/theme-roller/plugin.json +42 -0
  107. package/plugins/theme-roller/tests/api.test.js +161 -0
  108. package/plugins/theme-roller/tests/prompt-values.test.js +22 -0
  109. package/plugins/theme-roller/tests/themes.test.js +77 -0
  110. package/plugins/theme-roller/tests/view.test.js +55 -0
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Docs plugin defaults.
3
+ *
4
+ * The admin screen reads these through GET /api/plugins/docs/settings, which
5
+ * whitelists what it needs. Adding a secret here does not leak it.
6
+ *
7
+ * `editorMode` is gone (2.0): documents are always HTML. A site that was set
8
+ * to 'markdown' has its documents converted once at boot (see plugin.js).
9
+ */
10
+ export default {
11
+ // 'all' lets every signed-in user read every document (and admins change
12
+ // them); 'user' keeps documents private unless shared with people.
13
+ scope: 'user',
14
+ autoSaveInterval: 30000, // ms, 0 to disable
15
+ maxVersions: 50,
16
+ // Whether "Anyone with the link" can be switched on for a document.
17
+ publicLinks: true,
18
+ // Whether documents can be emailed from Docs (Send to > Email). Each
19
+ // person may email up to 50 addresses a day either way.
20
+ allowSend: true,
21
+ // Days a document stays in Trash before it is deleted for good.
22
+ trashDays: 30
23
+ // No `storage` key: Docs keeps everything as JSON files under data/.
24
+ };
@@ -0,0 +1,270 @@
1
+ ---
2
+ title: Guide
3
+ order: 1
4
+ ---
5
+
6
+ Docs is a document editor inside the admin. Write, share with the people you work with, publish a read-only link, download or print, and send a document on as a page, a blog post or an email.
7
+
8
+ ## What it does
9
+
10
+ - **Home** lists your documents as cards or a list: Recent, Starred, Shared with me, My documents (with your folders), Templates and Trash.
11
+ - Each document opens in its own **editor**, with a page to write on, a formatting toolbar, an Outline of its headings, and a word count.
12
+ - **Markdown shortcuts** while you type: `#` and a space for a heading, `-` for a list, `[]` for a checklist, and more (below).
13
+ - **Share** with other people on the site as Viewers or Editors, and optionally email them.
14
+ - **Public link**: "Anyone with the link can view" gives a read-only web page for the document.
15
+ - **Import** `.md`, `.html` and `.txt` files. **Download** as Markdown, a web page or plain text, or **print / save as PDF**.
16
+ - **PDF documents**: add a PDF and it is kept exactly as it is - filed, shared, linked, emailed and versioned like any document, and shown in your browser's own PDF viewer.
17
+ - **Send to** a new page, a new blog post, or an email.
18
+ - **One editor at a time**: while someone is editing, everyone else sees the document read only, with their name.
19
+ - **Version history** with a preview of every earlier version, and restore.
20
+ - **Trash**: deleted documents can be restored for 30 days (a setting).
21
+
22
+ Documents are private to whoever made them until they are shared.
23
+
24
+ ## Getting started
25
+
26
+ 1. Open [Docs](#/plugins/docs) from the admin sidebar.
27
+ 2. Click **New** (the **+** in the Docs banner at the top) and choose **Blank document**.
28
+ 3. Start typing. Type `# ` (a hash and a space) to make the first line a heading.
29
+ 4. The document saves by itself shortly after you stop typing. Ctrl+S saves at once.
30
+ 5. An untitled document takes its first line as its title when it first saves. Click the title at the top to change it.
31
+ 6. Click the arrow at the top left to go back to Home.
32
+
33
+ ## Home
34
+
35
+ - **New** (the **+** in the banner): **Blank document**, **From a template...**, **Import a file...**, **Upload a PDF...** or **New folder...**.
36
+ **Import** (the upload arrow beside it) goes straight to choosing files.
37
+ - Right-click a document, template or folder - or use its **⋮**, or select it and press **Shift+F10** - for everything you can do to it.
38
+ - The places on the left:
39
+ - **Recent**: documents you have opened or edited.
40
+ - **Starred**: click the star on a card, or in the editor, to keep a document here.
41
+ - **Shared with me**: other people's documents they have shared with you.
42
+ - **My documents**: your own. Your folders are listed under **Folders**.
43
+ - **Templates**: click one to start a new document from it.
44
+ - **Trash**.
45
+ - **Search** matches titles and text in the place you are looking at. Press `/` to jump to it.
46
+ - Sort by **Last modified**, **Last opened by me**, **Title** or **Created**. Switch between **grid** and **list** with the buttons on the right.
47
+ - Hide the left panel with the panel button next to the title.
48
+ - **Drag files** from your computer onto the list to import them.
49
+ - The Share, Email and Docs settings panels ask before closing if you have changed something and not saved or sent it.
50
+ Closing the browser tab while a document has unsaved changes asks too.
51
+ - Right-click a document, or click its three-dot button, for:
52
+ - **Open**
53
+ - **Open in a new tab**
54
+ - **Star** or **Remove star**
55
+ - **Rename...** (owner)
56
+ - **Make a copy**
57
+ - **Move to** (owner)
58
+ - **Share...** (owner and editors)
59
+ - **Download**
60
+ - **Email...**
61
+ - **Move to Trash** (owner)
62
+ - A card shows **Shared**, **Public link**, or who is editing it right now. A PDF's card says **PDF**, with its pages and size where a written document shows its word count.
63
+
64
+ ### Folders
65
+
66
+ - Folders are yours alone. Documents shared with you appear under **Shared with me**, never in your folders.
67
+ - Right-click a folder (or use its three-dot button) for **New subfolder...**, **Rename...** and **Delete folder...**.
68
+ - Deleting a folder never deletes documents. They, and any subfolders, move up a level.
69
+
70
+ ### Trash
71
+
72
+ - **Move to Trash** takes a document out of every list, including for the people it was shared with. Its public link stops working while it is there.
73
+ - In Trash, right-click a document for **Restore** or **Delete for good...**. **Empty Trash** deletes everything in it.
74
+ - Documents are deleted for good after the number of days set in Docs settings (30 by default).
75
+
76
+ ## The editor
77
+
78
+ The top bar holds:
79
+
80
+ - the **back** arrow
81
+ - the **title**
82
+ - where the document lives, and whether it is shared
83
+ - the save state: **Unsaved changes**, **Saving...**, **Saved 14:05**, or **Not saved** in red when a save failed
84
+ - the **star**
85
+ - **Outline**, **Find and replace** (Ctrl+F), **Version history** and **Focus mode**
86
+ - **Share**
87
+ - the three-dot menu
88
+
89
+ ### Toolbar
90
+
91
+ | Control | Does |
92
+ |---|---|
93
+ | Text style | Normal text, Heading 1 to 4, Code block, Quote |
94
+ | Undo, Redo | Ctrl+Z, Ctrl+Y |
95
+ | Bold, Italic, Underline, Strikethrough | Ctrl+B, Ctrl+I, Ctrl+U |
96
+ | Highlight | Marks the selection in yellow. Click again to remove. |
97
+ | Inline code | Monospaced text for commands and names |
98
+ | Align left, Centre, Align right | The paragraph's alignment |
99
+ | Bulleted list, Numbered list | Tab indents a list item, Shift+Tab outdents it |
100
+ | Checklist | A list with tick boxes. Click a box to tick it. |
101
+ | Quote | An indented quotation |
102
+ | Link | Ctrl+K. Ctrl+click a link to open it. |
103
+ | Image | Choose a picture from Media, or upload one. You can also paste or drop an image into the page. It is saved to Media. |
104
+ | Table | Enter the size, e.g. `3x4` (columns x rows). Right-click a cell to add or delete rows and columns, or delete the table. Tab moves to the next cell. |
105
+ | Horizontal line | A divider |
106
+ | Clear formatting | Removes bold, colours and so on from the selection |
107
+
108
+ ### Markdown shortcuts
109
+
110
+ Type these at the start of an empty line, then a space:
111
+
112
+ | Type | Get |
113
+ |---|---|
114
+ | `#`, `##`, `###`, `####` | Heading 1 to 4 |
115
+ | `-`, `*` or `+` | Bulleted list |
116
+ | `1.` or `1)` | Numbered list |
117
+ | `[]` or `[ ]` | Checklist |
118
+ | `>` | Quote |
119
+ | ```` ``` ```` | Code block |
120
+
121
+ Type `---` and press Enter for a horizontal line.
122
+
123
+ ### Outline
124
+
125
+ The **Outline** button shows the document's headings on the left. Click one to jump to it.
126
+
127
+ ### Focus mode
128
+
129
+ The **Focus mode** button (or Ctrl+Shift+F) hides the admin around the document. Press Escape or click the button again to leave.
130
+
131
+ ### The three-dot menu
132
+
133
+ - **Save** (Ctrl+S)
134
+ - **Rename...**
135
+ - **Make a copy**
136
+ - **Move to**
137
+ - **Save as template...**
138
+ - **Download**: **Markdown (.md)**, **Web page (.html)**, **Plain text (.txt)**, or **Print or save as PDF...**. In the print dialog, choose "Save as PDF" as the printer to get a PDF.
139
+ - **Print or save as PDF...**
140
+ - **Send to**:
141
+ - **Email...**
142
+ - **A new page...**
143
+ - **A new blog post...**
144
+ - **Version history**
145
+ - **Move to Trash**
146
+
147
+ ## Sharing
148
+
149
+ 1. Click **Share** in the editor, or **Share...** on a document's menu on Home.
150
+ 2. Under **Add people**, type a name and pick the person. Only people whose role includes Docs are listed.
151
+ 3. Choose **Viewer** or **Editor** (or **Suggester**, with Docs Pro) next to their name.
152
+ 4. Leave **Email the people I've added** ticked to tell them, and add a message if you like.
153
+ 5. Click **Save access**.
154
+
155
+ | Who | Can |
156
+ |---|---|
157
+ | Owner | Everything, including Move to, Rename, removing editors and Move to Trash |
158
+ | Editor | Edit, share with more people, turn the public link on or off, email the document, see Version history |
159
+ | Suggester (with Docs Pro) | What a Viewer can, and suggest changes for the owner or an editor to accept or reject. Without Docs Pro, a Viewer |
160
+ | Viewer | Read, make their own copy, download and print (not the Version history - that is the editors') |
161
+
162
+ To stop sharing with someone, click the **x** next to their name and **Save access**.
163
+ ### Public link
164
+
165
+ 1. In the Share panel, tick **Anyone with the link can view**.
166
+ 2. Click **Copy** and send the address to whoever needs it.
167
+
168
+ - The address is long and unguessable. Anyone who has it can read the document, and download it as Markdown, HTML or text, without signing in.
169
+ - The page uses your site's look, and search engines are told not to list it.
170
+ - Untick the box and the address stops working at once. Ticking it again makes a new address.
171
+ - An administrator can switch public links off for the whole site in **Docs settings**. That stops every existing link.
172
+
173
+ ## One editor at a time
174
+
175
+ - When you open a document you may edit, you are its editor until you leave it.
176
+ - Anyone else who opens it sees it **read only**, with "*name* is editing" at the top. Their screen refreshes by itself to show the latest saved version.
177
+ - When the editor leaves, **Edit now** appears.
178
+ - The owner can click **Take over**. Whatever the other person had not saved is kept for them as a copy called "*title* (your unsaved changes)", in their My documents.
179
+ - If you open a document you are already editing in another tab, you see **Open for editing in another tab**. Click **Edit here** to move editing to this tab.
180
+ - If your computer sleeps long enough for someone else to start editing and save, your next save is not written over theirs: your version is kept as a copy called "*title* (your unsaved changes)" and the page shows theirs.
181
+
182
+ ## Never losing work
183
+
184
+ - Autosave saves shortly after you stop typing (the delay is a setting).
185
+ - Leaving a document with unsaved changes asks you to **Save and leave** or **Stay**.
186
+ - Closing the browser tab with unsaved changes asks the browser to confirm, and keeps a draft in this browser.
187
+ - Next time you open the document, you are asked whether to **Put them back**.
188
+
189
+ ## Send to
190
+
191
+ - **Email...**: up to 10 addresses at a time, and 50 a day per person. Send **The document, in the email** (a copy as it is now) or **A link to read it online**, which turns on the public link if it is not on already. The site's email settings are used.
192
+ - **A new page...**: makes a draft page at an address you choose, converted to Markdown, and offers to open it in the page editor. Your role needs permission to create pages.
193
+ - **A new blog post...**: makes a draft post in the Blog, when the Blog is switched on and your role may write posts.
194
+
195
+ ## Import
196
+
197
+ - **New > Import a file...**, or drag files onto Home.
198
+ - `.md` / `.markdown`:
199
+ - Headings, lists, task lists, tables, links, images and code come across.
200
+ - Front matter at the top is ignored.
201
+ - `.html` / `.htm`: the body is kept, scripts and styles are removed.
202
+ - `.txt`: each block of lines becomes a paragraph.
203
+ - A first-level heading at the very top becomes the title. Otherwise the file name is used.
204
+ - Up to 4 MB per file.
205
+ - With **Docs Pro**, Word documents (`.docx`) too - see the Docs Pro guide.
206
+ - A `.pdf` is not converted: it becomes a **PDF document** (below).
207
+
208
+ ## PDF documents
209
+
210
+ A PDF you add is a document in Docs that happens to be a file. Docs never changes the file.
211
+
212
+ - **Add one**: **New > Upload a PDF...**, **Import**, or drop it on the list - up to 20 MB. Its name becomes the title.
213
+ - **Open it** and the PDF shows in your browser's own viewer, with the viewer's zoom, search, print and save. There is no
214
+ toolbar or outline: there is nothing to type into. The chip beside the title says **PDF**, its pages and size.
215
+ - The text inside the PDF is read when it is added, so **search** on Home finds words in it and its card shows the first
216
+ lines. A scanned PDF (pictures of pages) and a password-protected one have no text to read; they are kept all the same.
217
+ (On a server without the optional `pdfjs-dist` package, nothing inside PDFs is read - the title is still searched.)
218
+ - Everything else works as for any document: **Star**, **Rename**, **Make a copy** (the copy has its own file), **Move to**,
219
+ **Share** (Viewers and Editors see the same viewer), the **public link** (a web page framing the PDF, with **Download
220
+ PDF**), **Email** (the PDF is attached, or a link is sent), **Trash**.
221
+ - **Replace the PDF...** (owner and editors, in the **⋮** menu) uploads a new file. The one it had is kept in **Version
222
+ history**, where each version shows its pages and size, can be downloaded, and can be restored.
223
+ - **Download PDF** is in the **⋮** menu and under **Download** on Home.
224
+ - A PDF is **checked before it is kept**: one that contains JavaScript, can start programs, has files attached inside it,
225
+ holds rich media or is an XFA form is refused with the reason. Nothing is stripped out - the file is either kept whole
226
+ or not at all.
227
+ - Docs Pro features that work on a document's text (Spaces, signatures, Word files, comments, suggestions, merge fields)
228
+ do not apply to PDF documents yet.
229
+
230
+ ## Version history
231
+
232
+ 1. Click the clock in the editor, or **Version history** on the menu.
233
+ 2. Pick a version on the left to see it on the right, with when and who saved it.
234
+ 3. Click **Restore this version**. What the document says now is kept as a version first, so a restore can itself be undone.
235
+
236
+ - A version is kept whenever a save changes the text - except that one person's saves within ten minutes of each other count as one editing session, so autosave does not fill the history.
237
+ - The oldest go once there are more than the number set in **Docs settings** (50 by default).
238
+ - You can restore only while you are the editor.
239
+
240
+ ## Templates
241
+
242
+ - Open a document and choose **Save as template...** from its menu.
243
+ - Start from one with **New > From a template...**, or click it under **Templates**.
244
+ - Right-click a template to delete it. Documents made from it are not affected.
245
+ - With **Docs Pro**, ready-made templates (letters, quotes, agreements, policies and more) are listed after your own. They are marked with their category and cannot be deleted.
246
+
247
+ ## Wiki pages
248
+
249
+ With the **Wiki** switched on, a document can be a wiki page (the owner adds it from its menu). People who edit the wiki can then edit the page here too, and see its history - but they cannot share it, give it a public link or send it, because they reach it through the wiki. While a wiki is for members only, nobody - its owner included - can share its pages with anyone new, link them publicly or send them. A page protected or locked in the Wiki shows a **Protected** chip and opens read only, for its owner too; hover the chip to see why.
250
+
251
+ ## Settings
252
+
253
+ The cog on the banner opens **Docs settings**. **For everyone on this site** needs an administrator:
254
+
255
+ - **Autosave**: Off, or 10 seconds to 5 minutes after you stop typing.
256
+ - **Versions kept per document**: 5 to 500.
257
+ - **Days in Trash**: 1 to 365.
258
+ - **Allow public links**.
259
+ - **Allow emailing documents**: Send to > Email. Each person can email up to 50 addresses a day.
260
+ - **Who sees documents** is shown but not changed here. **Private, unless shared** is the default. **Everyone with Docs** lets every Docs user read every document and administrators change them. It is set in the plugin's configuration.
261
+
262
+ **Just for you** (this browser):
263
+
264
+ - **Show word counts on documents**.
265
+
266
+ ## Tips
267
+
268
+ - Ctrl+S saves, Ctrl+F finds, Ctrl+K adds a link, Ctrl+Shift+F is focus mode, and `/` on Home jumps to search.
269
+ - Right-click inside a table for the table tools. Anywhere else, right-click gives your browser's own menu (spelling suggestions).
270
+ - A Viewer who wants to change a document can **Make a copy**. The copy is theirs.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Docs' door for extensions - how Docs Pro adds signing, protected links and
3
+ * the rest without replacing Docs.
4
+ *
5
+ * On `globalThis` under a registered symbol, so an extension reaches it
6
+ * without importing a file from this plugin (plugins load in one process, in
7
+ * no promised order). An extension registers from its `onReady` hook:
8
+ *
9
+ * const docs = globalThis[Symbol.for('domma.docs')];
10
+ * if (docs?.version >= 1) docs.register('docs-pro', {
11
+ * label: 'Docs Pro',
12
+ * admin: '/plugins/docs-pro/admin/docs-extension.js', // imported by the Docs screens
13
+ * markup: {tags: [...], attributes: {span: ['data-comment']}},
14
+ * publicGate: async ({doc, request, reply}) => null | {handled: true},
15
+ * linkInfo: (doc) => ({expiresAt, protected}),
16
+ * on: {saved(doc, user), trashed(doc, user), deleted(doc), restored(doc, user, num)},
17
+ * // 2.5: suggested edits. check() gets the stored body and what a Suggester
18
+ * // sent; it answers the body to store, or null to refuse. publicHtml()
19
+ * // filters a body before it leaves Docs (public page, downloads, email).
20
+ * suggest: {check(beforeHtml, afterHtml, user) => string|null},
21
+ * publicHtml: (html) => html,
22
+ * // 2.6: ready-made templates, listed after the user's own, read only.
23
+ * templates: () => [{id, name, description, category, content}],
24
+ * // 2.11: access to documents the add-on looks after (the Wiki's pages), on
25
+ * // top of owner and shares. Synchronous; 'editor' | 'viewer' | null.
26
+ * access: (doc, user) => 'editor' | 'viewer' | null,
27
+ * // 2.11: documents that must not leave Docs right now (a members-only wiki's
28
+ * // pages): no public link (an existing one stops working), no sending.
29
+ * restrict: (doc) => null | {reason},
30
+ * // 2.12: documents this person may not change right now (a protected or locked
31
+ * // wiki page) - owner included. Synchronous. Refuses the text and title, the
32
+ * // edit lock, version restore and Trash; reading, history and the rest stay.
33
+ * freeze: (doc, user) => null | {reason},
34
+ * // 2.13: the kinds of document this extension understands - 'html' (a body
35
+ * // written in the editor) and/or 'pdf' (a file). Default ['html']: an add-on
36
+ * // written before PDF documents existed sees none of them - its menu items,
37
+ * // chips, share sections and events (`emit`) skip PDF documents, and its admin
38
+ * // module is not installed in a PDF document's screen. Services are not
39
+ * // filtered: an add-on that lists documents itself should check `kindOf(doc)`.
40
+ * kinds: ['html']
41
+ * });
42
+ *
43
+ * and uses `docs.services` (readDoc, writeDoc, accessOf, sanitizeDoc, ...) to
44
+ * work on documents the way Docs does. When Pro's licence lapses it never
45
+ * registers, and Docs carries on alone.
46
+ *
47
+ * @module docs/lib/extensions
48
+ */
49
+
50
+ export const KEY = Symbol.for('domma.docs');
51
+ export const VERSION = 1;
52
+
53
+ /** The registry, made once per process. */
54
+ export function docsRegistry() {
55
+ if (globalThis[KEY]?.version === VERSION) return globalThis[KEY];
56
+ const extensions = new Map();
57
+ const reg = {
58
+ version: VERSION,
59
+ services: {},
60
+
61
+ register(owner, ext = {}) {
62
+ if (!/^[a-z0-9-]{2,60}$/.test(String(owner))) throw new Error('An extension needs its plugin name.');
63
+ extensions.set(owner, {
64
+ owner,
65
+ label: String(ext.label || owner),
66
+ admin: typeof ext.admin === 'string' && ext.admin.startsWith('/plugins/') ? ext.admin : '',
67
+ publicGate: typeof ext.publicGate === 'function' ? ext.publicGate : null,
68
+ linkInfo: typeof ext.linkInfo === 'function' ? ext.linkInfo : null,
69
+ on: ext.on || {},
70
+ suggest: ext.suggest && typeof ext.suggest.check === 'function' ? ext.suggest : null,
71
+ publicHtml: typeof ext.publicHtml === 'function' ? ext.publicHtml : null,
72
+ templates: typeof ext.templates === 'function' ? ext.templates : null,
73
+ access: typeof ext.access === 'function' ? ext.access : null,
74
+ restrict: typeof ext.restrict === 'function' ? ext.restrict : null,
75
+ freeze: typeof ext.freeze === 'function' ? ext.freeze : null,
76
+ kinds: Array.isArray(ext.kinds) && ext.kinds.length ? ext.kinds.map(String).filter(k => k === 'html' || k === 'pdf') : ['html'],
77
+ order: Number(ext.order) || 0
78
+ });
79
+ if (ext.markup && typeof reg.services.allowMarkup === 'function') reg.services.allowMarkup(ext.markup);
80
+ },
81
+
82
+ unregister(owner) { extensions.delete(owner); },
83
+
84
+ list() { return [...extensions.values()].sort((a, b) => a.order - b.order); },
85
+
86
+ /** Admin modules the Docs screens import, in order - with the kinds of document each understands (2.13). */
87
+ adminModules() {
88
+ return reg.list().filter(e => e.admin).map(e => ({owner: e.owner, label: e.label, module: e.admin, kinds: e.kinds}));
89
+ },
90
+
91
+ /** Does this extension understand this document (2.13)? Extensions written before PDF documents understand HTML ones only. */
92
+ handles(ext, doc) {
93
+ const kind = doc?.format === 'pdf' ? 'pdf' : 'html';
94
+ return (ext?.kinds || ['html']).includes(kind);
95
+ },
96
+
97
+ /**
98
+ * Ask each extension whether it answers a public-link request itself
99
+ * (a password form, an expired link). The first that handles it wins.
100
+ */
101
+ async gate(ctx) {
102
+ for (const ext of reg.list()) {
103
+ if (!ext.publicGate) continue;
104
+ try {
105
+ const r = await ext.publicGate(ctx);
106
+ if (r?.handled) return r;
107
+ } catch (err) {
108
+ ctx.request?.log?.error?.({err}, `[docs] ${ext.owner} publicGate failed`);
109
+ return {handled: true, error: true};
110
+ }
111
+ }
112
+ return null;
113
+ },
114
+
115
+ /** Is any extension able to answer for a protected link? (2.3) */
116
+ hasGate() { return reg.list().some(e => e.publicGate); },
117
+
118
+ /** Extra facts about a document's public link (expiry, password), merged. */
119
+ linkInfo(doc) {
120
+ return Object.assign({}, ...reg.list().map((e) => {
121
+ try { return e.linkInfo ? e.linkInfo(doc) || {} : {}; } catch { return {}; }
122
+ }));
123
+ },
124
+
125
+ /** Does an extension let Suggesters suggest? (2.5) Without one a Suggester is a Viewer. */
126
+ hasSuggest() { return reg.list().some(e => e.suggest); },
127
+
128
+ /**
129
+ * A Suggester's save: the first extension's answer - the body to store, or
130
+ * null (refused). Refused when none is there, or when it throws.
131
+ */
132
+ checkSuggestion(before, after, user) {
133
+ const ext = reg.list().find(e => e.suggest);
134
+ if (!ext) return null;
135
+ try { const r = ext.suggest.check(before, after, user); return typeof r === 'string' ? r : null; } catch { return null; }
136
+ },
137
+
138
+ /** Extensions' ready-made templates (2.6): [{owner, label, ...template}]. Never throws. */
139
+ builtInTemplates() {
140
+ const out = [];
141
+ for (const ext of reg.list()) {
142
+ if (!ext.templates) continue;
143
+ try {
144
+ for (const t of ext.templates() || []) if (t && /^[a-z0-9-]{1,60}$/.test(String(t.id))) out.push({...t, owner: ext.owner, label: ext.label});
145
+ } catch { /* an extension's problem */ }
146
+ }
147
+ return out;
148
+ },
149
+
150
+ /**
151
+ * The strongest access any extension gives this user on this document (2.11):
152
+ * 'editor', 'viewer' or null. Never throws - an extension that fails gives none.
153
+ */
154
+ extraAccess(doc, user) {
155
+ let best = null;
156
+ for (const ext of extensions.values()) {
157
+ if (!ext.access) continue;
158
+ let r = null;
159
+ try { r = ext.access(doc, user); } catch { r = null; }
160
+ if (r === 'editor') return 'editor';
161
+ if (r === 'viewer') best = 'viewer';
162
+ }
163
+ return best;
164
+ },
165
+
166
+ /**
167
+ * Why this document may not leave Docs now (2.11) - a reason, or '' when it may.
168
+ * An extension that throws restricts (fail closed while it is there).
169
+ */
170
+ restrictionOf(doc) {
171
+ for (const ext of extensions.values()) {
172
+ if (!ext.restrict) continue;
173
+ try { const r = ext.restrict(doc); if (r) return String(r.reason || `${ext.label} keeps this document private.`); } catch { return `${ext.label} keeps this document private.`; }
174
+ }
175
+ return '';
176
+ },
177
+
178
+ /**
179
+ * Why this person may not change this document now (2.12) - a reason, or '' when
180
+ * they may. An extension that throws freezes it (fail closed while it is there).
181
+ */
182
+ frozenFor(doc, user) {
183
+ for (const ext of extensions.values()) {
184
+ if (!ext.freeze) continue;
185
+ try { const r = ext.freeze(doc, user); if (r) return String(r.reason || `${ext.label} keeps this document from being changed.`).slice(0, 300); } catch { return `${ext.label} keeps this document from being changed.`; }
186
+ }
187
+ return '';
188
+ },
189
+
190
+ /** A body as the outside world sees it (2.5): every extension's publicHtml in turn. */
191
+ publicHtml(html) {
192
+ let out = String(html ?? '');
193
+ for (const ext of reg.list()) {
194
+ if (!ext.publicHtml) continue;
195
+ try { const r = ext.publicHtml(out); if (typeof r === 'string') out = r; } catch { /* keep what we had */ }
196
+ }
197
+ return out;
198
+ },
199
+
200
+ /** Tell every extension that understands the document (2.13: a PDF document reaches only those declaring 'pdf'). Never throws and never waits. */
201
+ emit(event, ...args) {
202
+ const doc = args[0] && typeof args[0] === 'object' && 'format' in args[0] ? args[0] : null;
203
+ for (const ext of reg.list()) {
204
+ const fn = ext.on?.[event];
205
+ if (typeof fn !== 'function') continue;
206
+ if (doc && !reg.handles(ext, doc)) continue;
207
+ Promise.resolve().then(() => fn(...args)).catch(() => { /* an extension's problem */ });
208
+ }
209
+ }
210
+ };
211
+ globalThis[KEY] = reg;
212
+ return reg;
213
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Files into documents: Markdown, HTML and plain text.
3
+ *
4
+ * Markdown goes through the CMS's own `marked` (GitHub-flavoured, so tables
5
+ * and task lists come across). Whatever the source, the result is sanitised
6
+ * like any other document body before it is stored.
7
+ *
8
+ * @module docs/lib/importer
9
+ */
10
+
11
+ import {marked} from 'marked';
12
+ import {escapeHtml, parseHtml} from '../admin/lib/convert.js';
13
+ import {sanitizeDoc} from './store.js';
14
+
15
+ export const IMPORTABLE = ['md', 'markdown', 'html', 'htm', 'txt'];
16
+
17
+ /**
18
+ * @param {string} name the file name, for its extension and a fallback title
19
+ * @returns {string|null} 'md' | 'html' | 'txt', or null when not importable
20
+ */
21
+ export function kindOf(name) {
22
+ const ext = String(name || '').toLowerCase().split('.').pop();
23
+ if (ext === 'md' || ext === 'markdown') return 'md';
24
+ if (ext === 'html' || ext === 'htm') return 'html';
25
+ if (ext === 'txt') return 'txt';
26
+ return null;
27
+ }
28
+
29
+ /** GFM task list items come out of marked as disabled checkboxes; Docs has its own checklist. */
30
+ function checklists(html) {
31
+ return html.replace(/<ul>(\s*<li><input (?:checked="" )?disabled="" type="checkbox">[\s\S]*?)<\/ul>/g, (m, body) => {
32
+ const items = body.replace(/<li><input (checked="" )?disabled="" type="checkbox">\s*/g,
33
+ (x, on) => `<li data-checked="${on ? 'true' : 'false'}">`);
34
+ return `<ul data-type="checklist">${items}</ul>`;
35
+ });
36
+ }
37
+
38
+ /**
39
+ * Take the first top-level heading as the title when there is one and no
40
+ * better title was given; it comes out of the body so it is not doubled.
41
+ */
42
+ function splitTitle(html) {
43
+ const m = /^\s*<h1[^>]*>([\s\S]*?)<\/h1>\s*/i.exec(html);
44
+ if (!m) return {title: null, html};
45
+ const text = m[1].replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
46
+ return text ? {title: parseHtml(text).children.map(c => c.text || '').join(''), html: html.slice(m[0].length)} : {title: null, html};
47
+ }
48
+
49
+ /**
50
+ * @param {{name:string, text:string}} file
51
+ * @returns {{title:string, content:string}|null}
52
+ */
53
+ export function importFile({name, text}) {
54
+ const kind = kindOf(name);
55
+ if (!kind) return null;
56
+ const src = String(text ?? '').replace(/^/, '');
57
+ let html;
58
+ if (kind === 'md') {
59
+ // Front matter is metadata, not document.
60
+ const body = src.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, '');
61
+ html = checklists(marked.parse(body, {gfm: true, breaks: false, async: false}));
62
+ } else if (kind === 'html') {
63
+ const body = /<body[^>]*>([\s\S]*)<\/body>/i.exec(src);
64
+ html = body ? body[1] : src;
65
+ } else {
66
+ html = src.split(/\r?\n\s*\r?\n/).map(p => p.trim()).filter(Boolean)
67
+ .map(p => `<p>${escapeHtml(p).replace(/\r?\n/g, '<br>')}</p>`).join('');
68
+ }
69
+ const base = String(name || '').replace(/\.[^.]+$/, '').trim();
70
+ const split = splitTitle(html);
71
+ return {title: split.title || base || 'Imported document', content: sanitizeDoc(split.html)};
72
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The PDF text reader, in its own thread (lib/pdf.js starts and stops it).
3
+ *
4
+ * pdfjs-dist does the reading with scripting off and no fonts loaded - only
5
+ * the text is wanted. The parent gives it a time budget and ends it when
6
+ * that runs out, so a file built to stall never holds the server.
7
+ *
8
+ * @module docs/lib/pdf-worker
9
+ */
10
+
11
+ import {parentPort, workerData} from 'worker_threads';
12
+
13
+ async function run() {
14
+ const {data, maxPages, maxChars} = workerData;
15
+ const pdfjs = await import('pdfjs-dist/legacy/build/pdf.mjs');
16
+ const task = pdfjs.getDocument({
17
+ data: new Uint8Array(data),
18
+ isEvalSupported: false,
19
+ disableFontFace: true,
20
+ useSystemFonts: false,
21
+ verbosity: 0,
22
+ stopAtErrors: false
23
+ });
24
+ const doc = await task.promise;
25
+ const pages = doc.numPages;
26
+ const out = [];
27
+ let chars = 0;
28
+ for (let i = 1; i <= Math.min(pages, maxPages) && chars < maxChars; i++) {
29
+ let page;
30
+ try { page = await doc.getPage(i); } catch { continue; }
31
+ const content = await page.getTextContent({includeMarkedContent: false});
32
+ let line = '';
33
+ const lines = [];
34
+ for (const item of content.items) {
35
+ if (typeof item.str !== 'string') continue;
36
+ line += item.str;
37
+ if (item.hasEOL) { lines.push(line); line = ''; } else if (item.str && !/\s$/.test(item.str)) line += ' ';
38
+ }
39
+ if (line.trim()) lines.push(line);
40
+ const text = lines.map(l => l.replace(/\s+/g, ' ').trim()).filter(Boolean).join('\n');
41
+ if (text) { out.push(text); chars += text.length; }
42
+ page.cleanup();
43
+ }
44
+ await task.destroy();
45
+ parentPort.postMessage({pages, text: out.join('\n\n').slice(0, maxChars)});
46
+ }
47
+
48
+ run().catch((err) => parentPort.postMessage({pages: 0, text: null, error: String(err && err.message || err)}));