webspresso 0.0.97 → 0.0.99
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/.agents/ADMIN.md +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal Zero-Dependency QR Code Generator
|
|
3
|
+
* Pure JavaScript implementation of QR Code Model 2 (ISO/IEC 18004)
|
|
4
|
+
* Supports Byte mode (UTF-8 / ASCII) with Error Correction Levels (L, M, Q, H)
|
|
5
|
+
* Versions 1 to 10 (sufficient for URIs up to 271 bytes)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Error correction levels
|
|
9
|
+
const ECC_LEVELS = {
|
|
10
|
+
L: 1, // Low (~7%)
|
|
11
|
+
M: 0, // Medium (~15%)
|
|
12
|
+
Q: 3, // Quartile (~25%)
|
|
13
|
+
H: 2, // High (~30%)
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// Format info bits for each (ECC, mask) combination (15 bits, BCH encoded)
|
|
17
|
+
const FORMAT_INFO = [
|
|
18
|
+
[0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976], // M (0)
|
|
19
|
+
[0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0], // L (1)
|
|
20
|
+
[0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed], // H (2)
|
|
21
|
+
[0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b], // Q (3)
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Capacity and ECC table for Versions 1-10:
|
|
25
|
+
// [version, totalDataBytes, ecBytesPerBlock, numBlocksGroup1, dataBytesBlock1, numBlocksGroup2, dataBytesBlock2]
|
|
26
|
+
// ECC order: [M, L, H, Q]
|
|
27
|
+
const VERSION_TABLE = {
|
|
28
|
+
1: {
|
|
29
|
+
M: { totalCodewords: 26, ecPerBlock: 10, b1: 1, d1: 16, b2: 0, d2: 0 },
|
|
30
|
+
L: { totalCodewords: 26, ecPerBlock: 7, b1: 1, d1: 19, b2: 0, d2: 0 },
|
|
31
|
+
H: { totalCodewords: 26, ecPerBlock: 17, b1: 1, d1: 9, b2: 0, d2: 0 },
|
|
32
|
+
Q: { totalCodewords: 26, ecPerBlock: 13, b1: 1, d1: 13, b2: 0, d2: 0 },
|
|
33
|
+
},
|
|
34
|
+
2: {
|
|
35
|
+
M: { totalCodewords: 44, ecPerBlock: 16, b1: 1, d1: 28, b2: 0, d2: 0 },
|
|
36
|
+
L: { totalCodewords: 44, ecPerBlock: 10, b1: 1, d1: 34, b2: 0, d2: 0 },
|
|
37
|
+
H: { totalCodewords: 44, ecPerBlock: 28, b1: 1, d1: 16, b2: 0, d2: 0 },
|
|
38
|
+
Q: { totalCodewords: 44, ecPerBlock: 22, b1: 1, d1: 22, b2: 0, d2: 0 },
|
|
39
|
+
},
|
|
40
|
+
3: {
|
|
41
|
+
M: { totalCodewords: 70, ecPerBlock: 26, b1: 1, d1: 44, b2: 0, d2: 0 },
|
|
42
|
+
L: { totalCodewords: 70, ecPerBlock: 15, b1: 1, d1: 55, b2: 0, d2: 0 },
|
|
43
|
+
H: { totalCodewords: 70, ecPerBlock: 22, b1: 2, d1: 13, b2: 0, d2: 0 },
|
|
44
|
+
Q: { totalCodewords: 70, ecPerBlock: 18, b1: 2, d1: 17, b2: 0, d2: 0 },
|
|
45
|
+
},
|
|
46
|
+
4: {
|
|
47
|
+
M: { totalCodewords: 100, ecPerBlock: 18, b1: 2, d1: 32, b2: 0, d2: 0 },
|
|
48
|
+
L: { totalCodewords: 100, ecPerBlock: 20, b1: 1, d1: 80, b2: 0, d2: 0 },
|
|
49
|
+
H: { totalCodewords: 100, ecPerBlock: 16, b1: 4, d1: 9, b2: 0, d2: 0 },
|
|
50
|
+
Q: { totalCodewords: 100, ecPerBlock: 26, b1: 2, d1: 24, b2: 0, d2: 0 },
|
|
51
|
+
},
|
|
52
|
+
5: {
|
|
53
|
+
M: { totalCodewords: 134, ecPerBlock: 24, b1: 2, d1: 43, b2: 0, d2: 0 },
|
|
54
|
+
L: { totalCodewords: 134, ecPerBlock: 26, b1: 1, d1: 108, b2: 0, d2: 0 },
|
|
55
|
+
H: { totalCodewords: 134, ecPerBlock: 22, b1: 2, d1: 11, b2: 2, d2: 12 },
|
|
56
|
+
Q: { totalCodewords: 134, ecPerBlock: 18, b1: 2, d1: 15, b2: 2, d2: 16 },
|
|
57
|
+
},
|
|
58
|
+
6: {
|
|
59
|
+
M: { totalCodewords: 172, ecPerBlock: 16, b1: 4, d1: 27, b2: 0, d2: 0 },
|
|
60
|
+
L: { totalCodewords: 172, ecPerBlock: 18, b1: 2, d1: 68, b2: 0, d2: 0 },
|
|
61
|
+
H: { totalCodewords: 172, ecPerBlock: 28, b1: 4, d1: 15, b2: 0, d2: 0 },
|
|
62
|
+
Q: { totalCodewords: 172, ecPerBlock: 24, b1: 4, d1: 19, b2: 0, d2: 0 },
|
|
63
|
+
},
|
|
64
|
+
7: {
|
|
65
|
+
M: { totalCodewords: 196, ecPerBlock: 18, b1: 4, d1: 31, b2: 0, d2: 0 },
|
|
66
|
+
L: { totalCodewords: 196, ecPerBlock: 20, b1: 2, d1: 78, b2: 0, d2: 0 },
|
|
67
|
+
H: { totalCodewords: 196, ecPerBlock: 26, b1: 4, d1: 13, b2: 1, d2: 14 },
|
|
68
|
+
Q: { totalCodewords: 196, ecPerBlock: 18, b1: 2, d1: 14, b2: 4, d2: 15 },
|
|
69
|
+
},
|
|
70
|
+
8: {
|
|
71
|
+
M: { totalCodewords: 242, ecPerBlock: 22, b1: 2, d1: 38, b2: 2, d2: 39 },
|
|
72
|
+
L: { totalCodewords: 242, ecPerBlock: 24, b1: 2, d1: 97, b2: 0, d2: 0 },
|
|
73
|
+
H: { totalCodewords: 242, ecPerBlock: 26, b1: 4, d1: 14, b2: 2, d2: 15 },
|
|
74
|
+
Q: { totalCodewords: 242, ecPerBlock: 22, b1: 4, d1: 18, b2: 2, d2: 19 },
|
|
75
|
+
},
|
|
76
|
+
9: {
|
|
77
|
+
M: { totalCodewords: 292, ecPerBlock: 22, b1: 3, d1: 36, b2: 2, d2: 37 },
|
|
78
|
+
L: { totalCodewords: 292, ecPerBlock: 30, b1: 2, d1: 116, b2: 0, d2: 0 },
|
|
79
|
+
H: { totalCodewords: 292, ecPerBlock: 24, b1: 4, d1: 12, b2: 4, d2: 13 },
|
|
80
|
+
Q: { totalCodewords: 292, ecPerBlock: 20, b1: 4, d1: 16, b2: 4, d2: 17 },
|
|
81
|
+
},
|
|
82
|
+
10: {
|
|
83
|
+
M: { totalCodewords: 346, ecPerBlock: 26, b1: 4, d1: 43, b2: 1, d2: 44 },
|
|
84
|
+
L: { totalCodewords: 346, ecPerBlock: 18, b1: 2, d1: 68, b2: 2, d2: 69 },
|
|
85
|
+
H: { totalCodewords: 346, ecPerBlock: 28, b1: 6, d1: 15, b2: 2, d2: 16 },
|
|
86
|
+
Q: { totalCodewords: 346, ecPerBlock: 24, b1: 6, d1: 19, b2: 2, d2: 20 },
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Alignment pattern center positions for versions 2-10
|
|
91
|
+
const ALIGNMENT_PATTERN_POSITIONS = {
|
|
92
|
+
2: [6, 18],
|
|
93
|
+
3: [6, 22],
|
|
94
|
+
4: [6, 26],
|
|
95
|
+
5: [6, 30],
|
|
96
|
+
6: [6, 34],
|
|
97
|
+
7: [6, 22, 38],
|
|
98
|
+
8: [6, 24, 42],
|
|
99
|
+
9: [6, 26, 46],
|
|
100
|
+
10: [6, 28, 50],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Precompute GF(256) log and exp tables
|
|
104
|
+
const GF256_EXP = new Uint8Array(512);
|
|
105
|
+
const GF256_LOG = new Uint8Array(256);
|
|
106
|
+
let gfVal = 1;
|
|
107
|
+
for (let i = 0; i < 255; i++) {
|
|
108
|
+
GF256_EXP[i] = gfVal;
|
|
109
|
+
GF256_EXP[i + 255] = gfVal;
|
|
110
|
+
GF256_LOG[gfVal] = i;
|
|
111
|
+
gfVal <<= 1;
|
|
112
|
+
if (gfVal & 256) gfVal ^= 0x11d;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function gmul(a, b) {
|
|
116
|
+
if (a === 0 || b === 0) return 0;
|
|
117
|
+
return GF256_EXP[GF256_LOG[a] + GF256_LOG[b]];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function rsGenPoly(numEc) {
|
|
121
|
+
let poly = [1];
|
|
122
|
+
for (let i = 0; i < numEc; i++) {
|
|
123
|
+
const next = new Array(poly.length + 1).fill(0);
|
|
124
|
+
const root = GF256_EXP[i];
|
|
125
|
+
for (let j = 0; j < poly.length; j++) {
|
|
126
|
+
next[j] ^= gmul(poly[j], root);
|
|
127
|
+
next[j + 1] ^= poly[j];
|
|
128
|
+
}
|
|
129
|
+
poly = next;
|
|
130
|
+
}
|
|
131
|
+
return poly;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function rsCompute(data, numEc) {
|
|
135
|
+
const gen = rsGenPoly(numEc);
|
|
136
|
+
const result = new Array(numEc).fill(0);
|
|
137
|
+
for (const b of data) {
|
|
138
|
+
const factor = b ^ result.shift();
|
|
139
|
+
result.push(0);
|
|
140
|
+
for (let i = 0; i < numEc; i++) {
|
|
141
|
+
result[i] ^= gmul(gen[i], factor);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Determine the smallest QR Version that can hold the data bytes
|
|
149
|
+
*/
|
|
150
|
+
function findBestVersion(dataLength, ecc = 'M') {
|
|
151
|
+
for (let v = 1; v <= 10; v++) {
|
|
152
|
+
const table = VERSION_TABLE[v][ecc];
|
|
153
|
+
const maxDataBytes = table.b1 * table.d1 + table.b2 * table.d2;
|
|
154
|
+
// Overhead: 4 bits mode + (8 or 16 bits count)
|
|
155
|
+
const countBits = v <= 9 ? 8 : 16;
|
|
156
|
+
const available = Math.floor((maxDataBytes * 8 - 4 - countBits) / 8);
|
|
157
|
+
if (dataLength <= available) {
|
|
158
|
+
return v;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
throw new Error(`Data is too long for QR Code versions 1-10 (${dataLength} bytes)`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Encode raw text to bitstream with padding and interleaving
|
|
166
|
+
*/
|
|
167
|
+
function encodeData(text, version, ecc = 'M') {
|
|
168
|
+
const bytes = Buffer.from(text, 'utf8');
|
|
169
|
+
const table = VERSION_TABLE[version][ecc];
|
|
170
|
+
const totalDataBytes = table.b1 * table.d1 + table.b2 * table.d2;
|
|
171
|
+
|
|
172
|
+
// Bit buffer
|
|
173
|
+
let bits = '';
|
|
174
|
+
// 1. Mode indicator: 0100 for 8-bit byte mode
|
|
175
|
+
bits += '0100';
|
|
176
|
+
// 2. Character count indicator
|
|
177
|
+
const countBits = version <= 9 ? 8 : 16;
|
|
178
|
+
bits += bytes.length.toString(2).padStart(countBits, '0');
|
|
179
|
+
// 3. Data bytes
|
|
180
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
181
|
+
bits += bytes[i].toString(2).padStart(8, '0');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 4. Terminator (up to 4 zeros)
|
|
185
|
+
const maxBits = totalDataBytes * 8;
|
|
186
|
+
const termLen = Math.min(4, maxBits - bits.length);
|
|
187
|
+
bits += '0'.repeat(termLen);
|
|
188
|
+
|
|
189
|
+
// 5. Pad to multiple of 8
|
|
190
|
+
while (bits.length % 8 !== 0) {
|
|
191
|
+
bits += '0';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 6. Pad bytes (0xEC, 0x11 alternating)
|
|
195
|
+
const dataBytes = [];
|
|
196
|
+
for (let i = 0; i < bits.length; i += 8) {
|
|
197
|
+
dataBytes.push(parseInt(bits.substring(i, i + 8), 2));
|
|
198
|
+
}
|
|
199
|
+
const padBytes = [0xec, 0x11];
|
|
200
|
+
let padIdx = 0;
|
|
201
|
+
while (dataBytes.length < totalDataBytes) {
|
|
202
|
+
dataBytes.push(padBytes[padIdx % 2]);
|
|
203
|
+
padIdx++;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// 7. Group into blocks and calculate ECC
|
|
207
|
+
const blocks = [];
|
|
208
|
+
const ecBlocks = [];
|
|
209
|
+
let byteOffset = 0;
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < table.b1; i++) {
|
|
212
|
+
const block = dataBytes.slice(byteOffset, byteOffset + table.d1);
|
|
213
|
+
blocks.push(block);
|
|
214
|
+
ecBlocks.push(rsCompute(block, table.ecPerBlock));
|
|
215
|
+
byteOffset += table.d1;
|
|
216
|
+
}
|
|
217
|
+
for (let i = 0; i < table.b2; i++) {
|
|
218
|
+
const block = dataBytes.slice(byteOffset, byteOffset + table.d2);
|
|
219
|
+
blocks.push(block);
|
|
220
|
+
ecBlocks.push(rsCompute(block, table.ecPerBlock));
|
|
221
|
+
byteOffset += table.d2;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// 8. Interleave data bytes
|
|
225
|
+
const finalCodewords = [];
|
|
226
|
+
const maxDataLen = Math.max(table.d1, table.d2 || 0);
|
|
227
|
+
for (let j = 0; j < maxDataLen; j++) {
|
|
228
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
229
|
+
if (j < blocks[i].length) {
|
|
230
|
+
finalCodewords.push(blocks[i][j]);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// 9. Interleave error correction bytes
|
|
236
|
+
for (let j = 0; j < table.ecPerBlock; j++) {
|
|
237
|
+
for (let i = 0; i < ecBlocks.length; i++) {
|
|
238
|
+
finalCodewords.push(ecBlocks[i][j]);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return finalCodewords;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Generate 2D QR matrix
|
|
247
|
+
*/
|
|
248
|
+
function createQrMatrix(text, options = {}) {
|
|
249
|
+
const ecc = (options.ecc || 'M').toUpperCase();
|
|
250
|
+
const version = options.version || findBestVersion(Buffer.byteLength(text, 'utf8'), ecc);
|
|
251
|
+
const size = 17 + version * 4;
|
|
252
|
+
const codewords = encodeData(text, version, ecc);
|
|
253
|
+
|
|
254
|
+
// Initialize matrix: -1 represents unset, 0 false/white, 1 true/black
|
|
255
|
+
const matrix = Array.from({ length: size }, () => new Int8Array(size).fill(-1));
|
|
256
|
+
const isFunction = Array.from({ length: size }, () => new Uint8Array(size).fill(0));
|
|
257
|
+
|
|
258
|
+
function setFunction(r, c, val) {
|
|
259
|
+
matrix[r][c] = val ? 1 : 0;
|
|
260
|
+
isFunction[r][c] = 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 1. Finder patterns (7x7) + separators
|
|
264
|
+
function drawFinder(row, col) {
|
|
265
|
+
for (let r = -1; r <= 7; r++) {
|
|
266
|
+
for (let c = -1; c <= 7; c++) {
|
|
267
|
+
const nr = row + r;
|
|
268
|
+
const nc = col + c;
|
|
269
|
+
if (nr >= 0 && nr < size && nc >= 0 && nc < size) {
|
|
270
|
+
if (r >= 0 && r <= 6 && c >= 0 && c <= 6) {
|
|
271
|
+
const isBorder = r === 0 || r === 6 || c === 0 || c === 6;
|
|
272
|
+
const isCenter = r >= 2 && r <= 4 && c >= 2 && c <= 4;
|
|
273
|
+
setFunction(nr, nc, isBorder || isCenter);
|
|
274
|
+
} else {
|
|
275
|
+
setFunction(nr, nc, 0); // Separator
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
drawFinder(0, 0);
|
|
282
|
+
drawFinder(0, size - 7);
|
|
283
|
+
drawFinder(size - 7, 0);
|
|
284
|
+
|
|
285
|
+
// 2. Alignment patterns for Version >= 2
|
|
286
|
+
if (version >= 2) {
|
|
287
|
+
const coords = ALIGNMENT_PATTERN_POSITIONS[version];
|
|
288
|
+
for (const r of coords) {
|
|
289
|
+
for (const c of coords) {
|
|
290
|
+
// Skip if overlaps finder
|
|
291
|
+
if ((r < 9 && c < 9) || (r < 9 && c > size - 10) || (r > size - 10 && c < 9)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
for (let dr = -2; dr <= 2; dr++) {
|
|
295
|
+
for (let dc = -2; dc <= 2; dc++) {
|
|
296
|
+
const isBorder = Math.abs(dr) === 2 || Math.abs(dc) === 2;
|
|
297
|
+
const isCenter = dr === 0 && dc === 0;
|
|
298
|
+
setFunction(r + dr, c + dc, isBorder || isCenter);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 3. Timing patterns
|
|
306
|
+
for (let i = 8; i < size - 8; i++) {
|
|
307
|
+
if (!isFunction[6][i]) setFunction(6, i, i % 2 === 0);
|
|
308
|
+
if (!isFunction[i][6]) setFunction(i, 6, i % 2 === 0);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 4. Dark module
|
|
312
|
+
setFunction(size - 8, 8, 1);
|
|
313
|
+
|
|
314
|
+
// 5. Reserve format info areas
|
|
315
|
+
for (let i = 0; i < 9; i++) {
|
|
316
|
+
if (!isFunction[8][i]) isFunction[8][i] = 1;
|
|
317
|
+
if (!isFunction[i][8]) isFunction[i][8] = 1;
|
|
318
|
+
}
|
|
319
|
+
for (let i = size - 8; i < size; i++) {
|
|
320
|
+
if (!isFunction[8][i]) isFunction[8][i] = 1;
|
|
321
|
+
if (!isFunction[i][8]) isFunction[i][8] = 1;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// 6. Place data bits in matrix (zig-zag from right to left)
|
|
325
|
+
let bitIdx = 0;
|
|
326
|
+
const totalBits = codewords.length * 8;
|
|
327
|
+
|
|
328
|
+
function getBit(idx) {
|
|
329
|
+
if (idx >= totalBits) return 0;
|
|
330
|
+
const byte = codewords[Math.floor(idx / 8)];
|
|
331
|
+
return (byte >> (7 - (idx % 8))) & 1;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let upward = true;
|
|
335
|
+
for (let right = size - 1; right > 0; right -= 2) {
|
|
336
|
+
if (right === 6) right--; // Skip vertical timing line
|
|
337
|
+
const rows = [];
|
|
338
|
+
if (upward) {
|
|
339
|
+
for (let r = size - 1; r >= 0; r--) rows.push(r);
|
|
340
|
+
} else {
|
|
341
|
+
for (let r = 0; r < size; r++) rows.push(r);
|
|
342
|
+
}
|
|
343
|
+
for (const r of rows) {
|
|
344
|
+
for (let c = right; c >= right - 1; c--) {
|
|
345
|
+
if (!isFunction[r][c]) {
|
|
346
|
+
matrix[r][c] = getBit(bitIdx);
|
|
347
|
+
bitIdx++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
upward = !upward;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 7. Choose mask pattern with lowest penalty (or mask 0 by default)
|
|
355
|
+
const maskPattern = options.mask != null ? options.mask : chooseBestMask(matrix, isFunction, size);
|
|
356
|
+
|
|
357
|
+
// Apply mask and write format info
|
|
358
|
+
const eccIdx = ECC_LEVELS[ecc];
|
|
359
|
+
const formatInfo = FORMAT_INFO[eccIdx][maskPattern];
|
|
360
|
+
|
|
361
|
+
// Write format info to reserved areas
|
|
362
|
+
for (let i = 0; i < 15; i++) {
|
|
363
|
+
const bit = (formatInfo >> (14 - i)) & 1;
|
|
364
|
+
// Around top-left
|
|
365
|
+
if (i < 6) matrix[8][i] = bit;
|
|
366
|
+
else if (i === 6) matrix[8][7] = bit;
|
|
367
|
+
else if (i === 7) matrix[8][8] = bit;
|
|
368
|
+
else if (i === 8) matrix[7][8] = bit;
|
|
369
|
+
else matrix[14 - i][8] = bit;
|
|
370
|
+
|
|
371
|
+
// Around top-right and bottom-left
|
|
372
|
+
if (i < 8) {
|
|
373
|
+
matrix[size - 1 - i][8] = bit;
|
|
374
|
+
} else {
|
|
375
|
+
matrix[8][size - 15 + i] = bit;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Final boolean matrix
|
|
380
|
+
const result = [];
|
|
381
|
+
for (let r = 0; r < size; r++) {
|
|
382
|
+
const row = [];
|
|
383
|
+
for (let c = 0; c < size; c++) {
|
|
384
|
+
if (isFunction[r][c]) {
|
|
385
|
+
row.push(matrix[r][c] === 1);
|
|
386
|
+
} else {
|
|
387
|
+
const masked = maskCondition(maskPattern, r, c);
|
|
388
|
+
const bit = matrix[r][c] ^ (masked ? 1 : 0);
|
|
389
|
+
row.push(bit === 1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
result.push(row);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return { matrix: result, size, version };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function maskCondition(pattern, r, c) {
|
|
399
|
+
switch (pattern) {
|
|
400
|
+
case 0: return (r + c) % 2 === 0;
|
|
401
|
+
case 1: return r % 2 === 0;
|
|
402
|
+
case 2: return c % 3 === 0;
|
|
403
|
+
case 3: return (r + c) % 3 === 0;
|
|
404
|
+
case 4: return (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0;
|
|
405
|
+
case 5: return ((r * c) % 2) + ((r * c) % 3) === 0;
|
|
406
|
+
case 6: return (((r * c) % 2) + ((r * c) % 3)) % 2 === 0;
|
|
407
|
+
case 7: return (((r + c) % 2) + ((r * c) % 3)) % 2 === 0;
|
|
408
|
+
default: return false;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function chooseBestMask(matrix, isFunction, size) {
|
|
413
|
+
// Evaluates penalty scores for masks 0-7, returning the one with lowest penalty
|
|
414
|
+
let bestMask = 0;
|
|
415
|
+
let minPenalty = Infinity;
|
|
416
|
+
|
|
417
|
+
for (let mask = 0; mask < 8; mask++) {
|
|
418
|
+
let penalty = 0;
|
|
419
|
+
// Calculate penalty rule 1 (runs of 5+ same color) and rule 2 (2x2 blocks)
|
|
420
|
+
for (let r = 0; r < size; r++) {
|
|
421
|
+
let runColor = -1;
|
|
422
|
+
let runLen = 0;
|
|
423
|
+
for (let c = 0; c < size; c++) {
|
|
424
|
+
const val = isFunction[r][c] ? matrix[r][c] : (matrix[r][c] ^ (maskCondition(mask, r, c) ? 1 : 0));
|
|
425
|
+
if (val === runColor) {
|
|
426
|
+
runLen++;
|
|
427
|
+
} else {
|
|
428
|
+
if (runLen >= 5) penalty += 3 + (runLen - 5);
|
|
429
|
+
runColor = val;
|
|
430
|
+
runLen = 1;
|
|
431
|
+
}
|
|
432
|
+
// 2x2 blocks
|
|
433
|
+
if (r < size - 1 && c < size - 1) {
|
|
434
|
+
const v2 = isFunction[r][c+1] ? matrix[r][c+1] : (matrix[r][c+1] ^ (maskCondition(mask, r, c+1) ? 1 : 0));
|
|
435
|
+
const v3 = isFunction[r+1][c] ? matrix[r+1][c] : (matrix[r+1][c] ^ (maskCondition(mask, r+1, c) ? 1 : 0));
|
|
436
|
+
const v4 = isFunction[r+1][c+1] ? matrix[r+1][c+1] : (matrix[r+1][c+1] ^ (maskCondition(mask, r+1, c+1) ? 1 : 0));
|
|
437
|
+
if (val === v2 && val === v3 && val === v4) {
|
|
438
|
+
penalty += 3;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (runLen >= 5) penalty += 3 + (runLen - 5);
|
|
443
|
+
}
|
|
444
|
+
if (penalty < minPenalty) {
|
|
445
|
+
minPenalty = penalty;
|
|
446
|
+
bestMask = mask;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return bestMask;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Generate SVG string from QR Code
|
|
454
|
+
* @param {string} text - Text to encode
|
|
455
|
+
* @param {Object} [options] - Options
|
|
456
|
+
* @param {number} [options.margin=4] - Quiet zone margin (modules)
|
|
457
|
+
* @param {number} [options.size=256] - Width and height in px
|
|
458
|
+
* @param {string} [options.darkColor='#000000'] - Dark module color
|
|
459
|
+
* @param {string} [options.lightColor='#ffffff'] - Light module color (or 'none')
|
|
460
|
+
* @returns {string} SVG markup
|
|
461
|
+
*/
|
|
462
|
+
function generateQrSvg(text, options = {}) {
|
|
463
|
+
const { matrix, size } = createQrMatrix(text, options);
|
|
464
|
+
const margin = options.margin != null ? options.margin : 4;
|
|
465
|
+
const viewBoxSize = size + margin * 2;
|
|
466
|
+
const darkColor = options.darkColor || '#000000';
|
|
467
|
+
const lightColor = options.lightColor || '#ffffff';
|
|
468
|
+
const width = options.size || 256;
|
|
469
|
+
const height = options.size || 256;
|
|
470
|
+
|
|
471
|
+
let pathData = '';
|
|
472
|
+
for (let r = 0; r < size; r++) {
|
|
473
|
+
for (let c = 0; c < size; c++) {
|
|
474
|
+
if (matrix[r][c]) {
|
|
475
|
+
pathData += `M${c + margin} ${r + margin}h1v1h-1z `;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const bgRect = lightColor && lightColor !== 'none'
|
|
481
|
+
? `<rect width="${viewBoxSize}" height="${viewBoxSize}" fill="${lightColor}"/>`
|
|
482
|
+
: '';
|
|
483
|
+
|
|
484
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${viewBoxSize} ${viewBoxSize}" width="${width}" height="${height}" shape-rendering="crispEdges">${bgRect}<path d="${pathData.trim()}" fill="${darkColor}"/></svg>`;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Generate Data URI from SVG
|
|
489
|
+
*/
|
|
490
|
+
function generateQrDataUri(text, options = {}) {
|
|
491
|
+
const svg = generateQrSvg(text, options);
|
|
492
|
+
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
module.exports = {
|
|
496
|
+
createQrMatrix,
|
|
497
|
+
generateQrSvg,
|
|
498
|
+
generateQrDataUri,
|
|
499
|
+
};
|