Skip to content

Commit 951143d

Browse files
committed
feat: handle parsing of common qr code types
Fix CorentinTh#1224
1 parent e8cd98c commit 951143d

File tree

7 files changed

+275
-8
lines changed

7 files changed

+275
-8
lines changed

components.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,15 +129,18 @@ declare module '@vue/runtime-core' {
129129
NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default']
130130
NCode: typeof import('naive-ui')['NCode']
131131
NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
132+
NColorPicker: typeof import('naive-ui')['NColorPicker']
132133
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
133134
NDivider: typeof import('naive-ui')['NDivider']
134135
NEllipsis: typeof import('naive-ui')['NEllipsis']
136+
NForm: typeof import('naive-ui')['NForm']
135137
NFormItem: typeof import('naive-ui')['NFormItem']
136138
NGi: typeof import('naive-ui')['NGi']
137139
NGrid: typeof import('naive-ui')['NGrid']
138140
NH1: typeof import('naive-ui')['NH1']
139141
NH3: typeof import('naive-ui')['NH3']
140142
NIcon: typeof import('naive-ui')['NIcon']
143+
NImage: typeof import('naive-ui')['NImage']
141144
NInputNumber: typeof import('naive-ui')['NInputNumber']
142145
NLabel: typeof import('naive-ui')['NLabel']
143146
NLayout: typeof import('naive-ui')['NLayout']
@@ -152,6 +155,7 @@ declare module '@vue/runtime-core' {
152155
PdfSignatureDetails: typeof import('./src/tools/pdf-signature-checker/components/pdf-signature-details.vue')['default']
153156
PercentageCalculator: typeof import('./src/tools/percentage-calculator/percentage-calculator.vue')['default']
154157
PhoneParserAndFormatter: typeof import('./src/tools/phone-parser-and-formatter/phone-parser-and-formatter.vue')['default']
158+
QrCodeDecoder: typeof import('./src/tools/qr-code-decoder/qr-code-decoder.vue')['default']
155159
QrCodeGenerator: typeof import('./src/tools/qr-code-generator/qr-code-generator.vue')['default']
156160
RandomPortGenerator: typeof import('./src/tools/random-port-generator/random-port-generator.vue')['default']
157161
ResultRow: typeof import('./src/tools/ipv4-range-expander/result-row.vue')['default']

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"highlight.js": "^11.7.0",
6565
"iarna-toml-esm": "^3.0.5",
6666
"ibantools": "^4.3.3",
67+
"ical.js": "^2.0.1",
6768
"json5": "^2.2.3",
6869
"jwt-decode": "^3.1.2",
6970
"libphonenumber-js": "^1.10.28",

pnpm-lock.yaml

Lines changed: 16 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
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+
}
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',
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 (/\w+:\/\//.test(qrContent)) {
74+
return {
75+
type: 'Url',
76+
value: qrContent,
77+
};
78+
}
79+
return {
80+
type: 'Text',
81+
value: qrContent,
82+
};
83+
}

src/tools/qr-code-decoder/qr-code-decoder.vue

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import type { Ref } from 'vue';
33
import qrcodeParser from 'qrcode-parser';
4+
import { parseQRData } from './qr-code-decoder.service';
45
import TextareaCopyable from '@/components/TextareaCopyable.vue';
56
67
const fileInput = ref() as Ref<File>;
@@ -12,6 +13,15 @@ const qrCode = computedAsync(async () => {
1213
return e.toString();
1314
}
1415
});
16+
const qrCodeParsed = computed(() => {
17+
try {
18+
const parsed = parseQRData(qrCode.value);
19+
return `Type: ${parsed.type}\nValue:${JSON.stringify(parsed.value, null, 2)}`;
20+
}
21+
catch (e: any) {
22+
return e.toString();
23+
}
24+
});
1525
1626
async function onUpload(file: File) {
1727
if (file) {
@@ -37,6 +47,13 @@ async function onUpload(file: File) {
3747
:word-wrap="true"
3848
/>
3949
</div>
50+
<div>
51+
<h3>Parsed</h3>
52+
<TextareaCopyable
53+
:value="qrCodeParsed"
54+
:word-wrap="true"
55+
/>
56+
</div>
4057
</div>
4158
</template>
4259

0 commit comments

Comments
 (0)