Skip to content

Commit fea5e51

Browse files
committed
Merge branch 'feat/qr-decoder' into chore/all-my-stuffs
# Conflicts: # components.d.ts # package.json # pnpm-lock.yaml # src/tools/index.ts
2 parents 5c5e865 + 880d937 commit fea5e51

File tree

8 files changed

+333
-2
lines changed

8 files changed

+333
-2
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@
149149
"json-analyzer": "^1.2.2",
150150
"json2csharp": "^1.0.3",
151151
"js-base64": "^3.7.7",
152+
"ical.js": "^2.0.1",
152153
"json5": "^2.2.3",
153154
"jsonpath": "^1.1.1",
154155
"jsonar-mod": "^1.9.0",
@@ -200,6 +201,7 @@
200201
"pp-qr-code": "^0.6.3",
201202
"qrcode": "^1.5.1",
202203
"qrcode-terminal-nooctal": "^0.12.1",
204+
"qrcode-parser": "^2.1.3",
203205
"sql-formatter": "^13.0.0",
204206
"svg2png-wasm": "^1.4.1",
205207
"svg-to-url": "^4.0.0",

pnpm-lock.yaml

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/tools/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ import { tool as icalGenerator } from './ical-generator';
6969
import { tool as imageToCss } from './image-to-css';
7070
import { tool as jsonToGo } from './json-to-go';
7171
import { tool as jsonToSchema } from './json-to-schema';
72+
import { tool as qrCodeDecoder } from './qr-code-decoder';
7273
import { tool as markdownToHtml } from './markdown-to-html';
7374
import { tool as nginxFormatter } from './nginx-formatter';
7475
import { tool as potrace } from './potrace';
@@ -267,6 +268,9 @@ export const toolsByCategory: ToolCategory[] = [
267268
imageConverter,
268269
ocrImage,
269270
potrace,
271+
qrCodeDecoder,
272+
svgPlaceholderGenerator,
273+
cameraRecorder,
270274
],
271275
},
272276
{
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
declare module "ical.js" {
2+
export function parse(content: string): object;
3+
}

src/tools/qr-code-decoder/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Qrcode } from '@vicons/tabler';
2+
import { defineTool } from '../tool';
3+
4+
export const tool = defineTool({
5+
name: 'QR Code decoder',
6+
path: '/qr-code-decoder',
7+
description: 'QR Code Reader',
8+
keywords: ['qrcode', 'qr-code', 'decoder', 'reader'],
9+
component: () => import('./qr-code-decoder.vue'),
10+
icon: Qrcode,
11+
createdAt: new Date('2024-09-01'),
12+
});
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { parseQRData } from './qr-code-decoder.service';
3+
4+
describe('qr-code-decoder', () => {
5+
test('parseQRData should parse content correctly', () => {
6+
expect(parseQRData(null)).toEqual({
7+
type: 'Unknown',
8+
value: '',
9+
});
10+
expect(parseQRData('')).toEqual({
11+
type: 'Unknown',
12+
value: '',
13+
});
14+
expect(parseQRData('TEL:+33123456')).toEqual({
15+
type: 'Phone',
16+
value: '+33123456',
17+
});
18+
expect(parseQRData('MATMSG:TO: [email protected];SUB:email subject;BODY:Email text;;')).toEqual({
19+
type: 'Email',
20+
value: {
21+
body: 'Email text',
22+
subject: 'email subject',
23+
24+
},
25+
});
26+
expect(parseQRData('mailto:[email protected]?subject=email subject&body=Email text')).toEqual({
27+
type: 'Email',
28+
value: {
29+
body: 'Email text',
30+
subject: 'email subject',
31+
32+
},
33+
});
34+
expect(parseQRData('SMTP:[email protected]:email subject:Email text')).toEqual({
35+
type: 'Email',
36+
value: {
37+
body: 'Email text',
38+
subject: 'email subject',
39+
40+
},
41+
});
42+
expect(parseQRData('smsto:+33315555:message')).toEqual({
43+
type: 'SMS',
44+
value: {
45+
message: 'message',
46+
to: '+33315555',
47+
},
48+
});
49+
expect(parseQRData('WIFI:T:nopass;S:ssid;H:true;')).toEqual({
50+
type: 'Wifi',
51+
value: {
52+
authentication: 'nopass',
53+
hidden: 'true',
54+
name: 'ssid',
55+
password: undefined,
56+
},
57+
});
58+
expect(parseQRData('WIFI:T:WPA;S:ssid;P:password;H:false;')).toEqual({
59+
type: 'Wifi',
60+
value: {
61+
authentication: 'WPA',
62+
hidden: 'false',
63+
name: 'ssid',
64+
password: 'password', // NOSONAR
65+
},
66+
});
67+
expect(parseQRData('BEGIN:VCALENDAR\nPRODID:-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN\nVERSION:2.0\nBEGIN:VEVENT\nDTSTAMP:19960704T120000Z\nUID:[email protected]\nORGANIZER:mailto:[email protected]\nDTSTART:19960918T143000Z\nDTEND:19960920T220000Z\nSTATUS:CONFIRMED\nCATEGORIES:CONFERENCE\nSUMMARY:Networld+Interop Conference\nDESCRIPTION:Networld+Interop Conference\n and Exhibit\\nAtlanta World Congress Center\\n\n Atlanta\\, Georgia\nEND:VEVENT\nEND:VCALENDAR'))
68+
.toEqual({
69+
type: 'iCal',
70+
value: [
71+
'vcalendar',
72+
[
73+
[
74+
'prodid',
75+
{},
76+
'text',
77+
'-//xyz Corp//NONSGML PDA Calendar Version 1.0//EN',
78+
],
79+
[
80+
'version',
81+
{},
82+
'text',
83+
'2.0',
84+
],
85+
],
86+
[
87+
[
88+
'vevent',
89+
[
90+
[
91+
'dtstamp',
92+
{},
93+
'date-time',
94+
'1996-07-04T12:00:00Z',
95+
],
96+
[
97+
'uid',
98+
{},
99+
'text',
100+
101+
],
102+
[
103+
'organizer',
104+
{},
105+
'cal-address',
106+
107+
],
108+
[
109+
'dtstart',
110+
{},
111+
'date-time',
112+
'1996-09-18T14:30:00Z',
113+
],
114+
[
115+
'dtend',
116+
{},
117+
'date-time',
118+
'1996-09-20T22:00:00Z',
119+
],
120+
[
121+
'status',
122+
{},
123+
'text',
124+
'CONFIRMED',
125+
],
126+
[
127+
'categories',
128+
{},
129+
'text',
130+
'CONFERENCE',
131+
],
132+
[
133+
'summary',
134+
{},
135+
'text',
136+
'Networld+Interop Conference',
137+
],
138+
[
139+
'description',
140+
{},
141+
'text',
142+
'Networld+Interop Conference and Exhibit\nAtlanta World Congress Center\nAtlanta, Georgia',
143+
],
144+
],
145+
[],
146+
],
147+
],
148+
],
149+
});
150+
});
151+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import ICAL from 'ical.js';
2+
3+
export function parseQRData(qrContent: string | null) {
4+
if (!qrContent) {
5+
return { type: 'Unknown', value: '' };
6+
}
7+
if (qrContent.startsWith('BEGIN:VCALENDAR')) {
8+
return { type: 'iCal', value: ICAL.parse(qrContent?.trim()) };
9+
}
10+
if (qrContent.startsWith('TEL:')) {
11+
return { type: 'Phone', value: qrContent.substring(4)?.trim() };
12+
}
13+
if (qrContent.startsWith('MATMSG:')) {
14+
// MATMSG:TO: email@example.com;SUB:email subject;BODY:Email text;;
15+
const parsing = /^MATMSG:(?:TO:([^;]*);)?(?:SUB:([^;]*);)?(?:BODY:([^;]*))?;;$/.exec(qrContent) || [];
16+
return {
17+
type: 'Email',
18+
value: {
19+
to: parsing[1]?.trim(),
20+
subject: parsing[2]?.trim(),
21+
body: parsing[3]?.trim(),
22+
},
23+
};
24+
}
25+
if (qrContent.startsWith('mailto:')) {
26+
// mailto:email@example.com?subject=email subject&body=Email text
27+
const parsing = /^mailto:([^\?]+)\?subject=([^\&]*)(?:&body=(.*))$/.exec(qrContent) || [];
28+
return {
29+
type: 'Email',
30+
value: {
31+
to: parsing[1]?.trim(),
32+
subject: parsing[2]?.trim(),
33+
body: parsing[3]?.trim(),
34+
},
35+
};
36+
}
37+
if (qrContent.startsWith('SMTP:')) {
38+
// SMTP:email@example.com:email subject:Email text
39+
const parsing = /^SMTP:([^:]+)(?::([^:]*))(?::([^:]*))?$/.exec(qrContent) || [];
40+
return {
41+
type: 'Email',
42+
value: {
43+
to: parsing[1]?.trim(),
44+
subject: parsing[2]?.trim(),
45+
body: parsing[3]?.trim(),
46+
},
47+
};
48+
}
49+
if (qrContent.startsWith('smsto:')) {
50+
// smsto:${phoneNumber}:${message}
51+
const parsing = /^smsto:([^:]+)(?::(.+))$/.exec(qrContent) || [];
52+
return {
53+
type: 'SMS',
54+
value: {
55+
to: parsing[1]?.trim(),
56+
message: parsing[2]?.trim(),
57+
},
58+
};
59+
}
60+
if (qrContent.startsWith('WIFI:')) {
61+
// WIFI:T:${authentication};S:${name};${authentication !== 'nopass' ? `P:${password};` : ''}H:${hidden};
62+
const parsing = /^WIFI:T:([^;]+);S:([^;]+);(?:P:([^;]+);)?(?:H:([^;]+);)?$/.exec(qrContent) || [];
63+
return {
64+
type: 'Wifi',
65+
value: {
66+
authentication: parsing[1]?.trim(),
67+
name: parsing[2]?.trim(),
68+
password: parsing[3]?.trim(),
69+
hidden: parsing[4]?.trim(),
70+
},
71+
};
72+
}
73+
if (/^(?:https?|ftp):\/\//.test(qrContent)) {
74+
return {
75+
type: 'Url',
76+
value: qrContent,
77+
};
78+
}
79+
return {
80+
type: 'Text',
81+
value: qrContent,
82+
};
83+
}

0 commit comments

Comments
 (0)