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.
- package/CHANGELOG.md +9 -0
- package/docs/DommaFeatures.md +13 -1
- package/package.json +1 -1
- package/plugins/_lib/admin/games/games.css +1 -1
- package/plugins/_lib/admin/games/kit.js +329 -124
- package/plugins/_lib/games/http.js +11 -1
- package/plugins/_lib/games/plays.js +28 -7
- package/plugins/crossword/CLAUDE.md +37 -0
- package/plugins/crossword/admin/css/index.css +18 -1
- package/plugins/crossword/admin/views/crossword.js +337 -205
- package/plugins/crossword/docs/guide.md +53 -0
- package/plugins/crossword/inject/body.html +1 -1
- package/plugins/crossword/plugin.js +38 -5
- package/plugins/crossword/plugin.json +4 -3
- package/plugins/crossword/public/crossword.css +36 -1
- package/plugins/crossword/public/crossword.js +468 -6
- package/plugins/docs/CLAUDE.md +318 -0
- package/plugins/docs/admin/css/docs.css +783 -0
- package/plugins/docs/admin/lib/api.js +340 -0
- package/plugins/docs/admin/lib/convert.js +482 -0
- package/plugins/docs/admin/lib/drafts.js +115 -0
- package/plugins/docs/admin/lib/editor.js +851 -0
- package/plugins/docs/admin/lib/find-replace.js +426 -0
- package/plugins/docs/admin/lib/folders.js +203 -0
- package/plugins/docs/admin/lib/panel-guard.js +73 -0
- package/plugins/docs/admin/lib/pdf-view.js +126 -0
- package/plugins/docs/admin/lib/send.js +156 -0
- package/plugins/docs/admin/lib/settings.js +142 -0
- package/plugins/docs/admin/lib/share.js +323 -0
- package/plugins/docs/admin/lib/shortcuts.js +44 -0
- package/plugins/docs/admin/lib/templates.js +194 -0
- package/plugins/docs/admin/lib/versions.js +192 -0
- package/plugins/docs/admin/templates/docs.html +92 -0
- package/plugins/docs/admin/templates/editor.html +86 -0
- package/plugins/docs/admin/views/docs.js +777 -0
- package/plugins/docs/admin/views/editor.js +978 -0
- package/plugins/docs/config.js +24 -0
- package/plugins/docs/docs/guide.md +270 -0
- package/plugins/docs/lib/extensions.js +213 -0
- package/plugins/docs/lib/importer.js +72 -0
- package/plugins/docs/lib/pdf-worker.js +48 -0
- package/plugins/docs/lib/pdf.js +222 -0
- package/plugins/docs/lib/store.js +748 -0
- package/plugins/docs/plugin.js +1108 -0
- package/plugins/docs/plugin.json +89 -0
- package/plugins/docs/plugin.public.js +114 -0
- package/plugins/docs/templates/public-doc.html +36 -0
- package/plugins/docs/tests/_env.js +10 -0
- package/plugins/docs/tests/_pdf.js +73 -0
- package/plugins/docs/tests/convert.test.js +95 -0
- package/plugins/docs/tests/drafts.test.js +82 -0
- package/plugins/docs/tests/pdf.test.js +86 -0
- package/plugins/docs/tests/plugin.test.js +816 -0
- package/plugins/docs/tests/shortcuts.test.js +30 -0
- package/plugins/free-tier.lock.json +147 -1
- package/plugins/garage/CLAUDE.md +51 -0
- package/plugins/garage/admin/css/garage.css +73 -0
- package/plugins/garage/admin/lib/garage.js +236 -0
- package/plugins/garage/admin/templates/garage.html +86 -0
- package/plugins/garage/admin/views/garage.js +581 -0
- package/plugins/garage/collections/garage-vehicles/schema.json +101 -0
- package/plugins/garage/config.js +18 -0
- package/plugins/garage/docs/guide.md +118 -0
- package/plugins/garage/plugin.js +471 -0
- package/plugins/garage/plugin.json +68 -0
- package/plugins/garage/tests/access.test.js +134 -0
- package/plugins/garage/tests/garage.test.js +106 -0
- package/plugins/messages/CLAUDE.md +230 -0
- package/plugins/messages/admin/css/messages.css +117 -0
- package/plugins/messages/admin/lib/messages.js +452 -0
- package/plugins/messages/admin/templates/messages.html +76 -0
- package/plugins/messages/admin/views/messages.js +1219 -0
- package/plugins/messages/collections/user-message-threads/schema.json +22 -0
- package/plugins/messages/collections/user-messages/schema.json +23 -0
- package/plugins/messages/config.js +14 -0
- package/plugins/messages/docs/guide.md +176 -0
- package/plugins/messages/lib/call.js +138 -0
- package/plugins/messages/plugin.js +994 -0
- package/plugins/messages/plugin.json +59 -0
- package/plugins/messages/plugin.public.js +52 -0
- package/plugins/messages/templates/call.html +6 -0
- package/plugins/messages/tests/after-send.test.js +22 -0
- package/plugins/messages/tests/api.test.js +187 -0
- package/plugins/messages/tests/call.test.js +497 -0
- package/plugins/messages/tests/drafts.test.js +25 -0
- package/plugins/messages/tests/messages.test.js +167 -0
- package/plugins/quiz/CLAUDE.md +43 -0
- package/plugins/quiz/admin/css/index.css +13 -1
- package/plugins/quiz/admin/views/quiz.js +423 -245
- package/plugins/quiz/docs/guide.md +61 -0
- package/plugins/quiz/inject/body.html +1 -1
- package/plugins/quiz/plugin.js +39 -5
- package/plugins/quiz/plugin.json +4 -3
- package/plugins/quiz/public/quiz.css +36 -1
- package/plugins/quiz/public/quiz.js +298 -6
- package/plugins/theme-roller/CLAUDE.md +65 -0
- package/plugins/theme-roller/admin/css/theme-roller.css +43 -0
- package/plugins/theme-roller/admin/lib/prompt.js +22 -0
- package/plugins/theme-roller/admin/lib/themes.js +196 -0
- package/plugins/theme-roller/admin/lib/view.js +88 -0
- package/plugins/theme-roller/admin/templates/theme-roller.html +73 -0
- package/plugins/theme-roller/admin/views/theme-roller-view.js +639 -0
- package/plugins/theme-roller/config.js +1 -0
- package/plugins/theme-roller/docs/guide.md +104 -0
- package/plugins/theme-roller/plugin.js +263 -0
- package/plugins/theme-roller/plugin.json +42 -0
- package/plugins/theme-roller/tests/api.test.js +161 -0
- package/plugins/theme-roller/tests/prompt-values.test.js +22 -0
- package/plugins/theme-roller/tests/themes.test.js +77 -0
- 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)}));
|