diff --git a/.gitignore b/.gitignore index e3355e4..0ca899d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ - +*.DS_Store +*.idea dataset .venv job_posting.csv @@ -8,6 +9,4 @@ __pycache__/ .csv app.py parsed_resume.json -admin-scripts/bronze_jobs.json -admin-scripts/gold_jobs.json -admin-scripts/silver_jobs.json +admin-scripts* diff --git a/web/csv-lab/csv-lab-client.js b/web/csv-lab/csv-lab-client.js new file mode 100644 index 0000000..08b7d7f --- /dev/null +++ b/web/csv-lab/csv-lab-client.js @@ -0,0 +1,181 @@ +/** + * CSV Lab UI: registers the scoped service worker (HTTPS or localhost only). + * First install may require one reload so the page is controlled — see sessionStorage guard. + */ +(function () { + const swUrl = new URL('sw.js', window.location.href); + const scopeUrl = new URL('./', window.location.href).href; + + const els = { + file: document.getElementById('csvFile'), + tableWrap: document.getElementById('tableWrap'), + meta: document.getElementById('meta'), + prompt: document.getElementById('sqlPrompt'), + error: document.getElementById('sqlError'), + swStatus: document.getElementById('swStatus'), + }; + + els.file.disabled = true; + + function setError(msg) { + els.error.textContent = msg || ''; + els.error.hidden = !msg; + } + + function setSwStatus(text) { + els.swStatus.textContent = text; + } + + function renderTable(data) { + const { columns, rows, rowCount, truncated } = data; + const total = rowCount != null ? rowCount : rows.length; + const truncNote = truncated ? ' (showing first rows only; full set is in the worker)' : ''; + els.meta.textContent = `${total} row(s), ${columns.length} column(s)${truncNote}`; + + if (!columns.length) { + els.tableWrap.innerHTML = '

No columns in CSV.

'; + return; + } + + const thead = `${columns.map((c) => `${escapeHtml(c)}`).join('')}`; + const tbody = `${rows + .map( + (row) => + `${columns.map((c) => `${escapeHtml(row[c] ?? '')}`).join('')}` + ) + .join('')}`; + els.tableWrap.innerHTML = `${thead}${tbody}
`; + } + + function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function sendMessage(type, payload) { + return new Promise((resolve, reject) => { + const controller = navigator.serviceWorker.controller; + if (!controller) { + reject(new Error('Service worker is not controlling this page yet. Reload once and try again.')); + return; + } + const id = crypto.randomUUID(); + const onMessage = (ev) => { + const d = ev.data; + if (!d || d.id !== id) { + return; + } + navigator.serviceWorker.removeEventListener('message', onMessage); + if (d.ok) { + resolve(d.data); + } else { + reject(new Error(d.error || 'Unknown error')); + } + }; + navigator.serviceWorker.addEventListener('message', onMessage); + controller.postMessage({ id, type, payload }); + }); + } + + async function loadCsv(text) { + setError(''); + const data = await sendMessage('LOAD_CSV', { csvText: text }); + renderTable(data); + } + + async function runQuery(sql) { + setError(''); + const data = await sendMessage('QUERY', { sql }); + renderTable(data); + } + + async function resetView() { + setError(''); + const data = await sendMessage('RESET', {}); + renderTable(data); + } + + async function initSw() { + if (!('serviceWorker' in navigator)) { + setSwStatus('Service workers are not available in this browser.'); + return false; + } + if (window.location.protocol === 'file:') { + setSwStatus('Open this page over http://localhost (not file://) so the service worker can register.'); + return false; + } + + try { + await navigator.serviceWorker.register(swUrl.href, { scope: scopeUrl }); + } catch (e) { + setSwStatus(`Registration failed: ${e.message || e}`); + return false; + } + + if (!navigator.serviceWorker.controller) { + if (!sessionStorage.getItem('csv-lab-sw-activated')) { + sessionStorage.setItem('csv-lab-sw-activated', '1'); + setSwStatus('Activating worker… reloading once.'); + window.location.reload(); + return false; + } + setSwStatus('No controlling service worker. Try a hard refresh.'); + return false; + } + + setSwStatus('Service worker active — CSV parsing and queries run in the worker.'); + return true; + } + + els.file.addEventListener('change', async () => { + const f = els.file.files && els.file.files[0]; + if (!f) { + return; + } + if (!navigator.serviceWorker.controller) { + setError('Service worker not ready.'); + return; + } + const text = await f.text(); + try { + await loadCsv(text); + } catch (e) { + setError(e.message || String(e)); + } + }); + + els.prompt.addEventListener('keydown', async (ev) => { + if (ev.key !== 'Enter') { + return; + } + ev.preventDefault(); + if (!navigator.serviceWorker.controller) { + setError('Service worker not ready.'); + return; + } + const raw = els.prompt.value.trim(); + try { + if (!raw || /^reset$/i.test(raw)) { + await resetView(); + if (/^reset$/i.test(raw)) { + els.prompt.value = ''; + } + return; + } + await runQuery(raw); + } catch (e) { + setError(e.message || String(e)); + } + }); + + initSw().then((ok) => { + els.file.disabled = !ok; + if (ok) { + els.tableWrap.innerHTML = + '

Choose a .csv file. Then use the prompt, e.g. SELECT DISTINCT(JOBS) WHERE location = \'US\'

'; + } + }); +})(); diff --git a/web/csv-lab/index.html b/web/csv-lab/index.html new file mode 100644 index 0000000..b857a0e --- /dev/null +++ b/web/csv-lab/index.html @@ -0,0 +1,168 @@ + + + + + + CSV Lab — in-browser worker SQL + + + +
+ ← MeWannaJob home +

CSV Lab

+

+ Load a CSV: parsing and queries run in a scoped service worker (not on a server). + Micro-SQL: SELECT *, SELECT col1, col2, + SELECT DISTINCT col or SELECT DISTINCT (col), + optional WHERE col = 'value' AND …. Type RESET or leave empty + Enter to show the loaded preview again. +

+ +
+ + +
+
+
+ +
+
+ + +
+ +
+ +

+
+ + + diff --git a/web/csv-lab/micro-sql.js b/web/csv-lab/micro-sql.js new file mode 100644 index 0000000..d5132b2 --- /dev/null +++ b/web/csv-lab/micro-sql.js @@ -0,0 +1,228 @@ +/* global self */ +// Micro-SQL parser and executor (service worker global scope via importScripts). + +function tokenizeMicroSql(input) { + const tokens = []; + let i = 0; + const keywords = new Set(['SELECT', 'DISTINCT', 'WHERE', 'AND', 'FROM']); + + while (i < input.length) { + const c = input[i]; + if (/\s/.test(c)) { + i += 1; + continue; + } + if (c === '*') { + tokens.push({ type: '*' }); + i += 1; + continue; + } + if (c === ',') { + tokens.push({ type: ',' }); + i += 1; + continue; + } + if (c === '(') { + tokens.push({ type: '(' }); + i += 1; + continue; + } + if (c === ')') { + tokens.push({ type: ')' }); + i += 1; + continue; + } + if (c === '=') { + tokens.push({ type: '=' }); + i += 1; + continue; + } + if (c === "'") { + let j = i + 1; + let str = ''; + while (j < input.length) { + if (input[j] === "'" && input[j + 1] === "'") { + str += "'"; + j += 2; + continue; + } + if (input[j] === "'") { + j += 1; + break; + } + str += input[j]; + j += 1; + } + if (j > input.length || input[j - 1] !== "'") { + throw new Error('Unterminated string literal'); + } + tokens.push({ type: 'STRING', value: str }); + i = j; + continue; + } + if (/[a-zA-Z_]/.test(c)) { + let j = i; + while (j < input.length && /[a-zA-Z0-9_]/.test(input[j])) { + j += 1; + } + const word = input.slice(i, j); + const upper = word.toUpperCase(); + if (keywords.has(upper)) { + tokens.push({ type: 'KW', value: upper }); + } else { + tokens.push({ type: 'IDENT', value: word }); + } + i = j; + continue; + } + throw new Error(`Unexpected character "${c}" at position ${i}`); + } + return tokens; +} + +function parseMicroSql(sql) { + const trimmed = sql.trim().replace(/;+\s*$/, ''); + if (!trimmed) { + throw new Error('Empty query'); + } + const tokens = tokenizeMicroSql(trimmed); + let pos = 0; + + function peek() { + return tokens[pos]; + } + + function take() { + if (pos >= tokens.length) { + throw new Error('Unexpected end of input'); + } + return tokens[pos++]; + } + + function expectKw(w) { + const t = take(); + if (!t || t.type !== 'KW' || t.value !== w) { + throw new Error(`Expected keyword ${w}`); + } + } + + expectKw('SELECT'); + + let distinct = false; + if (peek() && peek().type === 'KW' && peek().value === 'DISTINCT') { + take(); + distinct = true; + } + + let selectCols; + if (peek() && peek().type === '*') { + take(); + selectCols = null; + } else { + selectCols = []; + while (true) { + if (peek() && peek().type === '(') { + take(); + const id = take(); + if (!id || id.type !== 'IDENT') { + throw new Error('Expected column name inside parentheses'); + } + selectCols.push(id.value); + const close = take(); + if (!close || close.type !== ')') { + throw new Error('Expected ) after column name'); + } + } else { + const id = take(); + if (!id || id.type !== 'IDENT') { + throw new Error('Expected column name or *'); + } + selectCols.push(id.value); + } + if (!peek() || peek().type !== ',') { + break; + } + take(); + } + } + + if (peek() && peek().type === 'KW' && peek().value === 'FROM') { + throw new Error('FROM is not supported; the CSV is the only table'); + } + + const conditions = []; + if (peek() && peek().type === 'KW' && peek().value === 'WHERE') { + take(); + while (true) { + const colTok = take(); + if (!colTok || colTok.type !== 'IDENT') { + throw new Error('Expected column name in WHERE'); + } + const eq = take(); + if (!eq || eq.type !== '=') { + throw new Error('Expected = in WHERE clause'); + } + const lit = take(); + if (!lit || lit.type !== 'STRING') { + throw new Error("Expected quoted string after = (e.g. 'US')"); + } + conditions.push({ col: colTok.value, value: lit.value }); + if (!peek() || peek().type !== 'KW' || peek().value !== 'AND') { + break; + } + take(); + } + } + + if (peek()) { + throw new Error(`Unexpected token after query: ${JSON.stringify(peek())}`); + } + + return { distinct, selectCols, conditions }; +} + +function executeMicroSql(ast, table) { + const { distinct, selectCols, conditions } = ast; + const colSet = new Set(table.columns); + + for (const w of conditions) { + if (!colSet.has(w.col)) { + throw new Error(`Unknown column in WHERE: ${w.col}`); + } + } + + const filtered = table.rows.filter((row) => + conditions.every((w) => (row[w.col] ?? '') === w.value) + ); + + const cols = + selectCols === null ? table.columns.slice() : selectCols.slice(); + + for (const c of cols) { + if (!colSet.has(c)) { + throw new Error(`Unknown column in SELECT: ${c}`); + } + } + + let out = filtered.map((row) => { + const o = {}; + for (const c of cols) { + o[c] = row[c]; + } + return o; + }); + + if (distinct) { + const seen = new Set(); + out = out.filter((o) => { + const key = cols.map((c) => o[c]).join('\0'); + if (seen.has(key)) { + return false; + } + seen.add(key); + return true; + }); + } + + return { columns: cols, rows: out }; +} diff --git a/web/csv-lab/sw.js b/web/csv-lab/sw.js new file mode 100644 index 0000000..2bfc8d8 --- /dev/null +++ b/web/csv-lab/sw.js @@ -0,0 +1,106 @@ +/* global Papa */ +/* importScripts loads Papa and micro-sql into global scope */ +importScripts('./vendor/papaparse.min.js', './micro-sql.js'); + +const MAX_PREVIEW = 500; +const MAX_QUERY_ROWS = 5000; + +/** @type {{ columns: string[], rows: Record[] } | null} */ +let table = null; + +self.addEventListener('install', () => { + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(self.clients.claim()); +}); + +function normalizeTable(parsed) { + const columns = parsed.meta.fields || []; + const rows = (parsed.data || []).map((row) => { + const o = {}; + for (const c of columns) { + o[c] = row[c] == null ? '' : String(row[c]); + } + return o; + }); + return { columns, rows }; +} + +function reply(source, id, ok, data, error) { + if (source && typeof source.postMessage === 'function') { + source.postMessage({ id, ok, data, error }); + } +} + +self.addEventListener('message', (event) => { + const { id, type, payload } = event.data || {}; + if (!id || !type) { + return; + } + const src = event.source; + + try { + if (type === 'LOAD_CSV') { + const csvText = payload && payload.csvText; + if (typeof csvText !== 'string') { + throw new Error('Missing csvText'); + } + const parsed = Papa.parse(csvText, { + header: true, + skipEmptyLines: true, + dynamicTyping: false, + }); + const quoteErr = (parsed.errors || []).find((e) => e && e.type === 'Quotes'); + if (quoteErr) { + throw new Error(quoteErr.message || 'CSV parse error'); + } + table = normalizeTable(parsed); + const preview = table.rows.slice(0, MAX_PREVIEW); + reply(src, id, true, { + columns: table.columns, + rowCount: table.rows.length, + rows: preview, + truncated: table.rows.length > MAX_PREVIEW, + }); + return; + } + + if (type === 'RESET') { + if (!table) { + throw new Error('No CSV loaded'); + } + const preview = table.rows.slice(0, MAX_PREVIEW); + reply(src, id, true, { + columns: table.columns, + rowCount: table.rows.length, + rows: preview, + truncated: table.rows.length > MAX_PREVIEW, + }); + return; + } + + if (type === 'QUERY') { + if (!table) { + throw new Error('No CSV loaded'); + } + const sql = (payload && payload.sql) || ''; + const ast = parseMicroSql(sql); + const result = executeMicroSql(ast, table); + const truncated = result.rows.length > MAX_QUERY_ROWS; + reply(src, id, true, { + columns: result.columns, + rows: result.rows.slice(0, MAX_QUERY_ROWS), + rowCount: result.rows.length, + truncated, + }); + return; + } + + reply(src, id, false, null, `Unknown message type: ${type}`); + } catch (e) { + const msg = e && e.message ? e.message : String(e); + reply(src, id, false, null, msg); + } +}); diff --git a/web/csv-lab/vendor/papaparse.min.js b/web/csv-lab/vendor/papaparse.min.js new file mode 100644 index 0000000..eeaf983 --- /dev/null +++ b/web/csv-lab/vendor/papaparse.min.js @@ -0,0 +1,7 @@ +/* @license +Papa Parse +v5.4.1 +https://github.com/mholt/PapaParse +License: MIT +*/ +!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof module&&"undefined"!=typeof exports?module.exports=t():e.Papa=t()}(this,function s(){"use strict";var f="undefined"!=typeof self?self:"undefined"!=typeof window?window:void 0!==f?f:{};var n=!f.document&&!!f.postMessage,o=f.IS_PAPA_WORKER||!1,a={},u=0,b={parse:function(e,t){var r=(t=t||{}).dynamicTyping||!1;J(r)&&(t.dynamicTypingFunction=r,r={});if(t.dynamicTyping=r,t.transform=!!J(t.transform)&&t.transform,t.worker&&b.WORKERS_SUPPORTED){var i=function(){if(!b.WORKERS_SUPPORTED)return!1;var e=(r=f.URL||f.webkitURL||null,i=s.toString(),b.BLOB_URL||(b.BLOB_URL=r.createObjectURL(new Blob(["var global = (function() { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } return {}; })(); global.IS_PAPA_WORKER=true; ","(",i,")();"],{type:"text/javascript"})))),t=new f.Worker(e);var r,i;return t.onmessage=_,t.id=u++,a[t.id]=t}();return i.userStep=t.step,i.userChunk=t.chunk,i.userComplete=t.complete,i.userError=t.error,t.step=J(t.step),t.chunk=J(t.chunk),t.complete=J(t.complete),t.error=J(t.error),delete t.worker,void i.postMessage({input:e,config:t,workerId:i.id})}var n=null;b.NODE_STREAM_INPUT,"string"==typeof e?(e=function(e){if(65279===e.charCodeAt(0))return e.slice(1);return e}(e),n=t.download?new l(t):new p(t)):!0===e.readable&&J(e.read)&&J(e.on)?n=new g(t):(f.File&&e instanceof File||e instanceof Object)&&(n=new c(t));return n.stream(e)},unparse:function(e,t){var n=!1,_=!0,m=",",y="\r\n",s='"',a=s+s,r=!1,i=null,o=!1;!function(){if("object"!=typeof t)return;"string"!=typeof t.delimiter||b.BAD_DELIMITERS.filter(function(e){return-1!==t.delimiter.indexOf(e)}).length||(m=t.delimiter);("boolean"==typeof t.quotes||"function"==typeof t.quotes||Array.isArray(t.quotes))&&(n=t.quotes);"boolean"!=typeof t.skipEmptyLines&&"string"!=typeof t.skipEmptyLines||(r=t.skipEmptyLines);"string"==typeof t.newline&&(y=t.newline);"string"==typeof t.quoteChar&&(s=t.quoteChar);"boolean"==typeof t.header&&(_=t.header);if(Array.isArray(t.columns)){if(0===t.columns.length)throw new Error("Option columns is empty");i=t.columns}void 0!==t.escapeChar&&(a=t.escapeChar+s);("boolean"==typeof t.escapeFormulae||t.escapeFormulae instanceof RegExp)&&(o=t.escapeFormulae instanceof RegExp?t.escapeFormulae:/^[=+\-@\t\r].*$/)}();var u=new RegExp(Q(s),"g");"string"==typeof e&&(e=JSON.parse(e));if(Array.isArray(e)){if(!e.length||Array.isArray(e[0]))return h(null,e,r);if("object"==typeof e[0])return h(i||Object.keys(e[0]),e,r)}else if("object"==typeof e)return"string"==typeof e.data&&(e.data=JSON.parse(e.data)),Array.isArray(e.data)&&(e.fields||(e.fields=e.meta&&e.meta.fields||i),e.fields||(e.fields=Array.isArray(e.data[0])?e.fields:"object"==typeof e.data[0]?Object.keys(e.data[0]):[]),Array.isArray(e.data[0])||"object"==typeof e.data[0]||(e.data=[e.data])),h(e.fields||[],e.data||[],r);throw new Error("Unable to serialize unrecognized input");function h(e,t,r){var i="";"string"==typeof e&&(e=JSON.parse(e)),"string"==typeof t&&(t=JSON.parse(t));var n=Array.isArray(e)&&0=this._config.preview;if(o)f.postMessage({results:n,workerId:b.WORKER_ID,finished:a});else if(J(this._config.chunk)&&!t){if(this._config.chunk(n,this._handle),this._handle.paused()||this._handle.aborted())return void(this._halted=!0);n=void 0,this._completeResults=void 0}return this._config.step||this._config.chunk||(this._completeResults.data=this._completeResults.data.concat(n.data),this._completeResults.errors=this._completeResults.errors.concat(n.errors),this._completeResults.meta=n.meta),this._completed||!a||!J(this._config.complete)||n&&n.meta.aborted||(this._config.complete(this._completeResults,this._input),this._completed=!0),a||n&&n.meta.paused||this._nextChunk(),n}this._halted=!0},this._sendError=function(e){J(this._config.error)?this._config.error(e):o&&this._config.error&&f.postMessage({workerId:b.WORKER_ID,error:e,finished:!1})}}function l(e){var i;(e=e||{}).chunkSize||(e.chunkSize=b.RemoteChunkSize),h.call(this,e),this._nextChunk=n?function(){this._readChunk(),this._chunkLoaded()}:function(){this._readChunk()},this.stream=function(e){this._input=e,this._nextChunk()},this._readChunk=function(){if(this._finished)this._chunkLoaded();else{if(i=new XMLHttpRequest,this._config.withCredentials&&(i.withCredentials=this._config.withCredentials),n||(i.onload=v(this._chunkLoaded,this),i.onerror=v(this._chunkError,this)),i.open(this._config.downloadRequestBody?"POST":"GET",this._input,!n),this._config.downloadRequestHeaders){var e=this._config.downloadRequestHeaders;for(var t in e)i.setRequestHeader(t,e[t])}if(this._config.chunkSize){var r=this._start+this._config.chunkSize-1;i.setRequestHeader("Range","bytes="+this._start+"-"+r)}try{i.send(this._config.downloadRequestBody)}catch(e){this._chunkError(e.message)}n&&0===i.status&&this._chunkError()}},this._chunkLoaded=function(){4===i.readyState&&(i.status<200||400<=i.status?this._chunkError():(this._start+=this._config.chunkSize?this._config.chunkSize:i.responseText.length,this._finished=!this._config.chunkSize||this._start>=function(e){var t=e.getResponseHeader("Content-Range");if(null===t)return-1;return parseInt(t.substring(t.lastIndexOf("/")+1))}(i),this.parseChunk(i.responseText)))},this._chunkError=function(e){var t=i.statusText||e;this._sendError(new Error(t))}}function c(e){var i,n;(e=e||{}).chunkSize||(e.chunkSize=b.LocalChunkSize),h.call(this,e);var s="undefined"!=typeof FileReader;this.stream=function(e){this._input=e,n=e.slice||e.webkitSlice||e.mozSlice,s?((i=new FileReader).onload=v(this._chunkLoaded,this),i.onerror=v(this._chunkError,this)):i=new FileReaderSync,this._nextChunk()},this._nextChunk=function(){this._finished||this._config.preview&&!(this._rowCount=this._input.size,this.parseChunk(e.target.result)},this._chunkError=function(){this._sendError(i.error)}}function p(e){var r;h.call(this,e=e||{}),this.stream=function(e){return r=e,this._nextChunk()},this._nextChunk=function(){if(!this._finished){var e,t=this._config.chunkSize;return t?(e=r.substring(0,t),r=r.substring(t)):(e=r,r=""),this._finished=!r,this.parseChunk(e)}}}function g(e){h.call(this,e=e||{});var t=[],r=!0,i=!1;this.pause=function(){h.prototype.pause.apply(this,arguments),this._input.pause()},this.resume=function(){h.prototype.resume.apply(this,arguments),this._input.resume()},this.stream=function(e){this._input=e,this._input.on("data",this._streamData),this._input.on("end",this._streamEnd),this._input.on("error",this._streamError)},this._checkIsFinished=function(){i&&1===t.length&&(this._finished=!0)},this._nextChunk=function(){this._checkIsFinished(),t.length?this.parseChunk(t.shift()):r=!0},this._streamData=v(function(e){try{t.push("string"==typeof e?e:e.toString(this._config.encoding)),r&&(r=!1,this._checkIsFinished(),this.parseChunk(t.shift()))}catch(e){this._streamError(e)}},this),this._streamError=v(function(e){this._streamCleanUp(),this._sendError(e)},this),this._streamEnd=v(function(){this._streamCleanUp(),i=!0,this._streamData("")},this),this._streamCleanUp=v(function(){this._input.removeListener("data",this._streamData),this._input.removeListener("end",this._streamEnd),this._input.removeListener("error",this._streamError)},this)}function r(m){var a,o,u,i=Math.pow(2,53),n=-i,s=/^\s*-?(\d+\.?|\.\d+|\d+\.\d+)([eE][-+]?\d+)?\s*$/,h=/^((\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d\.\d+([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z))|(\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d([+-][0-2]\d:[0-5]\d|Z)))$/,t=this,r=0,f=0,d=!1,e=!1,l=[],c={data:[],errors:[],meta:{}};if(J(m.step)){var p=m.step;m.step=function(e){if(c=e,_())g();else{if(g(),0===c.data.length)return;r+=e.data.length,m.preview&&r>m.preview?o.abort():(c.data=c.data[0],p(c,t))}}}function y(e){return"greedy"===m.skipEmptyLines?""===e.join("").trim():1===e.length&&0===e[0].length}function g(){return c&&u&&(k("Delimiter","UndetectableDelimiter","Unable to auto-detect delimiting character; defaulted to '"+b.DefaultDelimiter+"'"),u=!1),m.skipEmptyLines&&(c.data=c.data.filter(function(e){return!y(e)})),_()&&function(){if(!c)return;function e(e,t){J(m.transformHeader)&&(e=m.transformHeader(e,t)),l.push(e)}if(Array.isArray(c.data[0])){for(var t=0;_()&&t=l.length?"__parsed_extra":l[r]),m.transform&&(s=m.transform(s,n)),s=v(n,s),"__parsed_extra"===n?(i[n]=i[n]||[],i[n].push(s)):i[n]=s}return m.header&&(r>l.length?k("FieldMismatch","TooManyFields","Too many fields: expected "+l.length+" fields but parsed "+r,f+t):r=i.length/2?"\r\n":"\r"}(e,i)),u=!1,m.delimiter)J(m.delimiter)&&(m.delimiter=m.delimiter(e),c.meta.delimiter=m.delimiter);else{var n=function(e,t,r,i,n){var s,a,o,u;n=n||[",","\t","|",";",b.RECORD_SEP,b.UNIT_SEP];for(var h=0;h=N)return L(!0)}else for(S=W,W++;;){if(-1===(S=i.indexOf(z,S+1)))return r||h.push({type:"Quotes",code:"MissingQuotes",message:"Quoted field unterminated",row:u.length,index:W}),T();if(S===n-1)return T(i.substring(W,S).replace(C,z));if(z!==K||i[S+1]!==K){if(z===K||0===S||i[S-1]!==K){-1!==w&&w=N)return L(!0);break}h.push({type:"Quotes",code:"InvalidQuotes",message:"Trailing quote on quoted field is malformed",row:u.length,index:W}),S++}}else S++}return T();function I(e){u.push(e),d=W}function A(e){var t=0;if(-1!==e){var r=i.substring(S+1,e);r&&""===r.trim()&&(t=r.length)}return t}function T(e){return r||(void 0===e&&(e=i.substring(W)),f.push(e),W=n,I(f),o&&F()),L()}function D(e){W=e,I(f),f=[],R=i.indexOf(P,W)}function L(e){return{data:u,errors:h,meta:{delimiter:M,linebreak:P,aborted:H,truncated:!!e,cursor:d+(t||0)}}}function F(){q(L()),u=[],h=[]}},this.abort=function(){H=!0},this.getCharIndex=function(){return W}}function _(e){var t=e.data,r=a[t.workerId],i=!1;if(t.error)r.userError(t.error,t.file);else if(t.results&&t.results.data){var n={abort:function(){i=!0,m(t.workerId,{data:[],errors:[],meta:{aborted:!0}})},pause:y,resume:y};if(J(r.userStep)){for(var s=0;s