nexus-shared 3.2.0 → 4.1.0
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 +2359 -0
- package/README.md +1911 -1123
- package/dist/Client.Index.d.ts +27 -0
- package/dist/Client.Index.js +27 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +24 -3
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +628 -247
- package/dist/Components/Forms/CrudActions.d.ts +137 -0
- package/dist/Components/Forms/CrudActions.js +214 -0
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +37 -2
- package/dist/Components/Forms/CrudParts.js +123 -7
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +174 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
- package/dist/Components/Forms/CrudWorkspace.js +177 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +6 -5
- package/dist/Components/Forms/Form.js +134 -21
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +182 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -6
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +12 -12
- package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
- package/dist/Components/Inputs/InputRenderer.js +37 -32
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +75 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +15 -2
- package/dist/Components/Inputs/RowsInput.js +96 -8
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +45 -32
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +11 -2
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TreeView.d.ts +76 -2
- package/dist/Components/Inputs/TreeView.js +227 -16
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +34 -7
- package/dist/Components/Layouts/Popup.js +143 -13
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +153 -49
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
- package/dist/Components/Viewers/DataTableParts.js +26 -14
- package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
- package/dist/Components/Viewers/OwnerFiles.js +700 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudBackend.d.ts +20 -2
- package/dist/Helpers/CrudBackend.js +46 -2
- package/dist/Helpers/CrudColumns.d.ts +91 -6
- package/dist/Helpers/CrudColumns.js +148 -7
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +116 -2
- package/dist/Helpers/CrudHelpers.js +256 -0
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +32 -0
- package/dist/Helpers/CrudRowForms.js +76 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +158 -0
- package/dist/Helpers/FileAdapter.js +173 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +100 -5
- package/dist/Helpers/FormStore.js +368 -25
- package/dist/Helpers/InputConditions.d.ts +56 -0
- package/dist/Helpers/InputConditions.js +192 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +395 -0
- package/dist/Helpers/InputNarrowing.d.ts +50 -0
- package/dist/Helpers/InputNarrowing.js +148 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +157 -74
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
- package/dist/Helpers/NexusDriveAdapter.js +236 -0
- package/dist/Helpers/NumberHelpers.d.ts +156 -0
- package/dist/Helpers/NumberHelpers.js +242 -0
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +14 -2
- package/dist/Helpers/RequestCache.js +22 -3
- package/dist/Helpers/RowsStore.d.ts +61 -2
- package/dist/Helpers/RowsStore.js +203 -16
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +9 -3
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +38 -4
- package/dist/Helpers/TableHelpers.js +78 -10
- package/dist/Helpers/TableJoins.d.ts +13 -0
- package/dist/Helpers/TableJoins.js +34 -0
- package/dist/Helpers/TableStore.d.ts +28 -4
- package/dist/Helpers/TableStore.js +73 -11
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +156 -6
- package/dist/Helpers/TreeStore.js +547 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +183 -0
- package/dist/Interfaces/FileInterfaces.js +6 -0
- package/dist/Interfaces/FormInterfaces.d.ts +460 -32
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +22 -3
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +102 -3
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +79 -6
- package/dist/Services/JoinSources.js +132 -21
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Shared.Index.d.ts +28 -0
- package/dist/Shared.Index.js +28 -0
- package/package.json +13 -2
- package/src/Styles/Nexus.Avatar.css +6 -5
- package/src/Styles/Nexus.Button.css +39 -4
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Choice.css +16 -0
- package/src/Styles/Nexus.Crud.css +486 -21
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Dropdown.css +8 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +437 -0
- package/src/Styles/Nexus.Form.css +5 -2
- package/src/Styles/Nexus.Index.css +7 -0
- package/src/Styles/Nexus.Input.css +73 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Menu.css +10 -1
- package/src/Styles/Nexus.Popup.css +135 -1
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Tab.Buttons.css +87 -22
- package/src/Styles/Nexus.Table.css +1458 -1387
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Themes.css +12 -6
- package/src/Styles/Nexus.Tokens.css +8 -3
- package/src/Styles/Nexus.Tree.css +41 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* A QR code encoder, QR Code Model 2 as ISO/IEC 18004 describes it, written by hand so that no package is needed: an
|
|
3
|
+
* authenticator app's otpauth:// address for a phone to scan, a link, any text. `QrCodeImage` draws it. Byte mode only:
|
|
4
|
+
* the text goes in as UTF-8, which is what readers take byte mode to be. Versions 1 to 40 (21 to 177 modules a side) at
|
|
5
|
+
* the four error correction levels; L, M, Q and H restore about 7%, 15%, 25% and 30% of a damaged code.
|
|
6
|
+
*
|
|
7
|
+
* The steps are the standard's own:
|
|
8
|
+
* 1. the data codewords: the mode, the byte count, the bytes, a terminator and pad codewords, in the smallest version
|
|
9
|
+
* that holds them at the level asked;
|
|
10
|
+
* 2. the error correction: the data cut into the version's blocks, each followed by its Reed-Solomon codewords, then
|
|
11
|
+
* interleaved one codeword from each block at a time;
|
|
12
|
+
* 3. the matrix: the function patterns (the finders and their separators, the timing and alignment patterns, the dark
|
|
13
|
+
* module, the format and version information), then the codewords' bits in a zigzag around them;
|
|
14
|
+
* 4. the mask: all eight scored by the four penalty rules, and the lowest kept.
|
|
15
|
+
*
|
|
16
|
+
* Plain TypeScript that Node runs as it is (no enum, namespace or parameter properties), so that
|
|
17
|
+
* Project-Skills/Checks/qr-code-check.mjs can import this file and read every code back with an independent decoder
|
|
18
|
+
* (jsQR, a test-only oracle installed outside the repository: the check's header says how).
|
|
19
|
+
*/
|
|
20
|
+
/** The level's two bits in the format information. */
|
|
21
|
+
const LEVEL_BITS = { L: 1, M: 0, Q: 3, H: 2 };
|
|
22
|
+
/** Error correction codewords in each block, for versions 1 to 40 (ISO/IEC 18004 Table 9). */
|
|
23
|
+
const ECC_PER_BLOCK = {
|
|
24
|
+
L: [7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
25
|
+
M: [10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
26
|
+
Q: [13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
27
|
+
H: [17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
28
|
+
};
|
|
29
|
+
/** The blocks a version's codewords are cut into, for versions 1 to 40 (Table 9). */
|
|
30
|
+
const BLOCKS = {
|
|
31
|
+
L: [1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
32
|
+
M: [1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
33
|
+
Q: [1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
34
|
+
H: [1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
|
|
35
|
+
};
|
|
36
|
+
/** The rows (and the same columns) of the alignment patterns' centres, for versions 1 to 40 (Annex E). */
|
|
37
|
+
const ALIGNMENT_CENTRES = [
|
|
38
|
+
[], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34],
|
|
39
|
+
[6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50], [6, 30, 54], [6, 32, 58], [6, 34, 62],
|
|
40
|
+
[6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90],
|
|
41
|
+
[6, 28, 50, 72, 94], [6, 26, 50, 74, 98], [6, 30, 54, 78, 102], [6, 28, 54, 80, 106], [6, 32, 58, 84, 110],
|
|
42
|
+
[6, 30, 58, 86, 114], [6, 34, 62, 90, 118],
|
|
43
|
+
[6, 26, 50, 74, 98, 122], [6, 30, 54, 78, 102, 126], [6, 26, 52, 78, 104, 130], [6, 30, 56, 82, 108, 134],
|
|
44
|
+
[6, 34, 60, 86, 112, 138], [6, 30, 58, 86, 114, 142], [6, 34, 62, 90, 118, 146],
|
|
45
|
+
[6, 30, 54, 78, 102, 126, 150], [6, 24, 50, 76, 102, 128, 154], [6, 28, 54, 80, 106, 132, 158],
|
|
46
|
+
[6, 32, 58, 84, 110, 136, 162], [6, 26, 54, 82, 110, 138, 166], [6, 30, 58, 86, 114, 142, 170],
|
|
47
|
+
];
|
|
48
|
+
/** Dark, light, dark, dark, dark, light, dark: a finder pattern's row, which penalty rule 3 looks for. */
|
|
49
|
+
const FINDER_LIKE = [true, false, true, true, true, false, true];
|
|
50
|
+
/**
|
|
51
|
+
* The text as a QR code at the level asked (M unless told), in the smallest version that holds it. Throws when the
|
|
52
|
+
* text is too long even for version 40: 2,953 bytes of UTF-8 at L, 2,331 at M, 1,663 at Q, 1,273 at H.
|
|
53
|
+
*/
|
|
54
|
+
export function encodeQr(text, level = "M") {
|
|
55
|
+
const bytes = new TextEncoder().encode(text);
|
|
56
|
+
const version = smallestVersion(bytes.length, level);
|
|
57
|
+
const codewords = interleave(dataCodewords(bytes, version, level), version, level);
|
|
58
|
+
const grid = functionPatterns(version);
|
|
59
|
+
placeCodewords(grid, codewords);
|
|
60
|
+
// The mask that leaves a reader the fewest look-alike patterns: each is scored on the finished code, its own format
|
|
61
|
+
// information included, and the first lowest wins.
|
|
62
|
+
let mask = 0;
|
|
63
|
+
let code = withMask(grid, level, 0);
|
|
64
|
+
let lowest = penalty(code);
|
|
65
|
+
for (let candidate = 1; candidate < 8; candidate++) {
|
|
66
|
+
const masked = withMask(grid, level, candidate);
|
|
67
|
+
const points = penalty(masked);
|
|
68
|
+
if (points < lowest) {
|
|
69
|
+
mask = candidate;
|
|
70
|
+
code = masked;
|
|
71
|
+
lowest = points;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const { size, dark } = code;
|
|
75
|
+
return {
|
|
76
|
+
version,
|
|
77
|
+
size,
|
|
78
|
+
level,
|
|
79
|
+
mask,
|
|
80
|
+
isDark: (x, y) => x >= 0 && y >= 0 && x < size && y < size && dark[y * size + x] === true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The code as one SVG path in a square viewBox, with `border` light modules around it (4, the quiet zone the standard
|
|
85
|
+
* asks for). Each run of dark modules along a row is one rectangle.
|
|
86
|
+
*/
|
|
87
|
+
export function qrPath(code, border = 4) {
|
|
88
|
+
const runs = [];
|
|
89
|
+
for (let y = 0; y < code.size; y++) {
|
|
90
|
+
let x = 0;
|
|
91
|
+
while (x < code.size) {
|
|
92
|
+
if (!code.isDark(x, y)) {
|
|
93
|
+
x++;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const start = x;
|
|
97
|
+
while (x < code.size && code.isDark(x, y))
|
|
98
|
+
x++;
|
|
99
|
+
runs.push(`M${start + border} ${y + border}h${x - start}v1h-${x - start}z`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const side = code.size + border * 2;
|
|
103
|
+
return { viewBox: `0 0 ${side} ${side}`, d: runs.join("") };
|
|
104
|
+
}
|
|
105
|
+
/** The smallest version whose data codewords hold the mode, the byte count and the bytes at this level. */
|
|
106
|
+
function smallestVersion(byteCount, level) {
|
|
107
|
+
for (let version = 1; version <= 40; version++) {
|
|
108
|
+
if (4 + countBits(version) + byteCount * 8 <= dataCodewordCount(version, level) * 8)
|
|
109
|
+
return version;
|
|
110
|
+
}
|
|
111
|
+
const most = Math.floor((dataCodewordCount(40, level) * 8 - 4 - countBits(40)) / 8);
|
|
112
|
+
throw new Error(`The text is ${byteCount} bytes, more than a QR code holds at level ${level} (${most} bytes).`);
|
|
113
|
+
}
|
|
114
|
+
/** The bits of the byte count after the mode: 8 up to version 9, 16 from version 10. */
|
|
115
|
+
function countBits(version) {
|
|
116
|
+
return version <= 9 ? 8 : 16;
|
|
117
|
+
}
|
|
118
|
+
/** A version's error correction at a level: how many blocks, and how many check codewords in each. */
|
|
119
|
+
function blockLayout(version, level) {
|
|
120
|
+
const blocks = BLOCKS[level][version - 1];
|
|
121
|
+
const eccPerBlock = ECC_PER_BLOCK[level][version - 1];
|
|
122
|
+
if (blocks === undefined || eccPerBlock === undefined)
|
|
123
|
+
throw new Error(`There is no QR code version ${version}.`);
|
|
124
|
+
return { blocks, eccPerBlock };
|
|
125
|
+
}
|
|
126
|
+
function alignmentCentres(version) {
|
|
127
|
+
return ALIGNMENT_CENTRES[version - 1] ?? [];
|
|
128
|
+
}
|
|
129
|
+
/** All the codewords of a version: the modules its function patterns leave free, in whole bytes. */
|
|
130
|
+
function totalCodewords(version) {
|
|
131
|
+
const size = version * 4 + 17;
|
|
132
|
+
const across = alignmentCentres(version).length;
|
|
133
|
+
let modules = size * size;
|
|
134
|
+
modules -= 3 * 8 * 8; // the three finders, each with its separator
|
|
135
|
+
modules -= 2 * 15 + 1; // the format information twice, and the dark module
|
|
136
|
+
modules -= 2 * (size - 16); // the timing patterns between the separators
|
|
137
|
+
if (across > 0) {
|
|
138
|
+
// Every alignment pattern but the three where the finders are; those on a timing pattern share 5 modules with it.
|
|
139
|
+
modules -= (across * across - 3) * 25 - 2 * (across - 2) * 5;
|
|
140
|
+
}
|
|
141
|
+
if (version >= 7)
|
|
142
|
+
modules -= 2 * 18; // the version information twice
|
|
143
|
+
return Math.floor(modules / 8);
|
|
144
|
+
}
|
|
145
|
+
function dataCodewordCount(version, level) {
|
|
146
|
+
const { blocks, eccPerBlock } = blockLayout(version, level);
|
|
147
|
+
return totalCodewords(version) - blocks * eccPerBlock;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The data codewords: the byte mode indicator 0100, the byte count, the bytes and the terminator 0000, then the pad
|
|
151
|
+
* codewords 11101100 and 00010001 by turns until the version is full. The mode and the count leave the bytes 4 bits
|
|
152
|
+
* into a codeword, so the terminator always fits, and always completes the last one.
|
|
153
|
+
*/
|
|
154
|
+
function dataCodewords(bytes, version, level) {
|
|
155
|
+
const bits = [];
|
|
156
|
+
const write = (value, length) => {
|
|
157
|
+
for (let i = length - 1; i >= 0; i--)
|
|
158
|
+
bits.push((value >> i) & 1);
|
|
159
|
+
};
|
|
160
|
+
write(0b0100, 4);
|
|
161
|
+
write(bytes.length, countBits(version));
|
|
162
|
+
for (const byte of bytes)
|
|
163
|
+
write(byte, 8);
|
|
164
|
+
write(0b0000, 4);
|
|
165
|
+
const codewords = [];
|
|
166
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
167
|
+
codewords.push(bits.slice(i, i + 8).reduce((byte, bit) => (byte << 1) | bit, 0));
|
|
168
|
+
}
|
|
169
|
+
const capacity = dataCodewordCount(version, level);
|
|
170
|
+
for (let pad = 0; codewords.length < capacity; pad++)
|
|
171
|
+
codewords.push(pad % 2 === 0 ? 0b11101100 : 0b00010001);
|
|
172
|
+
return codewords;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* The data cut into the version's blocks, each followed by its check codewords, then interleaved: the first codeword of
|
|
176
|
+
* every block, then the second of every block, and so on, all the data before the check codewords. When the codewords
|
|
177
|
+
* do not share out evenly, the last blocks take one data codeword more.
|
|
178
|
+
*/
|
|
179
|
+
function interleave(data, version, level) {
|
|
180
|
+
const { blocks, eccPerBlock } = blockLayout(version, level);
|
|
181
|
+
const total = totalCodewords(version);
|
|
182
|
+
const shortBlocks = blocks - (total % blocks);
|
|
183
|
+
const shortLength = Math.floor(total / blocks) - eccPerBlock;
|
|
184
|
+
const generator = generatorPolynomial(eccPerBlock);
|
|
185
|
+
const dataBlocks = [];
|
|
186
|
+
const eccBlocks = [];
|
|
187
|
+
let start = 0;
|
|
188
|
+
for (let i = 0; i < blocks; i++) {
|
|
189
|
+
const length = i < shortBlocks ? shortLength : shortLength + 1;
|
|
190
|
+
const block = data.slice(start, start + length);
|
|
191
|
+
dataBlocks.push(block);
|
|
192
|
+
eccBlocks.push(reedSolomon(block, generator));
|
|
193
|
+
start += length;
|
|
194
|
+
}
|
|
195
|
+
const result = [];
|
|
196
|
+
for (const group of [dataBlocks, eccBlocks]) {
|
|
197
|
+
const longest = Math.max(...group.map((block) => block.length));
|
|
198
|
+
for (let i = 0; i < longest; i++) {
|
|
199
|
+
for (const block of group) {
|
|
200
|
+
const codeword = block[i];
|
|
201
|
+
if (codeword !== undefined)
|
|
202
|
+
result.push(codeword); // a short block has no last data codeword
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
/** Multiplies two elements of GF(256), the field of the codewords, reduced by x^8 + x^4 + x^3 + x^2 + 1 (0x11D). */
|
|
209
|
+
function gfMultiply(a, b) {
|
|
210
|
+
let product = 0;
|
|
211
|
+
let shifted = a;
|
|
212
|
+
for (let rest = b; rest > 0; rest >>= 1) {
|
|
213
|
+
if (rest & 1)
|
|
214
|
+
product ^= shifted;
|
|
215
|
+
shifted <<= 1;
|
|
216
|
+
if (shifted & 0x100)
|
|
217
|
+
shifted ^= 0x11d;
|
|
218
|
+
}
|
|
219
|
+
return product;
|
|
220
|
+
}
|
|
221
|
+
/** (x - 1)(x - 2)(x - 2^2)...(x - 2^(degree - 1)): the coefficients, highest power first, without the leading 1. */
|
|
222
|
+
function generatorPolynomial(degree) {
|
|
223
|
+
let poly = [1];
|
|
224
|
+
let root = 1;
|
|
225
|
+
for (let i = 0; i < degree; i++) {
|
|
226
|
+
// Times (x + root), since minus is plus in GF(256): each coefficient moves one power up, plus root times the next.
|
|
227
|
+
poly = [...poly, 0].map((coefficient, j) => coefficient ^ gfMultiply(poly[j - 1] ?? 0, root));
|
|
228
|
+
root = gfMultiply(root, 2);
|
|
229
|
+
}
|
|
230
|
+
return poly.slice(1);
|
|
231
|
+
}
|
|
232
|
+
/** A block's check codewords: what is left of its data, times x^degree, divided by the generator. */
|
|
233
|
+
function reedSolomon(data, generator) {
|
|
234
|
+
let rest = generator.map(() => 0);
|
|
235
|
+
for (const codeword of data) {
|
|
236
|
+
const factor = codeword ^ (rest[0] ?? 0);
|
|
237
|
+
rest = generator.map((coefficient, i) => (rest[i + 1] ?? 0) ^ gfMultiply(coefficient, factor));
|
|
238
|
+
}
|
|
239
|
+
return rest;
|
|
240
|
+
}
|
|
241
|
+
/** The check bits of a BCH code: the remainder of value x 2^count divided by the generator, in GF(2). */
|
|
242
|
+
function bchCheckBits(value, generator, count) {
|
|
243
|
+
let rest = value << count;
|
|
244
|
+
for (let bit = 31 - Math.clz32(rest); bit >= count; bit--) {
|
|
245
|
+
if ((rest >> bit) & 1)
|
|
246
|
+
rest ^= generator << (bit - count);
|
|
247
|
+
}
|
|
248
|
+
return rest;
|
|
249
|
+
}
|
|
250
|
+
/** Draws a function module: the codewords and the mask leave it alone. */
|
|
251
|
+
function setFixed(grid, x, y, dark) {
|
|
252
|
+
grid.dark[y * grid.size + x] = dark;
|
|
253
|
+
grid.fixed[y * grid.size + x] = true;
|
|
254
|
+
}
|
|
255
|
+
/** A version's function patterns, every other module light and free for the codewords. */
|
|
256
|
+
function functionPatterns(version) {
|
|
257
|
+
const size = version * 4 + 17;
|
|
258
|
+
const grid = { size, dark: new Array(size * size).fill(false), fixed: new Array(size * size).fill(false) };
|
|
259
|
+
// The timing patterns along row 6 and column 6, dark on the even modules; the finders cover their ends.
|
|
260
|
+
for (let i = 0; i < size; i++) {
|
|
261
|
+
setFixed(grid, 6, i, i % 2 === 0);
|
|
262
|
+
setFixed(grid, i, 6, i % 2 === 0);
|
|
263
|
+
}
|
|
264
|
+
drawFinder(grid, 3, 3);
|
|
265
|
+
drawFinder(grid, size - 4, 3);
|
|
266
|
+
drawFinder(grid, 3, size - 4);
|
|
267
|
+
// An alignment pattern at every pair of centres, but the three corners where the finders are.
|
|
268
|
+
const centres = alignmentCentres(version);
|
|
269
|
+
const last = centres.length - 1;
|
|
270
|
+
centres.forEach((y, row) => {
|
|
271
|
+
centres.forEach((x, column) => {
|
|
272
|
+
const onFinder = (row === 0 && column === 0) || (row === 0 && column === last) || (row === last && column === 0);
|
|
273
|
+
if (!onFinder)
|
|
274
|
+
drawAlignment(grid, x, y);
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
// Room for the format information, drawn again with each mask, the dark module, and the version information.
|
|
278
|
+
drawFormat(grid, "M", 0);
|
|
279
|
+
drawVersion(grid, version);
|
|
280
|
+
return grid;
|
|
281
|
+
}
|
|
282
|
+
/** A finder pattern centred on (x, y): 7 x 7, with its light separator around it, cut off at the code's edge. */
|
|
283
|
+
function drawFinder(grid, x, y) {
|
|
284
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
285
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
286
|
+
const column = x + dx;
|
|
287
|
+
const row = y + dy;
|
|
288
|
+
if (column < 0 || row < 0 || column >= grid.size || row >= grid.size)
|
|
289
|
+
continue;
|
|
290
|
+
// Rings out from the centre: 0 and 1 the dark 3 x 3, 2 light, 3 the dark frame, 4 the separator.
|
|
291
|
+
const ring = Math.max(Math.abs(dx), Math.abs(dy));
|
|
292
|
+
setFixed(grid, column, row, ring !== 2 && ring !== 4);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** An alignment pattern centred on (x, y): 5 x 5, a dark frame, a light ring and a dark centre. */
|
|
297
|
+
function drawAlignment(grid, x, y) {
|
|
298
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
299
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
300
|
+
setFixed(grid, x + dx, y + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* The format information: the level's two bits and the mask's three, ten BCH check bits after them, all XORed with
|
|
306
|
+
* 101010000010010 so that it is never all light. One copy around the top-left finder, the other split between the two
|
|
307
|
+
* other finders; and the dark module, always dark.
|
|
308
|
+
*/
|
|
309
|
+
function drawFormat(grid, level, mask) {
|
|
310
|
+
const data = (LEVEL_BITS[level] << 3) | mask;
|
|
311
|
+
const bits = ((data << 10) | bchCheckBits(data, 0b10100110111, 10)) ^ 0b101010000010010;
|
|
312
|
+
const bit = (i) => ((bits >> i) & 1) === 1;
|
|
313
|
+
const { size } = grid;
|
|
314
|
+
// Bits 0 to 7 down column 8 beside the top-left finder, stepping over the timing row; 8 to 14 leftwards along row 8.
|
|
315
|
+
for (let i = 0; i <= 5; i++)
|
|
316
|
+
setFixed(grid, 8, i, bit(i));
|
|
317
|
+
setFixed(grid, 8, 7, bit(6));
|
|
318
|
+
setFixed(grid, 8, 8, bit(7));
|
|
319
|
+
setFixed(grid, 7, 8, bit(8));
|
|
320
|
+
for (let i = 9; i < 15; i++)
|
|
321
|
+
setFixed(grid, 14 - i, 8, bit(i));
|
|
322
|
+
// The copy: bits 0 to 7 leftwards along row 8 from the right edge, 8 to 14 down column 8 to the bottom edge.
|
|
323
|
+
for (let i = 0; i < 8; i++)
|
|
324
|
+
setFixed(grid, size - 1 - i, 8, bit(i));
|
|
325
|
+
for (let i = 8; i < 15; i++)
|
|
326
|
+
setFixed(grid, 8, size - 15 + i, bit(i));
|
|
327
|
+
setFixed(grid, 8, size - 8, true);
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* From version 7, the version in 6 bits and 12 BCH check bits after them: a block 3 wide and 6 tall left of the
|
|
331
|
+
* top-right finder, and the same bits mirrored, 6 wide and 3 tall, over the bottom-left finder.
|
|
332
|
+
*/
|
|
333
|
+
function drawVersion(grid, version) {
|
|
334
|
+
if (version < 7)
|
|
335
|
+
return;
|
|
336
|
+
const bits = (version << 12) | bchCheckBits(version, 0b1111100100101, 12);
|
|
337
|
+
for (let i = 0; i < 18; i++) {
|
|
338
|
+
const dark = ((bits >> i) & 1) === 1;
|
|
339
|
+
const column = grid.size - 11 + (i % 3);
|
|
340
|
+
const row = Math.floor(i / 3);
|
|
341
|
+
setFixed(grid, column, row, dark);
|
|
342
|
+
setFixed(grid, row, column, dark);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* The codewords' bits, the most significant first, in the zigzag the standard reads them in: two columns at a time
|
|
347
|
+
* from the right edge, up the first pair and down the next, the right column before the left, around every function
|
|
348
|
+
* module. What is left after the last codeword (0 to 7 remainder bits) stays light.
|
|
349
|
+
*/
|
|
350
|
+
function placeCodewords(grid, codewords) {
|
|
351
|
+
const { size } = grid;
|
|
352
|
+
let bit = 0;
|
|
353
|
+
let upward = true;
|
|
354
|
+
for (let right = size - 1; right >= 1; right -= 2) {
|
|
355
|
+
if (right === 6)
|
|
356
|
+
right = 5; // column 6 is the vertical timing pattern: the pairs step over it
|
|
357
|
+
for (let step = 0; step < size; step++) {
|
|
358
|
+
const y = upward ? size - 1 - step : step;
|
|
359
|
+
for (const x of [right, right - 1]) {
|
|
360
|
+
const at = y * size + x;
|
|
361
|
+
if (grid.fixed[at])
|
|
362
|
+
continue;
|
|
363
|
+
const codeword = codewords[bit >> 3];
|
|
364
|
+
grid.dark[at] = codeword !== undefined && ((codeword >> (7 - (bit & 7))) & 1) === 1;
|
|
365
|
+
bit++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
upward = !upward;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** A copy of the code with mask pattern `mask` applied to every module the function patterns leave free, and its format information. */
|
|
372
|
+
function withMask(grid, level, mask) {
|
|
373
|
+
const masked = { size: grid.size, dark: grid.dark.slice(), fixed: grid.fixed };
|
|
374
|
+
for (let y = 0; y < grid.size; y++) {
|
|
375
|
+
for (let x = 0; x < grid.size; x++) {
|
|
376
|
+
const at = y * grid.size + x;
|
|
377
|
+
if (!grid.fixed[at] && maskFlips(mask, x, y))
|
|
378
|
+
masked.dark[at] = !masked.dark[at];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
drawFormat(masked, level, mask);
|
|
382
|
+
return masked;
|
|
383
|
+
}
|
|
384
|
+
/** Whether mask pattern `mask` flips the module at column x, row y (Table 10, where i is the row and j the column). */
|
|
385
|
+
function maskFlips(mask, x, y) {
|
|
386
|
+
switch (mask) {
|
|
387
|
+
case 0:
|
|
388
|
+
return (y + x) % 2 === 0;
|
|
389
|
+
case 1:
|
|
390
|
+
return y % 2 === 0;
|
|
391
|
+
case 2:
|
|
392
|
+
return x % 3 === 0;
|
|
393
|
+
case 3:
|
|
394
|
+
return (y + x) % 3 === 0;
|
|
395
|
+
case 4:
|
|
396
|
+
return (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0;
|
|
397
|
+
case 5:
|
|
398
|
+
return ((y * x) % 2) + ((y * x) % 3) === 0;
|
|
399
|
+
case 6:
|
|
400
|
+
return (((y * x) % 2) + ((y * x) % 3)) % 2 === 0;
|
|
401
|
+
case 7:
|
|
402
|
+
return (((y + x) % 2) + ((y * x) % 3)) % 2 === 0;
|
|
403
|
+
default:
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/** A masked code's penalty points by the four rules of ISO/IEC 18004 7.8.3: the fewer, the easier it reads. */
|
|
408
|
+
function penalty(grid) {
|
|
409
|
+
const { size, dark } = grid;
|
|
410
|
+
let points = 0;
|
|
411
|
+
// Rules 1 and 3, along every row and every column.
|
|
412
|
+
for (let i = 0; i < size; i++) {
|
|
413
|
+
const row = [];
|
|
414
|
+
const column = [];
|
|
415
|
+
for (let j = 0; j < size; j++) {
|
|
416
|
+
row.push(dark[i * size + j] === true);
|
|
417
|
+
column.push(dark[j * size + i] === true);
|
|
418
|
+
}
|
|
419
|
+
points += runPoints(row) + finderPoints(row) + runPoints(column) + finderPoints(column);
|
|
420
|
+
}
|
|
421
|
+
// Rule 2: 3 points for every 2 x 2 block of one colour, overlapping blocks each counted.
|
|
422
|
+
for (let y = 0; y + 1 < size; y++) {
|
|
423
|
+
for (let x = 0; x + 1 < size; x++) {
|
|
424
|
+
const at = y * size + x;
|
|
425
|
+
const colour = dark[at];
|
|
426
|
+
if (dark[at + 1] === colour && dark[at + size] === colour && dark[at + size + 1] === colour)
|
|
427
|
+
points += 3;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
// Rule 4: 10 points for every whole 5% that the share of dark modules is away from half.
|
|
431
|
+
const darkCount = dark.filter(Boolean).length;
|
|
432
|
+
points += 10 * Math.floor(Math.abs(darkCount * 20 - size * size * 10) / (size * size));
|
|
433
|
+
return points;
|
|
434
|
+
}
|
|
435
|
+
/** Rule 1: a run of five or more modules of one colour scores 3, and 1 more for every module past the fifth. */
|
|
436
|
+
function runPoints(line) {
|
|
437
|
+
let points = 0;
|
|
438
|
+
let run = 0;
|
|
439
|
+
for (let i = 0; i < line.length; i++) {
|
|
440
|
+
run = i > 0 && line[i] === line[i - 1] ? run + 1 : 1;
|
|
441
|
+
if (run === 5)
|
|
442
|
+
points += 3;
|
|
443
|
+
else if (run > 5)
|
|
444
|
+
points += 1;
|
|
445
|
+
}
|
|
446
|
+
return points;
|
|
447
|
+
}
|
|
448
|
+
/** Rule 3: 40 points for a finder-like run with four light modules before it, and 40 with four after it. */
|
|
449
|
+
function finderPoints(line) {
|
|
450
|
+
// Past either end is the quiet zone, which is light.
|
|
451
|
+
const light = (i) => line[i] !== true;
|
|
452
|
+
const fourLight = (from) => light(from) && light(from + 1) && light(from + 2) && light(from + 3);
|
|
453
|
+
let points = 0;
|
|
454
|
+
for (let start = 0; start + FINDER_LIKE.length <= line.length; start++) {
|
|
455
|
+
if (!FINDER_LIKE.every((dark, j) => line[start + j] === dark))
|
|
456
|
+
continue;
|
|
457
|
+
if (fourLight(start - 4))
|
|
458
|
+
points += 40;
|
|
459
|
+
if (fourLight(start + FINDER_LIKE.length))
|
|
460
|
+
points += 40;
|
|
461
|
+
}
|
|
462
|
+
return points;
|
|
463
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { ApiEndpoint, ApiResult } from "../Interfaces/ApiInterfaces.ts";
|
|
2
|
+
import type { DatePreferences } from "../Interfaces/DateInterfaces.ts";
|
|
3
|
+
import { type FormValues, type InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
4
|
+
import type { RecordCardParams, RecordContentCardParams, RecordContext, RecordFieldParams, RecordFieldsCardParams, RecordFlowParams, RecordItemsCardParams, RecordRead, RecordSourceParams, RecordStepParams, RecordWrites } from "../Interfaces/RecordInterfaces.ts";
|
|
5
|
+
/** A fields card with its data's type: `fieldsCard<Profile>({ ... })`. */
|
|
6
|
+
export declare const fieldsCard: <TData>(card: RecordFieldsCardParams<TData>) => RecordFieldsCardParams<TData>;
|
|
7
|
+
/** An items card with its data's and its items' types: `itemsCard<ContactList, Contact>({ ... })`. */
|
|
8
|
+
export declare const itemsCard: <TData, TItem>(card: RecordItemsCardParams<TData, TItem>) => RecordItemsCardParams<TData, TItem>;
|
|
9
|
+
/** A card of the page's own, with its data's type. */
|
|
10
|
+
export declare const contentCard: <TData>(card: RecordContentCardParams<TData>) => RecordContentCardParams<TData>;
|
|
11
|
+
/** Which of the three a card is. */
|
|
12
|
+
export declare function recordCardKind(card: RecordCardParams): "fields" | "items" | "content";
|
|
13
|
+
/** A field given as its input alone, or with its own label and look: always the second. */
|
|
14
|
+
export declare function recordField<TData>(entry: InputParams | RecordFieldParams<TData>): RecordFieldParams<TData>;
|
|
15
|
+
/**
|
|
16
|
+
* What a popup's form opens on, of the record or the item it works on: the values its inputs name that an input can hold
|
|
17
|
+
* (text, a number, a yes or no, a list of those; rows for a rows input - Tabular, Group). A field of another shape - the
|
|
18
|
+
* code record of a contact, when the step's box is also called `code` - is left out, so the box opens empty rather than
|
|
19
|
+
* on "[object Object]". Rows are kept whole: an Edit that opened with no rows would send none and wipe them.
|
|
20
|
+
*/
|
|
21
|
+
export declare function recordOpenedValues(given: unknown, inputs: readonly InputParams[]): FormValues;
|
|
22
|
+
/** A fields card's Edit inputs: the fields' own, without those only shown (`edit: false`) or that hold no value. */
|
|
23
|
+
export declare function recordFieldInputs(card: Pick<RecordFieldsCardParams, "fields">): InputParams[];
|
|
24
|
+
/** How one value reads in a card. */
|
|
25
|
+
export type RecordValueView = {
|
|
26
|
+
kind: "empty";
|
|
27
|
+
} | {
|
|
28
|
+
kind: "text";
|
|
29
|
+
text: string;
|
|
30
|
+
} | {
|
|
31
|
+
kind: "lines";
|
|
32
|
+
text: string;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "chips";
|
|
35
|
+
items: string[];
|
|
36
|
+
} | {
|
|
37
|
+
kind: "markdown";
|
|
38
|
+
text: string;
|
|
39
|
+
} | {
|
|
40
|
+
kind: "rich";
|
|
41
|
+
html: string;
|
|
42
|
+
};
|
|
43
|
+
/** Whether a field takes the card's whole width when it does not say: long text, chips and editors do. */
|
|
44
|
+
export declare function recordFieldWide(field: RecordFieldParams): boolean;
|
|
45
|
+
/** What a field holds while its source loads, so the card is as tall as it will be: a line, two lines, or a chip's row. */
|
|
46
|
+
export declare function recordFieldHeld(field: RecordFieldParams): "line" | "lines" | "chips";
|
|
47
|
+
/** How many lines a value held as lines holds while its source loads: the field's `heldLines`, else two. */
|
|
48
|
+
export declare function recordHeldLines(field: Pick<RecordFieldParams, "heldLines">): number;
|
|
49
|
+
/**
|
|
50
|
+
* A value as a card draws it, from the input that edits it: a choice by its label, a date in the person's format, a
|
|
51
|
+
* number with its format, a yes or a no, a list as chips (`look: "chips"`, a multiple choice, a group of checkboxes), long
|
|
52
|
+
* text on its own lines, Markdown and rich text as such, a password never. Empty when there is nothing to say.
|
|
53
|
+
*/
|
|
54
|
+
export declare function recordValueView(params: InputParams, value: unknown, options?: {
|
|
55
|
+
look?: RecordFieldParams["look"];
|
|
56
|
+
preferences?: DatePreferences;
|
|
57
|
+
}): RecordValueView;
|
|
58
|
+
/** A card's items: the field it names, what its function answers, or the data itself when that is the list. */
|
|
59
|
+
export declare function recordItems<TItem>(data: unknown, items?: string | ((data: any) => readonly TItem[])): readonly TItem[];
|
|
60
|
+
/** An item's key, as text. */
|
|
61
|
+
export declare function recordItemKey<TItem>(item: TItem, itemKey: string | ((item: TItem) => string | number)): string;
|
|
62
|
+
/** The data with these items in place of its own, or `null` when the items come from a function (which cannot be set). */
|
|
63
|
+
export declare function recordWithItems(data: unknown, items: string | ((data: any) => readonly unknown[]) | undefined, next: readonly unknown[]): unknown;
|
|
64
|
+
/** The most a card's list may hold, by its `limit`: a number, a field of the data, or a function. `null` for no limit. */
|
|
65
|
+
export declare function recordLimit(data: unknown, limit: RecordItemsCardParams["limit"]): number | null;
|
|
66
|
+
/** "3 of 5", or "3" with no limit. */
|
|
67
|
+
export declare const recordCount: (count: number, limit: number | null) => string;
|
|
68
|
+
/** Why Add is held at the limit. */
|
|
69
|
+
export declare function recordLimitReason(limit: number, reason: RecordItemsCardParams["limitReason"]): string;
|
|
70
|
+
/** What a write does to the card's data: its new value, a read again, or nothing. */
|
|
71
|
+
export type RecordWriteOutcome = {
|
|
72
|
+
kind: "write";
|
|
73
|
+
data: unknown;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "reload";
|
|
76
|
+
} | {
|
|
77
|
+
kind: "none";
|
|
78
|
+
};
|
|
79
|
+
export interface RecordWriteRequest {
|
|
80
|
+
writes: RecordWrites | undefined;
|
|
81
|
+
/** What the server answered. */
|
|
82
|
+
result: unknown;
|
|
83
|
+
/** The card's data as held now - never as the popup was opened on, so a change made meanwhile stays. */
|
|
84
|
+
data: unknown;
|
|
85
|
+
/** The values sent (before `transform`). */
|
|
86
|
+
values: unknown;
|
|
87
|
+
/** What an answer that is no record does under `"answer"`: a card's Edit writes its values, the rest read again. */
|
|
88
|
+
fallback: "values" | "reload";
|
|
89
|
+
/** The card's items, for `"item"` and `"remove"`. */
|
|
90
|
+
items?: string | ((data: any) => readonly unknown[]);
|
|
91
|
+
itemKey?: string | ((item: any) => string | number);
|
|
92
|
+
/** The item acted on, for `"remove"`. */
|
|
93
|
+
item?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What an answer writes into the card's data, as `writes` says (`RecordWrites`). Everything merges over the data as held
|
|
97
|
+
* now; a list (`"item"`, `"remove"`) is written back only where the card names its field or the data is the list, else
|
|
98
|
+
* the source is read again.
|
|
99
|
+
*/
|
|
100
|
+
export declare function recordWrite(request: RecordWriteRequest): RecordWriteOutcome;
|
|
101
|
+
/** Whether an action shows, and when it is held, why. */
|
|
102
|
+
export type RecordActionState = {
|
|
103
|
+
kind: "hidden";
|
|
104
|
+
} | {
|
|
105
|
+
kind: "held";
|
|
106
|
+
reason: string;
|
|
107
|
+
} | {
|
|
108
|
+
kind: "ready";
|
|
109
|
+
};
|
|
110
|
+
/** The endpoint an action calls first, when it is one: the action shows only to a person who may call it. */
|
|
111
|
+
export declare function recordActionEndpoint(action: {
|
|
112
|
+
flow?: RecordFlowParams;
|
|
113
|
+
confirm?: {
|
|
114
|
+
submit: unknown;
|
|
115
|
+
};
|
|
116
|
+
}): ApiEndpoint | undefined;
|
|
117
|
+
/** What a heading's button says while its card's data is read. */
|
|
118
|
+
export declare const RECORD_STILL_READ = "Still being read.";
|
|
119
|
+
/**
|
|
120
|
+
* An action's state: hidden on a read-only card, by its own rule, or to a person who may not call its endpoint; held with
|
|
121
|
+
* its reason; else ready. `args` is `null` while the data is still read: the rules typed on the data are not asked then,
|
|
122
|
+
* and the action is held ("Still being read.") or, with `loading: "hidden"`, not drawn yet.
|
|
123
|
+
*/
|
|
124
|
+
export declare function recordActionState(action: {
|
|
125
|
+
hidden?: (...args: any[]) => boolean;
|
|
126
|
+
held?: (...args: any[]) => string | false | null | undefined;
|
|
127
|
+
loading?: "held" | "hidden";
|
|
128
|
+
flow?: RecordFlowParams;
|
|
129
|
+
confirm?: {
|
|
130
|
+
submit: unknown;
|
|
131
|
+
};
|
|
132
|
+
}, args: readonly unknown[] | null, { readOnly, can }: {
|
|
133
|
+
readOnly: boolean;
|
|
134
|
+
can: (endpoint: ApiEndpoint) => boolean;
|
|
135
|
+
}): RecordActionState;
|
|
136
|
+
/**
|
|
137
|
+
* A card's calls on their way, by what each works on (an item's key, or "card"): one added as it starts, that one alone
|
|
138
|
+
* taken away as it ends, so a call that ends first never frees a row whose own call still runs.
|
|
139
|
+
*/
|
|
140
|
+
export declare function recordBusyKeys(keys: readonly string[] | undefined, key: string, on: boolean): readonly string[];
|
|
141
|
+
/** An action's accessible name with what it acts on: "Edit: Personal details", "Verify: ram@example.com". */
|
|
142
|
+
export declare const recordActionName: (label: string, of: string | null | undefined) => string;
|
|
143
|
+
/** A popup's steps, each with its id (its place, "1", when it names none): the flow itself when it has no `steps`. */
|
|
144
|
+
export declare function recordSteps<TData, TItem>(flow: RecordFlowParams<TData, TItem>): (RecordStepParams<TData, TItem> & {
|
|
145
|
+
id: string;
|
|
146
|
+
})[];
|
|
147
|
+
/** The step a popup opens on: `start` when it names a step there is, else the first. */
|
|
148
|
+
export declare function recordStartStep<TData, TItem>(flow: RecordFlowParams<TData, TItem>, context: RecordContext<TData, TItem>): string;
|
|
149
|
+
/** The status line a step's bar keeps: the full one for a form of several inputs, the quiet one for a step of one or two. */
|
|
150
|
+
export declare const recordStatusLine: (inputs: number) => "full" | "errors";
|
|
151
|
+
/** The default rule for a session that ended: a 401 that names no input (a wrong password names its box). */
|
|
152
|
+
export declare const recordSessionEnded: (answer: ApiResult) => boolean;
|
|
153
|
+
/** Whole seconds until a moment (an ISO text or ms), never below zero. */
|
|
154
|
+
export declare function secondsUntil(moment: string | number | null | undefined, now?: number): number;
|
|
155
|
+
/** One source as the page holds it: on its way (with what was known before), read, or not readable. */
|
|
156
|
+
export type RecordSourceState = {
|
|
157
|
+
status: "loading";
|
|
158
|
+
data: unknown;
|
|
159
|
+
} | {
|
|
160
|
+
status: "ready";
|
|
161
|
+
data: unknown;
|
|
162
|
+
} | {
|
|
163
|
+
status: "failed";
|
|
164
|
+
data: unknown;
|
|
165
|
+
answer: ApiResult;
|
|
166
|
+
};
|
|
167
|
+
/** How a source is read: its function, or a GET of its endpoint or URL by the app's client. */
|
|
168
|
+
export type RecordReader = (read: RecordRead, signal: AbortSignal) => Promise<ApiResult>;
|
|
169
|
+
/**
|
|
170
|
+
* The page's sources: each read once (and again on Try again or a step's `reload`), written by the answers. A read that
|
|
171
|
+
* fails before anything was read leaves the source failed; one that fails while its data shows keeps the data and only
|
|
172
|
+
* answers the failure. A newer read wins over an older one still on its way. `end` marks the session ended: the page
|
|
173
|
+
* shows its words in place of the cards and nothing more is read.
|
|
174
|
+
*/
|
|
175
|
+
export declare class RecordSources {
|
|
176
|
+
private states;
|
|
177
|
+
private readonly controllers;
|
|
178
|
+
private readonly listeners;
|
|
179
|
+
private endedAnswer;
|
|
180
|
+
private readonly sources;
|
|
181
|
+
private readonly reader;
|
|
182
|
+
constructor(sources: Readonly<Record<string, RecordRead | RecordSourceParams>>, reader: RecordReader);
|
|
183
|
+
/** The names, in the page's order: the first is every card's default. */
|
|
184
|
+
get names(): string[];
|
|
185
|
+
subscribe: (listener: () => void) => () => void;
|
|
186
|
+
/** Every source's state; a new object after each change, for `useSyncExternalStore`. */
|
|
187
|
+
snapshot: () => Readonly<Record<string, RecordSourceState>>;
|
|
188
|
+
get(name: string): RecordSourceState;
|
|
189
|
+
/** The answer that found the session ended, once one has. */
|
|
190
|
+
get ended(): ApiResult | null;
|
|
191
|
+
private set;
|
|
192
|
+
/** Reads every source not read yet (none read, or stopped on its way). */
|
|
193
|
+
start(): void;
|
|
194
|
+
/** Stops every read on its way; `start` reads them again. */
|
|
195
|
+
stop(): void;
|
|
196
|
+
/**
|
|
197
|
+
* Reads a source (again): resolves with the answer. A failed source goes back to loading while it runs; one that shows
|
|
198
|
+
* its data keeps it. An answer that a newer read or `stop` overtook changes nothing and resolves aborted.
|
|
199
|
+
*/
|
|
200
|
+
read(name: string): Promise<ApiResult>;
|
|
201
|
+
/** Writes a source's data: every card on it follows. */
|
|
202
|
+
write(name: string, data: unknown): void;
|
|
203
|
+
/** The session ended: nothing more is read, and the page shows its words. */
|
|
204
|
+
end(answer: ApiResult): void;
|
|
205
|
+
}
|