diff --git a/bookmarklet.js b/bookmarklet.js index de88d94..620fb70 100644 --- a/bookmarklet.js +++ b/bookmarklet.js @@ -1,16 +1,36 @@ (function() { - let pageType, template; + const reportUrl = window.location.pathname.replace('/sedi/', ''); + + let reportType; + + switch (reportUrl) { + case 'SVTIIBIviewResults': + reportType = 'insiderByIssuer'; + break; + case 'SVTItdSelectInsider': + case 'SVTItdSelectIssuer': + reportType = 'insiderTransactionDetail'; + break; + } + + // If format doesn't match either page, we're done + if (!reportType) return; + + let pageType, + template, + hasRemarks = false; - const issuerNameRegex = /^Issuer name:.*/, - insiderNameRegex = /^Insider name:.*/, - insiderRelationshipRegex = /^Insider's Relationship to Issuer:.*/, - ceasedToBeInsiderRegex = /^Ceased to be Insider:.*/, - securityDesignationRegex = /^Security designation:.*/, + const issuerNameRegex = /^Issuer name:.*/i, + insiderNameRegex = /^Insider name:.*/i, + insiderRelationshipRegex = /^Insider's Relationship to Issuer:|Insider Relationship:.*/i, + ceasedToBeInsiderRegex = /^Ceased to be Insider:.*/i, + securityDesignationRegex = /^Security designation:.*/i, endRegex = /^To download this information.*/, hasRemarksRegex = /^Do you want to view transactions.*/, generalRemarksRegex = /^General remarks:.*/, - transactionIdRegex = /^Transaction ID.*/; + transactionIdRegex = /^Transaction ID.*/, + lastReportedTransactionRegex = /^Date of Last ReportedTransaction.*/; const ignoreRows = [ "Legend: O - Original transaction, A - First amendment to transaction, A' - Second amendment to transaction, AP - Amendment to paper filing, etc.", @@ -20,44 +40,60 @@ // "Transaction ID Date of transactionYYYY-MM-DD Date of filingYYYY-MM-DD Ownership type (and registered holder, if applicable) Nature of transaction Number or value acquired or disposed of Unit price or exercise price Closing balance Insider's calculated balance Conversionor exerciseprice Date of expiry or maturityYYYY-MM-DD Underlying security designation Equivalent number or value of underlying securities acquired or disposed of Closing balance of equivalent number or value of underlying securities" ]; - const header = [ - 'Issuer name', - 'Insider name', - 'Insider\'s Relationship to Issuer', - 'Ceased to be Insider', - 'Security designation', - 'Transaction type', - // 'Transaction ID', - // 'Date of transaction', - // 'Date of filing', - // 'Ownership type', - // 'Nature of transaction', - // 'Number or value acquired or disposed of', - // 'Unit price or exercise price', - // 'Unit currency, if not CAD', - // 'Closing balance', - // 'Insider\'s calculated balance', - // 'Conversion or exercise price', - // 'Date of expiry or maturity', - // 'Underlying security designation', - // 'Equivalent number or value of underlying securities acquired or disposed of', - // 'Closing balance of equivalent number or value of underlying securities', - ]; + const header = []; + + if (reportType === 'insiderByIssuer') { + header.push( + 'Issuer name', + 'Insider name', + 'Insider Relationship', + 'Ceased to be Insider', + // 'Date of last reported transaction', + // 'Security designation', + // 'Registered holder', + // 'Closing balance', + // 'Insider\'s calculated balance', + // 'Closing balance of equivalent number or value of underlying securities' + ); + } - // if it has remarks on, add to header and flip flag + if (reportType === 'insiderTransactionDetail') { + header.push( + 'Issuer name', + 'Insider name', + 'Insider\'s Relationship to Issuer', + 'Ceased to be Insider', + 'Security designation', + 'Transaction type', + // 'Transaction ID', + // 'Date of transaction', + // 'Date of filing', + // 'Ownership type', + // 'Nature of transaction', + // 'Number or value acquired or disposed of', + // 'Unit price or exercise price', + // 'Unit currency, if not CAD', + // 'Closing balance', + // 'Insider\'s calculated balance', + // 'Conversion or exercise price', + // 'Date of expiry or maturity', + // 'Underlying security designation', + // 'Equivalent number or value of underlying securities acquired or disposed of', + // 'Closing balance of equivalent number or value of underlying securities', + ); + } - let hasRemarks = false; + const defaultHeaderColumns = header.length; const finalData = []; finalData.push(header); const anchor = document.createElement('a'); - anchor.setAttribute('href', `data:text/csv;charset=utf-8,${encodeURIComponent(sediBookmarklet(document))}`); anchor.setAttribute('download', constructFilename()); - anchor.click(); + anchor.remove(); function jsonToCSV(objArray, config) { const defaults = { @@ -92,22 +128,25 @@ function sediBookmarklet() { + // grab the third anchor tag with a name attribute, which always appearss + // right before Insider or Issuer details near the top of the page const indicatorAName = Array.from(document.querySelectorAll('a')).filter(d => d.hasAttribute('name'))[2]; + // use that to determine the page type, either issuer or insider const pageTypeEl = document .querySelector(`a[name='${indicatorAName.name}'] ~ table ~ table > tbody > tr > td:nth-child(1) font`); - if (pageTypeEl === null) { + if (pageTypeEl === null || reportType === 'insiderByIssuer') { pageType = 'issuer'; } else { pageType = pageTypeEl .textContent + .toLowerCase() .replace('name:', '') - .trim() - .toLowerCase(); + .trim(); } - // if "insider"-mode report, flip header order + // if "insider"-mode report, flip header order so that insider starts first, then issuer if (pageType === 'insider') { const temp = header[0]; header[0] = header[1]; @@ -127,13 +166,18 @@ // Handle headers and determine when to start and end each table lookup data.map((d, i) => { - if (transactionIdRegex.test(d.textContent.trim())) { + const firstColumnRegex = reportType === 'insiderByIssuer' ? + lastReportedTransactionRegex : + transactionIdRegex; + + if (firstColumnRegex.test(d.textContent.trim())) { // construct our headers const headerValues = Array.from(d.querySelectorAll('td')) - .map(d => d.textContent.trim().replace('YYYY-MM-DD', '')) + .map(d => d.innerText.replaceAll('\n', ' ').replaceAll('(YYYY-MM-DD)', '').replaceAll('YYYY-MM-DD', '').trim()) .filter(d => d !== ''); + const unitPriceIdx = headerValues.indexOf('Unit price or exercise price'); - headerValues.splice(unitPriceIdx + 1, 0, 'Unit currency, if not CAD'); + if (unitPriceIdx !== -1) headerValues.splice(unitPriceIdx + 1, 0, 'Unit currency, if not CAD'); header.splice(header.length, 0, ...headerValues); } if (firstOrderNameRegex.test(d.textContent.trim())) startIndices.push(i); @@ -203,7 +247,6 @@ }); // tables that start with the gray block - const grayTables = grayTableStartIndices .map((d, i) => { if (i === grayTableStartIndices.length - 1) { @@ -253,37 +296,43 @@ } }); - const secDesTableStartIndices = []; + const whiteTableStartIndices = []; const grayTable = grayTableData .filter((row, i) => grayTableRemoveIndices.indexOf(i) === -1); - grayTable.map((row, i) => { - const str = row.textContent.trim().replace(/\s+/g, ' '), - strClean = str.replace(/.+:\s/, ''); + // for insiderTransactionDetails pages, tables that start with "security designation" bit + // for insiderByIssuer pages, detail tables that come after gray boxes + let whiteTables; + + // if we're using insiderByIssuer, we can just take full grayTable + if (reportType === 'insiderByIssuer') { + whiteTables = [grayTable]; + } else { + grayTable.map((row, i) => { + const str = row.textContent.trim().replace(/\s+/g, ' '); switch (true) { case securityDesignationRegex.test(str): - secDesTableStartIndices.push(i); + whiteTableStartIndices.push(i); break; } }); - // tables that start with "security designation" bit - - const secDesTables = secDesTableStartIndices - .map((d, i) => { - if (i === secDesTableStartIndices.length - 1) { - return grayTable.slice(d, grayTable.length); - } else { - return grayTable.slice(d, secDesTableStartIndices[i + 1]); - } - }); + whiteTables = whiteTableStartIndices + .map((d, i) => { + if (i === whiteTableStartIndices.length - 1) { + return grayTable.slice(d, grayTable.length); + } else { + return grayTable.slice(d, whiteTableStartIndices[i + 1]); + } + }); + } const issuerName = pageType === 'issuer' ? firstOrderName : secondOrderName, insiderName = pageType === 'issuer' ? secondOrderName : firstOrderName; - secDesTables.map(d => { - extractSecurityDescriptionTable(d, { + whiteTables.map(d => { + extractWhiteTable(d, { issuerName, insiderName, insiderRelationship, @@ -293,7 +342,7 @@ } - function extractSecurityDescriptionTable(secDesTableData, params) { + function extractWhiteTable(whiteTableData, params) { const issuerName = params.issuerName, insiderName = params.insiderName, @@ -302,21 +351,21 @@ let securityDesignation; - const secDesTableRemoveIndices = []; + const whiteTableRemoveIndices = []; - secDesTableData + whiteTableData .map((row, i) => { const str = row.textContent.trim().replace(/\s+/g, ' '), strClean = str.replace(/.+:\s/, ''); switch (true) { case securityDesignationRegex.test(str): securityDesignation = strClean; - secDesTableRemoveIndices.push(i); + whiteTableRemoveIndices.push(i); break; } }); - const generalRemarks = secDesTableData + const generalRemarks = whiteTableData .map(row => { const str = row.textContent.trim().replace(/\s+/g, ' '), strClean = str.replace(/.+:/, '').trim(); @@ -327,8 +376,8 @@ }) .filter(d => d !== undefined); - secDesTable = secDesTableData - .filter((row, i) => secDesTableRemoveIndices.indexOf(i) === -1) + whiteTable = whiteTableData + .filter((row, i) => whiteTableRemoveIndices.indexOf(i) === -1) .filter(row => !generalRemarksRegex.test(row.textContent.trim().replace(/\s+/g, ' '))) .map((row, i) => { @@ -338,23 +387,28 @@ .map(d => d.textContent.trim()); // if "insider"-mode report, flip row data order - rowData[0] = pageType === 'issuer' ? issuerName : insiderName; rowData[1] = pageType === 'issuer' ? insiderName : issuerName; rowData[2] = insiderRelationship; rowData[3] = ceasedToBeInsider; - rowData[4] = securityDesignation; + + if (securityDesignation) rowData[4] = securityDesignation; let tdSkip = 0; - for (var j = 5; j < rowData.length; j++) { - // td[3], td[15] and td[20] are seemingly always empty, likely spacers, - // so let's skip them and adjust the count accordingly - if ([6, 17, 21].indexOf(j) > -1) tdSkip++; + let loopStartIndex = defaultHeaderColumns - 1; + + // table structure is different for whatever reason here + if (reportType === 'insiderByIssuer') loopStartIndex++; + + for (var j = loopStartIndex; j < rowData.length; j++) { + // for insiderTransactionDetail pages, td[3], td[15] and td[20] are seemingly always + // empty, likely spacers, so let's skip them and adjust the count accordingly + if (reportType !== 'insiderByIssuer' && [6, 17, 21].indexOf(j) > -1) tdSkip++; const tdIndex = j - 3 + tdSkip; rowData[j] = td[tdIndex] - // console.log(`rowData[${i}] equals td[${tdIndex}]`); + // console.log(`rowData[${j}] equals td[${tdIndex}]`); } if (hasRemarks) rowData[rowData.length - 1] = generalRemarks[i]; // adds remarks if applicable @@ -368,12 +422,40 @@ } function extractDateRange() { - const monthFrom = (parseInt(document.querySelector('input[name="MONTH_FROM_PUBLIC"]').value) + 1).toString(), - dayFrom = document.querySelector('input[name="DAY_FROM_PUBLIC"]').value, - yearFrom = document.querySelector('input[name="YEAR_FROM_PUBLIC"]').value, - monthTo = (parseInt(document.querySelector('input[name="MONTH_TO_PUBLIC"]').value) + 1).toString(), - dayTo = document.querySelector('input[name="DAY_TO_PUBLIC"]').value, - yearTo = document.querySelector('input[name="YEAR_TO_PUBLIC"]').value; + + let yearFromField, + monthFromField, + dayFromField, + yearToField, + monthToField, + dayToField; + + if (reportType === 'insiderTransactionDetail') { + yearFromField = 'YEAR_FROM_PUBLIC'; + monthFromField = 'MONTH_FROM_PUBLIC'; + dayFromField = 'DAY_FROM_PUBLIC'; + yearToField = 'YEAR_TO_PUBLIC'; + monthToField = 'MONTH_TO_PUBLIC'; + dayToField = 'DAY_TO_PUBLIC'; + } + + if (reportType === 'insiderByIssuer') { + yearFromField = 'FROM_YEAR'; + monthFromField = 'FROM_MONTH'; + dayFromField = 'FROM_DAY'; + yearToField = 'TO_YEAR'; + monthToField = 'TO_MONTH'; + dayToField = 'TO_DAY'; + } + + if (!(yearFromField && monthFromField && dayFromField && yearToField && monthToField && dayToField)) return; + + const monthFrom = (parseInt(document.querySelector(`input[name="${monthFromField}"]`).value) + 1).toString(), + dayFrom = document.querySelector(`input[name="${dayFromField}"]`).value, + yearFrom = document.querySelector(`input[name="${yearFromField}"]`).value, + monthTo = (parseInt(document.querySelector(`input[name="${monthToField}"]`).value) + 1).toString(), + dayTo = document.querySelector(`input[name="${dayToField}"]`).value, + yearTo = document.querySelector(`input[name="${yearToField}"]`).value; const fromDate = `${yearFrom}${monthFrom.padStart(2, '0')}${dayFrom.padStart(2, '0')}`, toDate = `${yearTo}${monthTo.padStart(2, '0')}${dayTo.padStart(2, '0')}`; @@ -382,34 +464,49 @@ } function extractDateRangeType() { + if (reportType === 'insiderByIssuer') return; const dateRangeType = document.querySelector('input[name="DATE_RANGE_TYPE"]'); return dateRangeType.value == 0 ? 'transactions' : 'filings'; } function constructFilename() { - const sediName = document.querySelector('body > table:nth-child(2) > tbody > tr:nth-child(3) > td > table > tbody > tr > td > table:nth-child(15) > tbody > tr > td:nth-child(2)').textContent.trim().replace(/[^a-z0-9]/gi, ''); // need to sanitize + + let sediNameEl; + + if (reportType === 'insiderByIssuer') { + sediNameEl = 'body > table:nth-child(2) > tbody > tr:nth-child(3) > td > table > tbody > tr > td > table:nth-child(17) > tbody > tr > td:nth-child(2)'; + } else if (reportType === 'insiderTransactionDetail') { + sediNameEl = 'body > table:nth-child(2) > tbody > tr:nth-child(3) > td > table > tbody > tr > td > table:nth-child(15) > tbody > tr > td:nth-child(2)'; + } + + const sediName = document.querySelector(sediNameEl).textContent.trim().replace(/[^a-z0-9]/gi, ''); // need to sanitize const sediNumber = document.querySelector('input[name="ATTRIB_DRILL_ID"]').value; - let filenamePrefix = ''; - if (sediName) filenamePrefix = `${sediName}_` - if (sediNumber) filenamePrefix = `${filenamePrefix}${sediNumber}_`; + const sediFilename = []; - const dateRangeType = document.querySelector('input[name="DATE_RANGE_TYPE"]'); + if (sediName) sediFilename.push(sediName); + if (sediNumber) sediFilename.push(sediNumber); - let filenameSuffix; + const dateRangeType = reportType === 'insiderTransactionDetail' ? + document.querySelector('input[name="DATE_RANGE_TYPE"]') : + document.querySelector('body > table:nth-child(2) > tbody > tr:nth-child(3) > td > table > tbody > tr > td > table:nth-child(10)').innerText.trim().includes('Date range :'); if (dateRangeType === null) { const currDate = new Date(), offset = currDate.getTimezoneOffset(), offsetDate = new Date(currDate.getTime() - (offset * 60 * 1000)), cleanedUpDate = offsetDate.toISOString().replace(/[-T:]/g, '').replace(/\..+/, ''); - filenameSuffix = `as_of_${cleanedUpDate}`; + sediFilename.push(`as_of_${cleanedUpDate}`); } else { - filenameSuffix = `${extractDateRangeType()}_${extractDateRange()}`; + const extractedDateRangeType = extractDateRangeType(), + extractedDateRange = extractDateRange(); + + if (extractedDateRangeType) sediFilename.push(extractedDateRangeType); + if (extractedDateRange) sediFilename.push(extractedDateRange); } - return `sedi_${filenamePrefix}${filenameSuffix}.csv`; + return `sedi_${sediFilename.join('_')}.csv`; } })();