more fields better parsing

This commit is contained in:
GuilhermeStrice
2025-06-26 02:48:44 +01:00
parent bd15c95017
commit 92280b2717
2 changed files with 153 additions and 36 deletions

View File

@ -14,10 +14,15 @@
<table id="contactsTable"> <table id="contactsTable">
<thead> <thead>
<tr> <tr>
<th>Name</th> <th>Formatted Name (FN)</th>
<th>Phone</th> <th>Family Name</th>
<th>Email</th> <th>Given Name</th>
<th>Phone 1</th>
<th>Phone 2</th>
<th>Email 1</th>
<th>Email 2</th>
<th>Organization</th> <th>Organization</th>
<th>Other Properties</th>
</tr> </tr>
</thead> </thead>
<tbody id="contactsTableBody"> <tbody id="contactsTableBody">

178
script.js
View File

@ -6,58 +6,124 @@ document.getElementById('vcardFile').addEventListener('change', function(event)
const rawContent = e.target.result; const rawContent = e.target.result;
const contacts = parseVCard(rawContent); const contacts = parseVCard(rawContent);
displayContactsInTable(contacts); displayContactsInTable(contacts);
// For debugging, also show raw content
// const vcardContentDiv = document.getElementById('vcardRawContent');
// vcardContentDiv.textContent = rawContent;
}; };
reader.readAsText(file); reader.readAsText(file); // Defaulting to UTF-8, VCard CHARSET can override
} }
}); });
function decodeQuotedPrintable(str, charset = 'UTF-8') {
// Handle soft line breaks (= at the end of a line)
str = str.replace(/=\r?\n/g, '');
// Decode =XX hex sequences
try {
const bytes = [];
for (let i = 0; i < str.length; i++) {
if (str[i] === '=' && str[i+1] && str[i+2]) {
const hex = str.substring(i+1, i+3);
bytes.push(parseInt(hex, 16));
i += 2;
} else {
bytes.push(str.charCodeAt(i));
}
}
const byteArray = new Uint8Array(bytes);
// Standard charsets like UTF-8, ISO-8859-1 are well supported
// For others, TextDecoder might not work or might need a polyfill
return new TextDecoder(charset).decode(byteArray);
} catch (e) {
console.error("Error decoding quoted-printable string:", e);
// Fallback to a simpler replacement if TextDecoder fails or for basic cases
return str.replace(/=([A-F0-9]{2})/g, (match, p1) => String.fromCharCode(parseInt(p1, 16)));
}
}
function parseVCard(rawContent) { function parseVCard(rawContent) {
const contacts = []; const contacts = [];
const lines = rawContent.split(/\r\n|\r|\n/); // Handle line folding: join lines that start with a space or tab
const foldedLines = rawContent.replace(/\r?\n[ \t]/g, '');
const lines = foldedLines.split(/\r?\n|\r|\n/);
let currentContact = null; let currentContact = null;
lines.forEach(line => { lines.forEach(line => {
if (line.trim() === '') return;
if (line.toUpperCase() === 'BEGIN:VCARD') { if (line.toUpperCase() === 'BEGIN:VCARD') {
currentContact = {}; currentContact = {
tel: [],
email: [],
adr: [],
other: {} // For all other properties
};
} else if (line.toUpperCase() === 'END:VCARD') { } else if (line.toUpperCase() === 'END:VCARD') {
if (currentContact) { if (currentContact) {
contacts.push(currentContact); contacts.push(currentContact);
currentContact = null; currentContact = null;
} }
} else if (currentContact) { } else if (currentContact) {
const parts = line.split(':'); let [fullKey, ...valueParts] = line.split(':');
if (parts.length >= 2) { let value = valueParts.join(':');
const keyPart = parts[0];
const value = parts.slice(1).join(':');
// Basic parsing for common fields const keyParts = fullKey.split(';');
// FN (Formatted Name) const mainKey = keyParts[0].toUpperCase();
if (keyPart.startsWith('FN')) { const params = {};
keyParts.slice(1).forEach(p => {
const [paramName, paramValue] = p.split('=');
params[paramName.toUpperCase()] = paramValue;
});
// Check for Quoted-Printable encoding
if (params['ENCODING'] === 'QUOTED-PRINTABLE') {
const charset = params['CHARSET'] || 'UTF-8';
value = decodeQuotedPrintable(value, charset);
}
switch (mainKey) {
case 'FN':
currentContact.fn = value; currentContact.fn = value;
} break;
// TEL (Telephone) case 'N':
else if (keyPart.startsWith('TEL')) { // N:Family;Given;Middle;Prefix;Suffix
// Simplistic approach: take the first TEL found const nameParts = value.split(';');
if (!currentContact.tel) currentContact.tel = value; currentContact.n = {
} family: nameParts[0] || '',
// EMAIL given: nameParts[1] || '',
else if (keyPart.startsWith('EMAIL')) { middle: nameParts[2] || '',
// Simplistic approach: take the first EMAIL found prefix: nameParts[3] || '',
if (!currentContact.email) currentContact.email = value; suffix: nameParts[4] || ''
} };
// ORG (Organization) break;
else if (keyPart.startsWith('ORG')) { case 'TEL':
currentContact.org = value; currentContact.tel.push({ value: value, params: params });
} break;
case 'EMAIL':
currentContact.email.push({ value: value, params: params });
break;
case 'ORG':
currentContact.org = value.split(';')[0]; // Organization name, ignore department for now
break;
case 'ADR':
currentContact.adr.push({ value: value, params: params });
break;
// Add more common cases if needed, or they fall into 'other'
default:
if (currentContact.other[mainKey]) {
if (!Array.isArray(currentContact.other[mainKey])) {
currentContact.other[mainKey] = [currentContact.other[mainKey]];
}
currentContact.other[mainKey].push({ value: value, params: params });
} else {
currentContact.other[mainKey] = { value: value, params: params };
}
break;
} }
} }
}); });
return contacts; return contacts;
} }
function displayContactsInTable(contacts) { function displayContactsInTable(contacts) {
const tableBody = document.getElementById('contactsTableBody'); const tableBody = document.getElementById('contactsTableBody');
if (!tableBody) { if (!tableBody) {
@ -66,19 +132,65 @@ function displayContactsInTable(contacts) {
} }
tableBody.innerHTML = ''; // Clear previous entries tableBody.innerHTML = ''; // Clear previous entries
if (contacts.length === 0) { if (!contacts || contacts.length === 0) {
const row = tableBody.insertRow(); const row = tableBody.insertRow();
const cell = row.insertCell(); const cell = row.insertCell();
cell.colSpan = 4; // Number of columns // Adjust colSpan when table structure is finalized in index.html
cell.textContent = 'No contacts found in the VCard file.'; cell.colSpan = 9; // Placeholder, update with actual number of columns
cell.textContent = 'No contacts found or VCard is empty/invalid.';
return; return;
} }
contacts.forEach(contact => { contacts.forEach(contact => {
const row = tableBody.insertRow(); const row = tableBody.insertRow();
row.insertCell().textContent = contact.fn || 'N/A'; row.insertCell().textContent = contact.fn || 'N/A';
row.insertCell().textContent = contact.tel || 'N/A';
row.insertCell().textContent = contact.email || 'N/A'; // Structured Name (N)
row.insertCell().textContent = contact.n ? contact.n.family : 'N/A';
row.insertCell().textContent = contact.n ? contact.n.given : 'N/A';
// Phones - display first two, with types if available
for (let i = 0; i < 2; i++) {
const telCell = row.insertCell();
if (contact.tel && contact.tel[i]) {
let telStr = contact.tel[i].value;
const type = contact.tel[i].params['TYPE'];
if (type) telStr += ` (${type.split(',').join('/')})`; // Display multiple types e.g. (HOME/VOICE)
telCell.textContent = telStr;
} else {
telCell.textContent = 'N/A';
}
}
// Emails - display first two, with types if available
for (let i = 0; i < 2; i++) {
const emailCell = row.insertCell();
if (contact.email && contact.email[i]) {
let emailStr = contact.email[i].value;
const type = contact.email[i].params['TYPE'];
if (type) emailStr += ` (${type.split(',').join('/')})`;
emailCell.textContent = emailStr;
} else {
emailCell.textContent = 'N/A';
}
}
row.insertCell().textContent = contact.org || 'N/A'; row.insertCell().textContent = contact.org || 'N/A';
// Other Properties
const otherCell = row.insertCell();
let otherText = '';
for (const key in contact.other) {
const prop = contact.other[key];
if (Array.isArray(prop)) {
prop.forEach(pItem => {
otherText += `${key}: ${pItem.value} (${JSON.stringify(pItem.params)})\n`;
});
} else {
otherText += `${key}: ${prop.value} (${JSON.stringify(prop.params)})\n`;
}
}
otherCell.textContent = otherText.trim() || 'N/A';
otherCell.style.whiteSpace = 'pre-wrap'; // To respect newlines
}); });
} }