more fields better parsing
This commit is contained in:
11
index.html
11
index.html
@ -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">
|
||||||
|
|||||||
174
script.js
174
script.js
@ -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;
|
||||||
|
case 'N':
|
||||||
|
// N:Family;Given;Middle;Prefix;Suffix
|
||||||
|
const nameParts = value.split(';');
|
||||||
|
currentContact.n = {
|
||||||
|
family: nameParts[0] || '',
|
||||||
|
given: nameParts[1] || '',
|
||||||
|
middle: nameParts[2] || '',
|
||||||
|
prefix: nameParts[3] || '',
|
||||||
|
suffix: nameParts[4] || ''
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case 'TEL':
|
||||||
|
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]];
|
||||||
}
|
}
|
||||||
// TEL (Telephone)
|
currentContact.other[mainKey].push({ value: value, params: params });
|
||||||
else if (keyPart.startsWith('TEL')) {
|
} else {
|
||||||
// Simplistic approach: take the first TEL found
|
currentContact.other[mainKey] = { value: value, params: params };
|
||||||
if (!currentContact.tel) currentContact.tel = value;
|
|
||||||
}
|
|
||||||
// EMAIL
|
|
||||||
else if (keyPart.startsWith('EMAIL')) {
|
|
||||||
// Simplistic approach: take the first EMAIL found
|
|
||||||
if (!currentContact.email) currentContact.email = value;
|
|
||||||
}
|
|
||||||
// ORG (Organization)
|
|
||||||
else if (keyPart.startsWith('ORG')) {
|
|
||||||
currentContact.org = value;
|
|
||||||
}
|
}
|
||||||
|
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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user