Browse Source

Implemented the quotes config parameter optionally as a function (#532)

pull/703/head
Puzzleton 6 years ago
parent
commit
88b9d750e3
  1. 2
      .eslintrc.js
  2. 1
      .npmignore
  3. 3
      .travis.yml
  4. 6
      Gruntfile.js
  5. 6
      README.md
  6. 12
      docs/demo.html
  7. 170
      docs/docs.html
  8. 20
      docs/faq.html
  9. 20
      docs/index.html
  10. 15
      docs/resources/css/common.css
  11. 19
      docs/resources/js/lovers.js
  12. 663
      docs/resources/js/papaparse.js
  13. 14
      package.json
  14. 403
      papaparse.js
  15. 12
      papaparse.min.js
  16. 36
      tests/node-tests.js
  17. 533
      tests/test-cases.js
  18. 4
      tests/test.js
  19. 13
      tests/tests.html

2
.eslintrc.js

@ -178,7 +178,7 @@ module.exports = {
"no-tabs": "off", "no-tabs": "off",
"no-template-curly-in-string": "error", "no-template-curly-in-string": "error",
"no-ternary": "off", "no-ternary": "off",
"no-throw-literal": "off", "no-throw-literal": "error",
"no-trailing-spaces": "error", "no-trailing-spaces": "error",
"no-undef-init": "error", "no-undef-init": "error",
"no-undefined": "off", "no-undefined": "off",

1
.npmignore

@ -1,3 +1,4 @@
_gitignore/ _gitignore/
bower_components/ bower_components/
node_modules/ node_modules/
docs/

3
.travis.yml

@ -1,7 +1,6 @@
language: node_js language: node_js
node_js: node_js:
- "6"
- "8" - "8"
- "9" - "9"
- "10" - "10"
- "11"

6
Gruntfile.js

@ -2,6 +2,12 @@ module.exports = function(grunt) {
grunt.initConfig({ grunt.initConfig({
uglify: { uglify: {
options: { options: {
compress: {
global_defs: {
'PAPA_BROWSER_CONTEXT': true
},
dead_code: true
},
output: { output: {
comments: 'some', comments: 'some',
}, },

6
README.md

@ -26,7 +26,7 @@ can be installed with the following command:
npm install papaparse npm install papaparse
If you don't want to use npm, [papaparse.min.js](https://github.com/mholt/PapaParse/blob/master/papaparse.min.js) can be downloaded to your project source. If you don't want to use npm, [papaparse.min.js](https://unpkg.com/papaparse@latest/papaparse.min.js) can be downloaded to your project source.
Homepage & Demo Homepage & Demo
@ -39,9 +39,7 @@ To learn how to use Papa Parse:
- [Documentation](http://papaparse.com/docs) - [Documentation](http://papaparse.com/docs)
The website is hosted on on [Github Pages](https://pages.github.com/). If The website is hosted on [Github Pages](https://pages.github.com/). It's content is also include in the docs folder of this repository. If you want to contribute on it just clone the master of this repository and open a pull request.
you want to contribute just clone the gh-branch of this repository and
open a pull request.
Papa Parse for Node Papa Parse for Node

12
docs/demo.html

@ -12,7 +12,7 @@
<link rel="stylesheet" href="/resources/css/demo.css"> <link rel="stylesheet" href="/resources/css/demo.css">
<script src="/resources/js/jquery.min.js"></script> <script src="/resources/js/jquery.min.js"></script>
<script src="/resources/js/common.js"></script> <script src="/resources/js/common.js"></script>
<script src="/resources/js/papaparse.js"></script> <script src="https://unpkg.com/papaparse@latest/papaparse.min.js"></script>
<script src="/resources/js/demo.js"></script> <script src="/resources/js/demo.js"></script>
</head> </head>
<body> <body>
@ -34,7 +34,7 @@
</div> </div>
</div> </div>
<div class="grid-20 hide-on-mobile text-center"> <div class="grid-20 hide-on-mobile text-center">
<a href="/" class="text-logo">Papa Parse 4</a> <a href="/" class="text-logo">Papa Parse 5</a>
</div> </div>
<div class="grid-40 mobile-grid-50 text-right"> <div class="grid-40 mobile-grid-50 text-right">
<div class="links"> <div class="links">
@ -44,9 +44,6 @@
<a href="http://stackoverflow.com/questions/tagged/papaparse"> <a href="http://stackoverflow.com/questions/tagged/papaparse">
<i class="fa fa-stack-overflow fa-lg"></i> Help <i class="fa fa-stack-overflow fa-lg"></i> Help
</a> </a>
<!-- <a href="https://matt.life/pay" class="donate">
<i class="fa fa-heart fa-lg"></i> Donate
</a> -->
</div> </div>
</div> </div>
</div> </div>
@ -89,6 +86,7 @@
<input type="checkbox" id="skipEmptyLines"> Skip empty lines <input type="checkbox" id="skipEmptyLines"> Skip empty lines
<dfn>By default, empty lines are parsed; check to skip.</dfn> <dfn>By default, empty lines are parsed; check to skip.</dfn>
</label> </label>
</div> </div>
<div class="grid-75 grid-parent"> <div class="grid-75 grid-parent">
@ -223,7 +221,7 @@
<br><br> <br><br>
Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a> Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a>
<br> <br>
&copy; 2013-2018 &copy; 2013-2019
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Learn</h5> <h5>Learn</h5>
@ -233,7 +231,7 @@
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Project</h5> <h5>Project</h5>
<a href="https://gratipay.com/mholt">Donate</a> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=S6VTL9FQ6L8EN&item_name=PapaParse&currency_code=EUR&source=url">Donate</a>
<a href="https://github.com/mholt/PapaParse">GitHub</a> <a href="https://github.com/mholt/PapaParse">GitHub</a>
<a href="https://twitter.com/search?q=%23PapaParse">Share</a> <a href="https://twitter.com/search?q=%23PapaParse">Share</a>
</div> </div>

170
docs/docs.html

@ -34,7 +34,7 @@
</div> </div>
</div> </div>
<div class="grid-20 hide-on-mobile text-center"> <div class="grid-20 hide-on-mobile text-center">
<a href="/" class="text-logo">Papa Parse 4</a> <a href="/" class="text-logo">Papa Parse 5</a>
</div> </div>
<div class="grid-40 mobile-grid-50 text-right"> <div class="grid-40 mobile-grid-50 text-right">
<div class="links"> <div class="links">
@ -44,9 +44,6 @@
<a href="http://stackoverflow.com/questions/tagged/papaparse"> <a href="http://stackoverflow.com/questions/tagged/papaparse">
<i class="fa fa-stack-overflow fa-lg"></i> Help <i class="fa fa-stack-overflow fa-lg"></i> Help
</a> </a>
<!-- <a href="https://matt.life/pay" class="donate">
<i class="fa fa-heart fa-lg"></i> Donate
</a> -->
</div> </div>
</div> </div>
</div> </div>
@ -99,7 +96,7 @@
</div> </div>
<div class="grid-50"> <div class="grid-50">
<pre><code class="language-javascript">Papa.parse(csvString<i>[, <a href="#config">config</a>]</i>)</pre></code> <pre><code class="language-javascript">Papa.parse(csvString<i>[, <a href="#config">config</a>]</i>)</code></pre>
</div> </div>
<div class="grid-50"> <div class="grid-50">
@ -188,7 +185,7 @@
reason: "Some reason", reason: "Some reason",
config: <span class="comment">// altered config...</span> config: <span class="comment">// altered config...</span>
}</code></pre> }</code></pre>
to alter the flow of parsing. Actions can be <code>"abort"</code> to skip this and all other files in the queue, <code>"skip"</code> to skip just this file, or <code>"continue"</code> to carry on (equivalent to returning nothing). <code>reason</code> can be a reason for aborting. <code>config</code> can be a modified <a href="#config">configuration</a> for parsing just this file.</li> to alter the flow of parsing. Actions can be <code>"abort"</code> to skip this and all other files in the queue, <code>"skip"</code> to skip just this file, or <code>"continue"</code> to carry on (equivalent to returning nothing). <code>reason</code> can be a reason for aborting. <code>config</code> can be a modified <a href="#config">configuration</a> for parsing just this file.
</li> </li>
<li>The <code>complete</code> callback shown here is executed after <i>all</i> files are finished and does not receive any data. Use the complete callback in <a href="#config">config</a> for per-file results.</li> <li>The <code>complete</code> callback shown here is executed after <i>all</i> files are finished and does not receive any data. Use the complete callback in <a href="#config">config</a> for per-file results.</li>
</ul> </ul>
@ -228,7 +225,7 @@
<div class="grid-50"> <div class="grid-50">
<pre><code class="language-javascript">Papa.unparse(data<i>[, config]</i>)</code></pre> <pre><code class="language-javascript">Papa.unparse(data<i>[, <a href="#unparse-config-default">config</a>]</i>)</code></pre>
</div> </div>
<div class="grid-50"> <div class="grid-50">
@ -243,19 +240,104 @@
</ul> </ul>
</li> </li>
<li> <li>
<code>config</code> is an optional object with any of these properties: <code>config</code> is an optional <a href="#unparse-config-default">config object</a>
<pre><code class="language-javascript">// defaults shown </li>
</ul>
</div>
<div class="clear"></div>
<div class="grid-100">
<h5 id="unparse-config-default">Default Unparse Config with all options</h5>
</div>
<div class="prefix-25 grid-50 suffix-25">
<pre><code class="language-javascript">
{ {
quotes: false, quotes: false, //or array of booleans
quoteChar: '"', quoteChar: '"',
escapeChar: '"', escapeChar: '"',
delimiter: ",", delimiter: ",",
header: true, header: true,
newline: "\r\n" newline: "\r\n",
}</code></pre> skipEmptyLines: false, //or 'greedy',
Set <code>quotes</code> to <code>true</code> to always enclose each field in quotes, or an array of true/false values correlating to specific to columns to force-quote. The character used to quote can be customized using <code>quoteChar</code>. The character used to escape the <code>quoteChar</code> within a field can be customized using <code>escapeChar</code>. The <code>delimiter</code> can be any valid delimiting character. The <code>newline</code> character(s) may also be customized. Setting <code>header</code> to <code>false</code> will omit the header row. columns: null //or array of strings
</li> }
</ul> </code></pre>
</div>
<div class="clear"></div>
<div class="grid-100">
<h5>Unparse Config Options</h5>
</div>
<div class="grid-100" style="overflow-x: auto;">
<table>
<tr>
<th style="width: 20%;">Option</th>
<th style="width: 80%;">Explanation</th>
</tr>
<tr>
<td>
<code>quotes</code>
</td>
<td>
If <code>true</code>, forces all fields to be enclosed in quotes. If an array of <code>true/false</code> values, specifies which fields should be force-quoted (first boolean is for the first column, second boolean for the second column, ...). If a function (accepting cell value & column index as parameters), returning <code>true</code> will force quotes for that value and <code>false</code> will not.
</td>
</tr>
<tr>
<td><code>quoteChar</code></td>
<td>
The character used to quote fields.
</td>
</tr>
<tr>
<td><code>escapeChar</code></td>
<td>
The character used to escape <code>quoteChar</code> inside field values.
</td>
</tr>
<tr>
<td>
<code>delimiter</code>
</td>
<td>
The delimiting character. It must not be found in <a href="#readonly">Papa.BAD_DELIMITERS</a>.
</td>
</tr>
<tr>
<td>
<code>header</code>
</td>
<td>
If <code>false</code>, will omit the header row. If <code>data</code> is an array of arrays this option is ignored. If <code>data</code> is an array of objects the keys of the first object are the header row. If <code>data</code> is an object with the keys <code>fields</code> and <code>data</code> the <code>fields</code> are the header row.
</td>
</tr>
<tr>
<td>
<code>newline</code>
</td>
<td>
The newline sequence. Must be one of <code>"\r"</code>, <code>"\n"</code>, or <code>"\r\n"</code>.
</td>
</tr>
<tr>
<td>
<code>skipEmptyLines</code>
</td>
<td>
If <code>true</code>, lines that are completely empty (those which evaluate to an empty string) will be skipped. If set to <code>'greedy'</code>, lines that don't have any content (those which have only whitespace after parsing) will also be skipped.
</td>
</tr>
<tr>
<td>
<code>columns</code>
</td>
<td>
If <code>data</code> is an array of objects this option can be used to manually specify the keys (columns) you expect in the objects. If not set the keys of the first objects are used as column.
</td>
</tr>
</table>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
@ -289,8 +371,8 @@ var csv = Papa.unparse([
<div class="grid-33"> <div class="grid-33">
<pre><code class="language-javascript">// Specifying fields and data explicitly <pre><code class="language-javascript">// Specifying fields and data explicitly
var csv = Papa.unparse({ var csv = Papa.unparse({
fields: ["Column 1", "Column 2"], "fields": ["Column 1", "Column 2"],
data: [ "data": [
["foo", "bar"], ["foo", "bar"],
["abc", "def"] ["abc", "def"]
] ]
@ -340,7 +422,7 @@ var csv = Papa.unparse({
quoteChar: '"', quoteChar: '"',
escapeChar: '"', escapeChar: '"',
header: false, header: false,
trimHeaders: false, transformHeader: undefined,
dynamicTyping: false, dynamicTyping: false,
preview: 0, preview: 0,
encoding: "", encoding: "",
@ -350,12 +432,14 @@ var csv = Papa.unparse({
complete: undefined, complete: undefined,
error: undefined, error: undefined,
download: false, download: false,
downloadRequestHeaders: undefined,
skipEmptyLines: false, skipEmptyLines: false,
chunk: undefined, chunk: undefined,
fastMode: undefined, fastMode: undefined,
beforeFirstChunk: undefined, beforeFirstChunk: undefined,
withCredentials: undefined, withCredentials: undefined,
transform: undefined transform: undefined,
delimitersToGuess: [',', '\t', '|', ';', <a href="#readonly">Papa.RECORD_SEP</a>, <a href="#readonly">Papa.UNIT_SEP</a>]
}</code></pre> }</code></pre>
</div> </div>
<div class="clear"></div> <div class="clear"></div>
@ -375,7 +459,7 @@ var csv = Papa.unparse({
<code>delimiter</code> <code>delimiter</code>
</td> </td>
<td> <td>
The delimiting character. Leave blank to auto-detect from a list of most common delimiters. It can be a string or a function. If string, it must be one of length 1. If a function, it must accept the input as first parameter and it must return a string which will be used as delimiter. In both cases it cannot be found in <a href="#readonly">Papa.BAD_DELIMITERS</a>. The delimiting character. Leave blank to auto-detect from a list of most common delimiters, or any values passed in through <code>delimitersToGuess</code>. It can be a string or a function. If string, it must be one of length 1. If a function, it must accept the input as first parameter and it must return a string which will be used as delimiter. In both cases it cannot be found in <a href="#readonly">Papa.BAD_DELIMITERS</a>.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -412,10 +496,11 @@ var csv = Papa.unparse({
</tr> </tr>
<tr> <tr>
<td> <td>
<code>trimHeaders</code> <code>transformHeader</code>
</td> </td>
<td> <td>
If true leading/trailing spaces will be trimed from headers. A function to apply on each header. Requires <code>header</code> to be <code>true</code>. The function receives the header as its first argument.<br>
Only available starting with version 5.0.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -423,7 +508,7 @@ var csv = Papa.unparse({
<code>dynamicTyping</code> <code>dynamicTyping</code>
</td> </td>
<td> <td>
If true, numeric and boolean data will be converted to their type instead of remaining strings. Numeric data must conform to the definition of a decimal literal. European-formatted numbers must have commas and dots swapped. If also accepts an object or a function. If object it's values should be a boolean to indicate if dynamic typing should be applied for each column number (or header name if using headers). If it's a function, it should return a boolean value for each field number (or name if using headers) which will be passed as first argument. If true, numeric and boolean data will be converted to their type instead of remaining strings. Numeric data must conform to the definition of a decimal literal. Numerical values greater than <code>2^53</code> or less than <code>-2^53</code> will not be converted to numbers to preserve precision. European-formatted numbers must have commas and dots swapped. If also accepts an object or a function. If object it's values should be a boolean to indicate if dynamic typing should be applied for each column number (or header name if using headers). If it's a function, it should return a boolean value for each field number (or name if using headers) which will be passed as first argument.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -431,7 +516,7 @@ var csv = Papa.unparse({
<code>preview</code> <code>preview</code>
</td> </td>
<td> <td>
If > 0, only that many rows will be parsed. If &gt; 0, only that many rows will be parsed.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -447,7 +532,7 @@ var csv = Papa.unparse({
<code>worker</code> <code>worker</code>
</td> </td>
<td> <td>
Whether or not to use a <a href="/faq#workers">worker thread</a>. Using a worker will keep your page reactive, but may be slightly slower. Web Workers also load the entire Javascript file, so be careful when <a href="/faq#combine">combining other libraries</a> in the same file as Papa Parse. Note that worker option is only available when parsing files and not when converting from JSON to CSV. Whether or not to use a <a href="/faq#workers">worker thread</a>. Using a worker will keep your page reactive, but may be slightly slower.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -500,12 +585,25 @@ var csv = Papa.unparse({
If true, this indicates that the string you passed as the first argument to <code>parse()</code> is actually a URL from which to download a file and parse its contents. If true, this indicates that the string you passed as the first argument to <code>parse()</code> is actually a URL from which to download a file and parse its contents.
</td> </td>
</tr> </tr>
<tr>
<td>
<code>downloadRequestHeaders</code>
</td>
<td>
If defined, should be an object that describes the headers, example:
<pre>
<code class="language-javascript">downloadRequestHeaders: {
'Authorization': 'token 123345678901234567890',
}</code>
</pre>
</tr>
<tr> <tr>
<td> <td>
<code>skipEmptyLines</code> <code>skipEmptyLines</code>
</td> </td>
<td> <td>
If true, lines that are completely empty will be skipped. An empty line is defined to be one which evaluates to empty string. If true, lines that are completely empty (those which evaluate to an empty string) will be skipped. If set to <code>'greedy'</code>, lines that don't have any content (those which have only whitespace after parsing) will also be skipped.
</td> </td>
</tr> </tr>
<tr> <tr>
@ -545,7 +643,15 @@ var csv = Papa.unparse({
<code>transform</code> <code>transform</code>
</td> </td>
<td> <td>
A function to apply on each value. The function receives the value as its first argument and the column number as its second argument. The return value of the function will replace the value it received. The transform function is applied before dynamicTyping. A function to apply on each value. The function receives the value as its first argument and the column number or header name when enabled as its second argument. The return value of the function will replace the value it received. The transform function is applied before dynamicTyping.
</td>
</tr>
<tr>
<td>
<code>delimitersToGuess</code>
</td>
<td>
An array of delimiters to guess from if the <code>delimiter</code> option is not set.
</td> </td>
</tr> </tr>
</table> </table>
@ -749,12 +855,6 @@ var csv = Papa.unparse({
Whether or not the browser supports HTML5 Web Workers. If false, <code>worker: true</code> will have no effect. Whether or not the browser supports HTML5 Web Workers. If false, <code>worker: true</code> will have no effect.
</td> </td>
</tr> </tr>
<tr>
<td><code>Papa.SCRIPT_PATH</code></td>
<td>
The relative path to Papa Parse. This is automatically detected when Papa Parse is loaded synchronously. However, if you load Papa Parse asynchronously (e.g. with RequireJS), you need to set this variable manually in order to use Web Workers. (In those cases, this variable is <i>not</i> read-only and you should set it!)
</td>
</tr>
</table> </table>
</div> </div>
@ -812,7 +912,7 @@ var csv = Papa.unparse({
<br><br> <br><br>
Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a> Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a>
<br> <br>
&copy; 2013-2018 &copy; 2013-2019
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Learn</h5> <h5>Learn</h5>
@ -822,7 +922,7 @@ var csv = Papa.unparse({
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Project</h5> <h5>Project</h5>
<a href="https://gratipay.com/mholt">Donate</a> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=S6VTL9FQ6L8EN&item_name=PapaParse&currency_code=EUR&source=url">Donate</a>
<a href="https://github.com/mholt/PapaParse">GitHub</a> <a href="https://github.com/mholt/PapaParse">GitHub</a>
<a href="https://twitter.com/search?q=%23PapaParse">Share</a> <a href="https://twitter.com/search?q=%23PapaParse">Share</a>
</div> </div>

20
docs/faq.html

@ -35,7 +35,7 @@
</div> </div>
</div> </div>
<div class="grid-20 hide-on-mobile text-center"> <div class="grid-20 hide-on-mobile text-center">
<a href="/" class="text-logo">Papa Parse 4</a> <a href="/" class="text-logo">Papa Parse 5</a>
</div> </div>
<div class="grid-40 mobile-grid-50 text-right"> <div class="grid-40 mobile-grid-50 text-right">
<div class="links"> <div class="links">
@ -45,9 +45,6 @@
<a href="http://stackoverflow.com/questions/tagged/papaparse"> <a href="http://stackoverflow.com/questions/tagged/papaparse">
<i class="fa fa-stack-overflow fa-lg"></i> Help <i class="fa fa-stack-overflow fa-lg"></i> Help
</a> </a>
<!-- <a href="https://matt.life/pay" class="donate">
<i class="fa fa-heart fa-lg"></i> Donate
</a> -->
</div> </div>
</div> </div>
</div> </div>
@ -84,7 +81,7 @@
<h6 id="combine">Can I put other libraries in the same file as Papa Parse?</h6> <h6 id="combine">Can I put other libraries in the same file as Papa Parse?</h6>
<p> <p>
Yes, but then don't use the Web Worker feature unless your other dependencies are battle-hardened for worker threads. A worker thread loads an entire file, not just a function, so all those dependencies would be executed in an environment without a DOM and other <code>window</code> features. If any of those dependencies crash (<code>Cannot read property "defaultView" of undefined</code> <a href="https://github.com/mholt/PapaParse/issues/114">is</a> <a href="https://github.com/mholt/PapaParse/issues/163">common</a>), the whole worker thread will crash and parsing will not succeed. Yes.
</p> </p>
@ -96,7 +93,7 @@
<h6 id="async">Can Papa Parse be loaded asynchronously (after the page loads)?</h6> <h6 id="async">Can Papa Parse be loaded asynchronously (after the page loads)?</h6>
<p> <p>
Yes. But if you want to use Web Workers, you'll need to specify the relative path to Papa Parse. To do this, set <a href="/docs#readonly">Papa.SCRIPT_PATH</a> to the relative path of the Papa Parse file. In synchronous loading, this is automatically detected. Yes.
</p> </p>
@ -209,7 +206,7 @@
<h6>Can I use a worker if I combine/concatenate my Javascript files?</h6> <h6>Can I use a worker if I combine/concatenate my Javascript files?</h6>
<p> <p>
Probably not. It's safest to concatenate the rest of your dependencies and include Papa Parse in a seperate file. Any library that expects to have access to the <code>window</code> or DOM will crash when executed in a worker thread. Only put <a href="/faq#combine">other libraries in the same file</a> if they are ready to be used in worker threads. Yes.
</p> </p>
<h6>When should I use a worker?</h6> <h6>When should I use a worker?</h6>
@ -241,6 +238,11 @@
<p> <p>
No. This would drastically slow down parsing, as it would require the worker to wait after every chunk for a "continue" signal from the main thread. But you <i>can</i> abort workers by calling <code>.abort()</code> on the parser that gets passed to your callback function. No. This would drastically slow down parsing, as it would require the worker to wait after every chunk for a "continue" signal from the main thread. But you <i>can</i> abort workers by calling <code>.abort()</code> on the parser that gets passed to your callback function.
</p> </p>
<h6>I set worker:true and now I'm getting an error: "window is not defined." How do I fix it?</h6>
<p>
This is a fairly common issue with configuration and it appears to be related to the use of React or other third party tools. Since this is a configuration issue, the exact steps needed to solve it may vary. See <a href="https://github.com/mholt/PapaParse/issues/655">Issue #655</a> on GitHub for a solution that worked for one person, and for links to other related issues.
</p>
</div> </div>
</div> </div>
</main> </main>
@ -257,7 +259,7 @@
<br><br> <br><br>
Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a> Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a>
<br> <br>
&copy; 2013-2018 &copy; 2013-2019
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Learn</h5> <h5>Learn</h5>
@ -267,7 +269,7 @@
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Project</h5> <h5>Project</h5>
<a href="https://gratipay.com/mholt">Donate</a> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=S6VTL9FQ6L8EN&item_name=PapaParse&currency_code=EUR&source=url">Donate</a>
<a href="https://github.com/mholt/PapaParse">GitHub</a> <a href="https://github.com/mholt/PapaParse">GitHub</a>
<a href="https://twitter.com/search?q=%23PapaParse">Share</a> <a href="https://twitter.com/search?q=%23PapaParse">Share</a>
</div> </div>

20
docs/index.html

@ -27,7 +27,7 @@
<h1>Papa Parse</h1> <h1>Papa Parse</h1>
<h2>The powerful, in-browser CSV parser for big boys and girls</h2> <h2>The powerful, in-browser CSV parser for big boys and girls</h2>
<a href="https://github.com/mholt/PapaParse/archive/4.5.0.zip" class="button"> <a href="https://github.com/mholt/PapaParse/archive/5.0.2.zip" class="button">
<i class="fa fa-download"></i>&nbsp; Download <i class="fa fa-download"></i>&nbsp; Download
</a> </a>
<a href="/demo" class="button red"> <a href="/demo" class="button red">
@ -81,7 +81,7 @@ Papa.parse(bigFile, {
</div> </div>
</div> </div>
<div class="grid-20 hide-on-mobile text-center"> <div class="grid-20 hide-on-mobile text-center">
<a href="/" class="text-logo">Papa Parse 4</a> <a href="/" class="text-logo">Papa Parse 5</a>
</div> </div>
<div class="grid-40 mobile-grid-50 text-right"> <div class="grid-40 mobile-grid-50 text-right">
<div class="links"> <div class="links">
@ -91,16 +91,13 @@ Papa.parse(bigFile, {
<a href="http://stackoverflow.com/questions/tagged/papaparse"> <a href="http://stackoverflow.com/questions/tagged/papaparse">
<i class="fa fa-stack-overflow fa-lg"></i> Help <i class="fa fa-stack-overflow fa-lg"></i> Help
</a> </a>
<!-- <a href="https://matt.life/pay" class="donate">
<i class="fa fa-heart fa-lg"></i> Donate
</a> -->
</div> </div>
</div> </div>
</div> </div>
</header> </header>
<div class="insignia"> <div class="insignia">
<div class="firefox-hack"><div id="version-intro">Version</div><div id="version">4.5</div></div> <div class="firefox-hack"><div id="version-intro">Version</div><div id="version">5.0</div></div>
</div> </div>
@ -199,7 +196,7 @@ Papa.parse(bigFile, {
<div class="grid-100 text-center"> <div class="grid-100 text-center">
<br> <br>
<b><a href="https://github.com/mholt/PapaParse/blob/gh-pages/resources/js/lovers.js" class="add-lover-link subheader"><i class="fa fa-plus-square"></i> Add your link (it's free)</a></b> <b><a href="https://github.com/mholt/PapaParse/blob/master/docs/resources/js/lovers.js" class="add-lover-link subheader"><i class="fa fa-plus-square"></i> Add your link (it's free)</a></b>
</div> </div>
</div> </div>
</section> </section>
@ -506,6 +503,7 @@ var csv = Papa.unparse(yourData);</code></pre>
<i class="fa fa-book"></i>&nbsp; Documentation <i class="fa fa-book"></i>&nbsp; Documentation
</a> </a>
</div> </div>
</div>
</section> </section>
@ -524,7 +522,7 @@ var csv = Papa.unparse(yourData);</code></pre>
<br><br> <br><br>
Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a> Papa Parse by <a href="https://twitter.com/mholt6">Matt Holt</a>
<br> <br>
&copy; 2013-2018 &copy; 2013-2019
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Learn</h5> <h5>Learn</h5>
@ -534,7 +532,7 @@ var csv = Papa.unparse(yourData);</code></pre>
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Project</h5> <h5>Project</h5>
<a href="https://gratipay.com/mholt">Donate</a> <a href="https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=S6VTL9FQ6L8EN&item_name=PapaParse&currency_code=EUR&source=url">Donate</a>
<a href="https://github.com/mholt/PapaParse">GitHub</a> <a href="https://github.com/mholt/PapaParse">GitHub</a>
<a href="https://twitter.com/search?q=%23PapaParse">Share</a> <a href="https://twitter.com/search?q=%23PapaParse">Share</a>
</div> </div>
@ -543,8 +541,8 @@ var csv = Papa.unparse(yourData);</code></pre>
<h5>Download</h5> <h5>Download</h5>
<a href="https://github.com/mholt/PapaParse/archive/master.zip">Latest (master)</a> <a href="https://github.com/mholt/PapaParse/archive/master.zip">Latest (master)</a>
<hr> <hr>
<a href="https://github.com/mholt/PapaParse/blob/master/papaparse.min.js">Lil' Papa</a> <a href="https://unpkg.com/papaparse@latest/papaparse.min.js">Lil' Papa</a>
<a href="https://github.com/mholt/PapaParse/blob/master/papaparse.js">Fat Papa</a> <a href="https://unpkg.com/papaparse@latest/papaparse.js">Fat Papa</a>
</div> </div>
<div class="grid-15 mobile-grid-50 links"> <div class="grid-15 mobile-grid-50 links">
<h5>Community</h5> <h5>Community</h5>

15
docs/resources/css/common.css

@ -77,14 +77,13 @@ h4 {
line-height: 1.25em; line-height: 1.25em;
} }
h5, h5, footer h4 {
footer h4 { font-size: 20px;
font-size: 12px; letter-spacing: 2px;
letter-spacing: 2px; text-transform: uppercase;
text-transform: uppercase; color: #242627;
color: #A1B2C2; /* darker alternative: #698EB0 */ line-height: 1em;
line-height: 1em; margin: 50px auto;
margin: 50px auto;
} }
h6 { h6 {

19
docs/resources/js/lovers.js

@ -78,5 +78,24 @@ var peopleLovePapa = [
name: "Novel.js", name: "Novel.js",
description: "is a text adventure framework that uses Papa Parse to enable user-friendly translations.", description: "is a text adventure framework that uses Papa Parse to enable user-friendly translations.",
quote: "Papa saves countless hours of work and makes reading large CSV files so easy!" quote: "Papa saves countless hours of work and makes reading large CSV files so easy!"
},
{
link: "https://mailcheck.co",
name: "Mailcheck.co",
description: "Mailcheck is email validation service. All emails usually stored in CSV's. We use Papa Parse to process data from our customers in browser",
quote: "Papa Parser allowed our customers to preview and process csv's in browser, without uploading them to server. It saves lots of time and space :)"
},
{
link: "https://flatfile.io",
name: "Flatfile.io",
description: "is an add-in data importer for web-apps, providing the full UX to upload a spreadsheet, field match, and repair issues found during import.",
quote: "Papa is a core part of our importer, so much so that we're committed to helping maintain it!"
},
{
link: "https://familiohq.com",
name: "Familio",
description: "is a brand-new messaging app made specifically for busy families. Automatically align all family members when sending text messages to parents in the kindergarten or school or when planning your kids birthday parties.",
quote: "With Papa it was a joy to implement our tool for importing messages and places from external systems."
} }
]; ];

663
docs/resources/js/papaparse.js

File diff suppressed because it is too large Load Diff

14
package.json

@ -1,6 +1,6 @@
{ {
"name": "papaparse", "name": "papaparse",
"version": "4.5.0", "version": "5.0.2",
"description": "Fast and powerful CSV parser for the browser that supports web workers and streaming large files. Converts CSV to JSON and JSON to CSV.", "description": "Fast and powerful CSV parser for the browser that supports web workers and streaming large files. Converts CSV to JSON and JSON to CSV.",
"keywords": [ "keywords": [
"csv", "csv",
@ -35,23 +35,23 @@
}, },
"license": "MIT", "license": "MIT",
"main": "papaparse.js", "main": "papaparse.js",
"browser": "papaparse.min.js",
"devDependencies": { "devDependencies": {
"chai": "^4.1.2", "chai": "^4.2.0",
"connect": "^3.3.3", "connect": "^3.3.3",
"eslint": "^4.19.1", "eslint": "^4.19.1",
"grunt": "^1.0.2", "grunt": "^1.0.2",
"grunt-contrib-uglify": "^3.3.0", "grunt-contrib-uglify": "^3.3.0",
"mocha": "^3.5.0", "mocha": "^5.2.0",
"mocha-phantomjs": "^4.1.0", "mocha-headless-chrome": "^2.0.1",
"open": "0.0.5", "open": "0.0.5",
"phantomjs-prebuilt": "^2.1.16",
"serve-static": "^1.7.1" "serve-static": "^1.7.1"
}, },
"scripts": { "scripts": {
"lint": "eslint --no-ignore papaparse.js Gruntfile.js .eslintrc.js 'tests/**/*.js'", "lint": "eslint --no-ignore papaparse.js Gruntfile.js .eslintrc.js 'tests/**/*.js'",
"test-browser": "node tests/test.js", "test-browser": "node tests/test.js",
"test-phantomjs": "node tests/test.js --phantomjs", "test-mocha-headless-chrome": "node tests/test.js --mocha-headless-chrome",
"test-node": "mocha tests/node-tests.js tests/test-cases.js", "test-node": "mocha tests/node-tests.js tests/test-cases.js",
"test": "npm run lint && npm run test-node && npm run test-phantomjs" "test": "npm run lint && npm run test-node && npm run test-mocha-headless-chrome"
} }
} }

403
papaparse.js

@ -1,9 +1,10 @@
/*@license /* @license
Papa Parse Papa Parse
v4.5.0 v5.0.2
https://github.com/mholt/PapaParse https://github.com/mholt/PapaParse
License: MIT License: MIT
*/ */
(function(root, factory) (function(root, factory)
{ {
/* globals define */ /* globals define */
@ -24,7 +25,10 @@
// Browser globals (root is window) // Browser globals (root is window)
root.Papa = factory(); root.Papa = factory();
} }
}(this, function() // in strict mode we cannot access arguments.callee, so we need a named reference to
// stringify the factory method for the blob worker
// eslint-disable-next-line func-name
}(this, function moduleFactory()
{ {
'use strict'; 'use strict';
@ -42,9 +46,14 @@
})(); })();
function getWorkerBlob() {
var URL = global.URL || global.webkitURL || null;
var code = moduleFactory.toString();
return Papa.BLOB_URL || (Papa.BLOB_URL = URL.createObjectURL(new Blob(['(', code, ')();'], {type: 'text/javascript'})));
}
var IS_WORKER = !global.document && !!global.postMessage, var IS_WORKER = !global.document && !!global.postMessage,
IS_PAPA_WORKER = IS_WORKER && /(\?|&)papaworker(=|&|$)/.test(global.location.search), IS_PAPA_WORKER = IS_WORKER && /blob:/i.test((global.location || {}).protocol);
LOADED_SYNC = false, AUTO_SCRIPT_PATH;
var workers = {}, workerIdCounter = 0; var workers = {}, workerIdCounter = 0;
var Papa = {}; var Papa = {};
@ -57,7 +66,6 @@
Papa.BYTE_ORDER_MARK = '\ufeff'; Papa.BYTE_ORDER_MARK = '\ufeff';
Papa.BAD_DELIMITERS = ['\r', '\n', '"', Papa.BYTE_ORDER_MARK]; Papa.BAD_DELIMITERS = ['\r', '\n', '"', Papa.BYTE_ORDER_MARK];
Papa.WORKERS_SUPPORTED = !IS_WORKER && !!global.Worker; Papa.WORKERS_SUPPORTED = !IS_WORKER && !!global.Worker;
Papa.SCRIPT_PATH = null; // Must be set by your code if you use workers and this lib is loaded asynchronously
Papa.NODE_STREAM_INPUT = 1; Papa.NODE_STREAM_INPUT = 1;
// Configurable chunk sizes for local and remote files, respectively // Configurable chunk sizes for local and remote files, respectively
@ -72,7 +80,9 @@
Papa.FileStreamer = FileStreamer; Papa.FileStreamer = FileStreamer;
Papa.StringStreamer = StringStreamer; Papa.StringStreamer = StringStreamer;
Papa.ReadableStreamStreamer = ReadableStreamStreamer; Papa.ReadableStreamStreamer = ReadableStreamStreamer;
Papa.DuplexStreamStreamer = DuplexStreamStreamer; if (typeof PAPA_BROWSER_CONTEXT === 'undefined') {
Papa.DuplexStreamStreamer = DuplexStreamStreamer;
}
if (global.jQuery) if (global.jQuery)
{ {
@ -173,23 +183,6 @@
{ {
global.onmessage = workerThreadReceivedMessage; global.onmessage = workerThreadReceivedMessage;
} }
else if (Papa.WORKERS_SUPPORTED)
{
AUTO_SCRIPT_PATH = getScriptPath();
// Check if the script was loaded synchronously
if (!document.body)
{
// Body doesn't exist yet, must be synchronous
LOADED_SYNC = true;
}
else
{
document.addEventListener('DOMContentLoaded', function() {
LOADED_SYNC = true;
}, true);
}
}
@ -232,7 +225,7 @@
} }
var streamer = null; var streamer = null;
if (_input === Papa.NODE_STREAM_INPUT) if (_input === Papa.NODE_STREAM_INPUT && typeof PAPA_BROWSER_CONTEXT === 'undefined')
{ {
// create a node Duplex stream for use // create a node Duplex stream for use
// with .pipe // with .pipe
@ -280,44 +273,53 @@
/** quote character */ /** quote character */
var _quoteChar = '"'; var _quoteChar = '"';
/** escaped quote character, either "" or <config.escapeChar>" */
var _escapedQuote = _quoteChar + _quoteChar;
/** whether to skip empty lines */
var _skipEmptyLines = false;
/** the columns (keys) we expect when we unparse objects */
var _columns = null;
unpackConfig(); unpackConfig();
var quoteCharRegex = new RegExp(_quoteChar, 'g'); var quoteCharRegex = new RegExp(escapeRegExp(_quoteChar), 'g');
if (typeof _input === 'string') if (typeof _input === 'string')
_input = JSON.parse(_input); _input = JSON.parse(_input);
if (_input instanceof Array) if (Array.isArray(_input))
{ {
if (!_input.length || _input[0] instanceof Array) if (!_input.length || Array.isArray(_input[0]))
return serialize(null, _input); return serialize(null, _input, _skipEmptyLines);
else if (typeof _input[0] === 'object') else if (typeof _input[0] === 'object')
return serialize(objectKeys(_input[0]), _input); return serialize(_columns || objectKeys(_input[0]), _input, _skipEmptyLines);
} }
else if (typeof _input === 'object') else if (typeof _input === 'object')
{ {
if (typeof _input.data === 'string') if (typeof _input.data === 'string')
_input.data = JSON.parse(_input.data); _input.data = JSON.parse(_input.data);
if (_input.data instanceof Array) if (Array.isArray(_input.data))
{ {
if (!_input.fields) if (!_input.fields)
_input.fields = _input.meta && _input.meta.fields; _input.fields = _input.meta && _input.meta.fields;
if (!_input.fields) if (!_input.fields)
_input.fields = _input.data[0] instanceof Array _input.fields = Array.isArray(_input.data[0])
? _input.fields ? _input.fields
: objectKeys(_input.data[0]); : objectKeys(_input.data[0]);
if (!(_input.data[0] instanceof Array) && typeof _input.data[0] !== 'object') if (!(Array.isArray(_input.data[0])) && typeof _input.data[0] !== 'object')
_input.data = [_input.data]; // handles input like [1,2,3] or ['asdf'] _input.data = [_input.data]; // handles input like [1,2,3] or ['asdf']
} }
return serialize(_input.fields || [], _input.data || []); return serialize(_input.fields || [], _input.data || [], _skipEmptyLines);
} }
// Default (any valid paths should return before this) // Default (any valid paths should return before this)
throw 'exception: Unable to serialize unrecognized input'; throw new Error('Unable to serialize unrecognized input');
function unpackConfig() function unpackConfig()
@ -332,9 +334,14 @@
} }
if (typeof _config.quotes === 'boolean' if (typeof _config.quotes === 'boolean'
|| _config.quotes instanceof Array) || typeof _config.quotes === 'function'
|| Array.isArray(_config.quotes))
_quotes = _config.quotes; _quotes = _config.quotes;
if (typeof _config.skipEmptyLines === 'boolean'
|| typeof _config.skipEmptyLines === 'string')
_skipEmptyLines = _config.skipEmptyLines;
if (typeof _config.newline === 'string') if (typeof _config.newline === 'string')
_newline = _config.newline; _newline = _config.newline;
@ -343,6 +350,17 @@
if (typeof _config.header === 'boolean') if (typeof _config.header === 'boolean')
_writeHeader = _config.header; _writeHeader = _config.header;
if (Array.isArray(_config.columns)) {
if (_config.columns.length === 0) throw new Error('Option columns is empty');
_columns = _config.columns;
}
if (_config.escapeChar !== undefined) {
_escapedQuote = _config.escapeChar + _quoteChar;
}
} }
@ -358,7 +376,7 @@
} }
/** The double for loop that iterates the data and writes out a CSV string including header row */ /** The double for loop that iterates the data and writes out a CSV string including header row */
function serialize(fields, data) function serialize(fields, data, skipEmptyLines)
{ {
var csv = ''; var csv = '';
@ -367,8 +385,8 @@
if (typeof data === 'string') if (typeof data === 'string')
data = JSON.parse(data); data = JSON.parse(data);
var hasHeader = fields instanceof Array && fields.length > 0; var hasHeader = Array.isArray(fields) && fields.length > 0;
var dataKeyedByField = !(data[0] instanceof Array); var dataKeyedByField = !(Array.isArray(data[0]));
// If there a header row, write it first // If there a header row, write it first
if (hasHeader && _writeHeader) if (hasHeader && _writeHeader)
@ -388,18 +406,35 @@
{ {
var maxCol = hasHeader ? fields.length : data[row].length; var maxCol = hasHeader ? fields.length : data[row].length;
for (var col = 0; col < maxCol; col++) var emptyLine = false;
var nullLine = hasHeader ? Object.keys(data[row]).length === 0 : data[row].length === 0;
if (skipEmptyLines && !hasHeader)
{ {
if (col > 0) emptyLine = skipEmptyLines === 'greedy' ? data[row].join('').trim() === '' : data[row].length === 1 && data[row][0].length === 0;
csv += _delimiter; }
var colIdx = hasHeader && dataKeyedByField ? fields[col] : col; if (skipEmptyLines === 'greedy' && hasHeader) {
csv += safe(data[row][colIdx], col); var line = [];
for (var c = 0; c < maxCol; c++) {
var cx = dataKeyedByField ? fields[c] : c;
line.push(data[row][cx]);
}
emptyLine = line.join('').trim() === '';
}
if (!emptyLine)
{
for (var col = 0; col < maxCol; col++)
{
if (col > 0 && !nullLine)
csv += _delimiter;
var colIdx = hasHeader && dataKeyedByField ? fields[col] : col;
csv += safe(data[row][colIdx], col);
}
if (row < data.length - 1 && (!skipEmptyLines || (maxCol > 0 && !nullLine)))
{
csv += _newline;
}
} }
if (row < data.length - 1)
csv += _newline;
} }
return csv; return csv;
} }
@ -412,10 +447,11 @@
if (str.constructor === Date) if (str.constructor === Date)
return JSON.stringify(str).slice(1, 25); return JSON.stringify(str).slice(1, 25);
str = str.toString().replace(quoteCharRegex, _quoteChar + _quoteChar); str = str.toString().replace(quoteCharRegex, _escapedQuote);
var needsQuotes = (typeof _quotes === 'boolean' && _quotes) var needsQuotes = (typeof _quotes === 'boolean' && _quotes)
|| (_quotes instanceof Array && _quotes[col]) || (typeof _quotes === 'function' && _quotes(str, col))
|| (Array.isArray(_quotes) && _quotes[col])
|| hasAny(str, Papa.BAD_DELIMITERS) || hasAny(str, Papa.BAD_DELIMITERS)
|| str.indexOf(_delimiter) > -1 || str.indexOf(_delimiter) > -1
|| str.charAt(0) === ' ' || str.charAt(0) === ' '
@ -439,6 +475,7 @@
this._handle = null; this._handle = null;
this._finished = false; this._finished = false;
this._completed = false; this._completed = false;
this._halted = false;
this._input = null; this._input = null;
this._baseIndex = 0; this._baseIndex = 0;
this._partialLine = ''; this._partialLine = '';
@ -463,6 +500,7 @@
chunk = modifiedChunk; chunk = modifiedChunk;
} }
this.isFirstChunk = false; this.isFirstChunk = false;
this._halted = false;
// Rejoin the line we likely just split in two by chunking the file // Rejoin the line we likely just split in two by chunking the file
var aggregate = this._partialLine + chunk; var aggregate = this._partialLine + chunk;
@ -470,8 +508,10 @@
var results = this._handle.parse(aggregate, this._baseIndex, !this._finished); var results = this._handle.parse(aggregate, this._baseIndex, !this._finished);
if (this._handle.paused() || this._handle.aborted()) if (this._handle.paused() || this._handle.aborted()) {
this._halted = true;
return; return;
}
var lastIndex = results.meta.cursor; var lastIndex = results.meta.cursor;
@ -497,8 +537,10 @@
else if (isFunction(this._config.chunk) && !isFakeChunk) else if (isFunction(this._config.chunk) && !isFakeChunk)
{ {
this._config.chunk(results, this._handle); this._config.chunk(results, this._handle);
if (this._handle.paused() || this._handle.aborted()) if (this._handle.paused() || this._handle.aborted()) {
this._halted = true;
return; return;
}
results = undefined; results = undefined;
this._completeResults = undefined; this._completeResults = undefined;
} }
@ -616,7 +658,6 @@
{ {
var end = this._start + this._config.chunkSize - 1; // minus one because byte range is inclusive var end = this._start + this._config.chunkSize - 1; // minus one because byte range is inclusive
xhr.setRequestHeader('Range', 'bytes=' + this._start + '-' + end); xhr.setRequestHeader('Range', 'bytes=' + this._start + '-' + end);
xhr.setRequestHeader('If-None-Match', 'webkit-no-cache'); // https://bugs.webkit.org/show_bug.cgi?id=82672
} }
try { try {
@ -863,13 +904,11 @@
this._onCsvData = function(results) this._onCsvData = function(results)
{ {
var data = results.data; var data = results.data;
for (var i = 0; i < data.length; i++) { if (!stream.push(data) && !this._handle.paused()) {
if (!stream.push(data[i]) && !this._handle.paused()) { // the writeable consumer buffer has filled up
// the writeable consumer buffer has filled up // so we need to pause until more items
// so we need to pause until more items // can be processed
// can be processed this._handle.pause();
this._handle.pause();
}
} }
}; };
@ -949,17 +988,20 @@
}); });
stream.once('finish', bindFunction(this._onWriteComplete, this)); stream.once('finish', bindFunction(this._onWriteComplete, this));
} }
DuplexStreamStreamer.prototype = Object.create(ChunkStreamer.prototype); if (typeof PAPA_BROWSER_CONTEXT === 'undefined') {
DuplexStreamStreamer.prototype.constructor = DuplexStreamStreamer; DuplexStreamStreamer.prototype = Object.create(ChunkStreamer.prototype);
DuplexStreamStreamer.prototype.constructor = DuplexStreamStreamer;
}
// Use one ParserHandle per entire CSV file or string // Use one ParserHandle per entire CSV file or string
function ParserHandle(_config) function ParserHandle(_config)
{ {
// One goal is to minimize the use of regular expressions... // One goal is to minimize the use of regular expressions...
var MAX_FLOAT = Math.pow(2, 53);
var MIN_FLOAT = -MAX_FLOAT;
var FLOAT = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i; var FLOAT = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i;
var ISO_DATE = /(\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))/; var ISO_DATE = /(\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))/;
var self = this; var self = this;
var _stepCounter = 0; // Number of times step was called (number of rows parsed) var _stepCounter = 0; // Number of times step was called (number of rows parsed)
var _rowCounter = 0; // Number of rows that have been parsed so far var _rowCounter = 0; // Number of rows that have been parsed so far
@ -1008,13 +1050,14 @@
*/ */
this.parse = function(input, baseIndex, ignoreLastRow) this.parse = function(input, baseIndex, ignoreLastRow)
{ {
var quoteChar = _config.quoteChar || '"';
if (!_config.newline) if (!_config.newline)
_config.newline = guessLineEndings(input); _config.newline = guessLineEndings(input, quoteChar);
_delimiterError = false; _delimiterError = false;
if (!_config.delimiter) if (!_config.delimiter)
{ {
var delimGuess = guessDelimiter(input, _config.newline, _config.skipEmptyLines, _config.comments); var delimGuess = guessDelimiter(input, _config.newline, _config.skipEmptyLines, _config.comments, _config.delimitersToGuess);
if (delimGuess.successful) if (delimGuess.successful)
_config.delimiter = delimGuess.bestDelimiter; _config.delimiter = delimGuess.bestDelimiter;
else else
@ -1055,8 +1098,14 @@
this.resume = function() this.resume = function()
{ {
_paused = false; if(self.streamer._halted) {
self.streamer.parseChunk(_input, true); _paused = false;
self.streamer.parseChunk(_input, true);
} else {
// Bugfix: #636 In case the processing hasn't halted yet
// wait for it to halt in order to resume
setTimeout(this.resume, 3);
}
}; };
this.aborted = function() this.aborted = function()
@ -1074,6 +1123,20 @@
_input = ''; _input = '';
}; };
function testEmptyLine(s) {
return _config.skipEmptyLines === 'greedy' ? s.join('').trim() === '' : s.length === 1 && s[0].length === 0;
}
function testFloat(s) {
if (FLOAT.test(s)) {
var floatValue = parseFloat(s);
if (floatValue > MIN_FLOAT && floatValue < MAX_FLOAT) {
return true;
}
}
return false;
}
function processResults() function processResults()
{ {
if (_results && _delimiterError) if (_results && _delimiterError)
@ -1085,7 +1148,7 @@
if (_config.skipEmptyLines) if (_config.skipEmptyLines)
{ {
for (var i = 0; i < _results.data.length; i++) for (var i = 0; i < _results.data.length; i++)
if (_results.data[i].length === 1 && _results.data[i][0] === '') if (testEmptyLine(_results.data[i]))
_results.data.splice(i--, 1); _results.data.splice(i--, 1);
} }
@ -1104,18 +1167,25 @@
{ {
if (!_results) if (!_results)
return; return;
for (var i = 0; needsHeaderRow() && i < _results.data.length; i++)
for (var j = 0; j < _results.data[i].length; j++)
{
var header = _results.data[i][j];
if (_config.trimHeaders) { function addHeder(header)
header = header.trim(); {
} if (isFunction(_config.transformHeader))
header = _config.transformHeader(header);
_fields.push(header); _fields.push(header);
} }
_results.data.splice(0, 1);
if (Array.isArray(_results.data[0]))
{
for (var i = 0; needsHeaderRow() && i < _results.data.length; i++)
_results.data[i].forEach(addHeder);
_results.data.splice(0, 1);
}
// if _results.data[0] is not an array, we are in a step where _results.data is the row.
else
_results.data.forEach(addHeder);
} }
function shouldApplyDynamicTyping(field) { function shouldApplyDynamicTyping(field) {
@ -1134,7 +1204,7 @@
return true; return true;
else if (value === 'false' || value === 'FALSE') else if (value === 'false' || value === 'FALSE')
return false; return false;
else if (FLOAT.test(value)) else if (testFloat(value))
return parseFloat(value); return parseFloat(value);
else if (ISO_DATE.test(value)) else if (ISO_DATE.test(value))
return new Date(value); return new Date(value);
@ -1149,15 +1219,15 @@
if (!_results || (!_config.header && !_config.dynamicTyping && !_config.transform)) if (!_results || (!_config.header && !_config.dynamicTyping && !_config.transform))
return _results; return _results;
for (var i = 0; i < _results.data.length; i++) function processRow(rowSource, i)
{ {
var row = _config.header ? {} : []; var row = _config.header ? {} : [];
var j; var j;
for (j = 0; j < _results.data[i].length; j++) for (j = 0; j < rowSource.length; j++)
{ {
var field = j; var field = j;
var value = _results.data[i][j]; var value = rowSource[j];
if (_config.header) if (_config.header)
field = j >= _fields.length ? '__parsed_extra' : _fields[j]; field = j >= _fields.length ? '__parsed_extra' : _fields[j];
@ -1176,7 +1246,6 @@
row[field] = value; row[field] = value;
} }
_results.data[i] = row;
if (_config.header) if (_config.header)
{ {
@ -1185,23 +1254,34 @@
else if (j < _fields.length) else if (j < _fields.length)
addError('FieldMismatch', 'TooFewFields', 'Too few fields: expected ' + _fields.length + ' fields but parsed ' + j, _rowCounter + i); addError('FieldMismatch', 'TooFewFields', 'Too few fields: expected ' + _fields.length + ' fields but parsed ' + j, _rowCounter + i);
} }
return row;
} }
var incrementBy = 1;
if (!_results.data[0] || Array.isArray(_results.data[0]))
{
_results.data = _results.data.map(processRow);
incrementBy = _results.data.length;
}
else
_results.data = processRow(_results.data, 0);
if (_config.header && _results.meta) if (_config.header && _results.meta)
_results.meta.fields = _fields; _results.meta.fields = _fields;
_rowCounter += _results.data.length; _rowCounter += incrementBy;
return _results; return _results;
} }
function guessDelimiter(input, newline, skipEmptyLines, comments) function guessDelimiter(input, newline, skipEmptyLines, comments, delimitersToGuess) {
{ var bestDelim, bestDelta, fieldCountPrevRow, maxFieldCount;
var delimChoices = [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP];
var bestDelim, bestDelta, fieldCountPrevRow;
for (var i = 0; i < delimChoices.length; i++) delimitersToGuess = delimitersToGuess || [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP];
{
var delim = delimChoices[i]; for (var i = 0; i < delimitersToGuess.length; i++) {
var delim = delimitersToGuess[i];
var delta = 0, avgFieldCount = 0, emptyLinesCount = 0; var delta = 0, avgFieldCount = 0, emptyLinesCount = 0;
fieldCountPrevRow = undefined; fieldCountPrevRow = undefined;
@ -1212,22 +1292,19 @@
preview: 10 preview: 10
}).parse(input); }).parse(input);
for (var j = 0; j < preview.data.length; j++) for (var j = 0; j < preview.data.length; j++) {
{ if (skipEmptyLines && testEmptyLine(preview.data[j])) {
if (skipEmptyLines && preview.data[j].length === 1 && preview.data[j][0].length === 0) {
emptyLinesCount++; emptyLinesCount++;
continue; continue;
} }
var fieldCount = preview.data[j].length; var fieldCount = preview.data[j].length;
avgFieldCount += fieldCount; avgFieldCount += fieldCount;
if (typeof fieldCountPrevRow === 'undefined') if (typeof fieldCountPrevRow === 'undefined') {
{
fieldCountPrevRow = fieldCount; fieldCountPrevRow = fieldCount;
continue; continue;
} }
else if (fieldCount > 1) else if (fieldCount > 0) {
{
delta += Math.abs(fieldCount - fieldCountPrevRow); delta += Math.abs(fieldCount - fieldCountPrevRow);
fieldCountPrevRow = fieldCount; fieldCountPrevRow = fieldCount;
} }
@ -1236,11 +1313,11 @@
if (preview.data.length > 0) if (preview.data.length > 0)
avgFieldCount /= (preview.data.length - emptyLinesCount); avgFieldCount /= (preview.data.length - emptyLinesCount);
if ((typeof bestDelta === 'undefined' || delta < bestDelta) if ((typeof bestDelta === 'undefined' || delta <= bestDelta)
&& avgFieldCount > 1.99) && (typeof maxFieldCount === 'undefined' || avgFieldCount > maxFieldCount) && avgFieldCount > 1.99) {
{
bestDelta = delta; bestDelta = delta;
bestDelim = delim; bestDelim = delim;
maxFieldCount = avgFieldCount;
} }
} }
@ -1252,9 +1329,12 @@
}; };
} }
function guessLineEndings(input) function guessLineEndings(input, quoteChar)
{ {
input = input.substr(0, 1024 * 1024); // max length 1 MB input = input.substr(0, 1024 * 1024); // max length 1 MB
// Replace all the text inside quotes
var re = new RegExp(escapeRegExp(quoteChar) + '([^]*?)' + escapeRegExp(quoteChar), 'gm');
input = input.replace(re, '');
var r = input.split('\r'); var r = input.split('\r');
@ -1286,9 +1366,11 @@
} }
} }
/** https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions */
function escapeRegExp(string)
{
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
/** The core parser implements speedy and correct CSV parsing */ /** The core parser implements speedy and correct CSV parsing */
function Parser(config) function Parser(config)
@ -1320,7 +1402,7 @@
// Comment character must be valid // Comment character must be valid
if (comments === delim) if (comments === delim)
throw 'Comment character same as delimiter'; throw new Error('Comment character same as delimiter');
else if (comments === true) else if (comments === true)
comments = '#'; comments = '#';
else if (typeof comments !== 'string' else if (typeof comments !== 'string'
@ -1339,7 +1421,7 @@
{ {
// For some reason, in Chrome, this speeds things up (!?) // For some reason, in Chrome, this speeds things up (!?)
if (typeof input !== 'string') if (typeof input !== 'string')
throw 'Input must be a string'; throw new Error('Input must be a string');
// We don't need to compute some of these every time parse() is called, // We don't need to compute some of these every time parse() is called,
// but having them in a more local scope seems to perform better // but having them in a more local scope seems to perform better
@ -1390,8 +1472,8 @@
var nextDelim = input.indexOf(delim, cursor); var nextDelim = input.indexOf(delim, cursor);
var nextNewline = input.indexOf(newline, cursor); var nextNewline = input.indexOf(newline, cursor);
var quoteCharRegex = new RegExp(escapeChar.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') + quoteChar, 'g'); var quoteCharRegex = new RegExp(escapeRegExp(escapeChar) + escapeRegExp(quoteChar), 'g');
var quoteSearch; var quoteSearch = input.indexOf(quoteChar, cursor);
// Parser loop // Parser loop
for (;;) for (;;)
@ -1456,6 +1538,12 @@
{ {
row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar)); row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar));
cursor = quoteSearch + 1 + spacesBetweenQuoteAndDelimiter + delimLen; cursor = quoteSearch + 1 + spacesBetweenQuoteAndDelimiter + delimLen;
// If char after following delimiter is not quoteChar, we find next quote char position
if (input[quoteSearch + 1 + spacesBetweenQuoteAndDelimiter + delimLen] !== quoteChar)
{
quoteSearch = input.indexOf(quoteChar, cursor);
}
nextDelim = input.indexOf(delim, cursor); nextDelim = input.indexOf(delim, cursor);
nextNewline = input.indexOf(newline, cursor); nextNewline = input.indexOf(newline, cursor);
break; break;
@ -1469,6 +1557,7 @@
row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar)); row.push(input.substring(cursor, quoteSearch).replace(quoteCharRegex, quoteChar));
saveRow(quoteSearch + 1 + spacesBetweenQuoteAndNewLine + newlineLen); saveRow(quoteSearch + 1 + spacesBetweenQuoteAndNewLine + newlineLen);
nextDelim = input.indexOf(delim, cursor); // because we may have skipped the nextDelim in the quoted field nextDelim = input.indexOf(delim, cursor); // because we may have skipped the nextDelim in the quoted field
quoteSearch = input.indexOf(quoteChar, cursor); // we search for first quote in next line
if (stepIsFunction) if (stepIsFunction)
{ {
@ -1515,10 +1604,27 @@
// Next delimiter comes before next newline, so we've reached end of field // Next delimiter comes before next newline, so we've reached end of field
if (nextDelim !== -1 && (nextDelim < nextNewline || nextNewline === -1)) if (nextDelim !== -1 && (nextDelim < nextNewline || nextNewline === -1))
{ {
row.push(input.substring(cursor, nextDelim)); // we check, if we have quotes, because delimiter char may be part of field enclosed in quotes
cursor = nextDelim + delimLen; if (quoteSearch > nextDelim) {
nextDelim = input.indexOf(delim, cursor); // we have quotes, so we try to find the next delimiter not enclosed in quotes and also next starting quote char
continue; var nextDelimObj = getNextUnqotedDelimiter(nextDelim, quoteSearch, nextNewline);
// if we have next delimiter char which is not enclosed in quotes
if (nextDelimObj && typeof nextDelimObj.nextDelim !== 'undefined') {
nextDelim = nextDelimObj.nextDelim;
quoteSearch = nextDelimObj.quoteSearch;
row.push(input.substring(cursor, nextDelim));
cursor = nextDelim + delimLen;
// we look for next delimiter char
nextDelim = input.indexOf(delim, cursor);
continue;
}
} else {
row.push(input.substring(cursor, nextDelim));
cursor = nextDelim + delimLen;
nextDelim = input.indexOf(delim, cursor);
continue;
}
} }
// End of row // End of row
@ -1601,10 +1707,11 @@
} }
/** Returns an object with the results, errors, and meta. */ /** Returns an object with the results, errors, and meta. */
function returnable(stopped) function returnable(stopped, step)
{ {
var isStep = step || false;
return { return {
data: data, data: isStep ? data[0] : data,
errors: errors, errors: errors,
meta: { meta: {
delimiter: delim, delimiter: delim,
@ -1619,10 +1726,44 @@
/** Executes the user's step function and resets data & errors. */ /** Executes the user's step function and resets data & errors. */
function doStep() function doStep()
{ {
step(returnable()); step(returnable(undefined, true));
data = []; data = [];
errors = []; errors = [];
} }
/** Gets the delimiter character, which is not inside the quoted field */
function getNextUnqotedDelimiter(nextDelim, quoteSearch, newLine) {
var result = {
nextDelim: undefined,
quoteSearch: undefined
};
// get the next closing quote character
var nextQuoteSearch = input.indexOf(quoteChar, quoteSearch + 1);
// if next delimiter is part of a field enclosed in quotes
if (nextDelim > quoteSearch && nextDelim < nextQuoteSearch && (nextQuoteSearch < newLine || newLine === -1)) {
// get the next delimiter character after this one
var nextNextDelim = input.indexOf(delim, nextQuoteSearch);
// if there is no next delimiter, return default result
if (nextNextDelim === -1) {
return result;
}
// find the next opening quote char position
if (nextNextDelim > nextQuoteSearch) {
nextQuoteSearch = input.indexOf(quoteChar, nextQuoteSearch + 1);
}
// try to get the next delimiter position
result = getNextUnqotedDelimiter(nextNextDelim, nextQuoteSearch, newLine);
} else {
result = {
nextDelim: nextDelim,
quoteSearch: quoteSearch
};
}
return result;
}
}; };
/** Sets the abort flag */ /** Sets the abort flag */
@ -1639,26 +1780,12 @@
} }
// If you need to load Papa Parse asynchronously and you also need worker threads, hard-code
// the script path here. See: https://github.com/mholt/PapaParse/issues/87#issuecomment-57885358
function getScriptPath()
{
var scripts = document.getElementsByTagName('script');
return scripts.length ? scripts[scripts.length - 1].src : '';
}
function newWorker() function newWorker()
{ {
if (!Papa.WORKERS_SUPPORTED) if (!Papa.WORKERS_SUPPORTED)
return false; return false;
if (!LOADED_SYNC && Papa.SCRIPT_PATH === null)
throw new Error( var workerUrl = getWorkerBlob();
'Script path cannot be determined automatically when Papa Parse is loaded asynchronously. ' +
'You need to set Papa.SCRIPT_PATH manually.'
);
var workerUrl = Papa.SCRIPT_PATH || AUTO_SCRIPT_PATH;
// Append 'papaworker' to the search string to tell papaparse that this is our worker.
workerUrl += (workerUrl.indexOf('?') !== -1 ? '&' : '?') + 'papaworker';
var w = new global.Worker(workerUrl); var w = new global.Worker(workerUrl);
w.onmessage = mainThreadReceivedMessage; w.onmessage = mainThreadReceivedMessage;
w.id = workerIdCounter++; w.id = workerIdCounter++;
@ -1693,7 +1820,7 @@
for (var i = 0; i < msg.results.data.length; i++) for (var i = 0; i < msg.results.data.length; i++)
{ {
worker.userStep({ worker.userStep({
data: [msg.results.data[i]], data: msg.results.data[i],
errors: msg.results.errors, errors: msg.results.errors,
meta: msg.results.meta meta: msg.results.meta
}, handle); }, handle);
@ -1722,7 +1849,7 @@
} }
function notImplemented() { function notImplemented() {
throw 'Not implemented.'; throw new Error('Not implemented.');
} }
/** Callback when worker thread receives a message */ /** Callback when worker thread receives a message */
@ -1758,7 +1885,7 @@
{ {
if (typeof obj !== 'object' || obj === null) if (typeof obj !== 'object' || obj === null)
return obj; return obj;
var cpy = obj instanceof Array ? [] : {}; var cpy = Array.isArray(obj) ? [] : {};
for (var key in obj) for (var key in obj)
cpy[key] = copy(obj[key]); cpy[key] = copy(obj[key]);
return cpy; return cpy;

12
papaparse.min.js vendored

File diff suppressed because one or more lines are too long

36
tests/node-tests.js

@ -41,6 +41,42 @@ describe('PapaParse', function() {
assertLongSampleParsedCorrectly(Papa.parse(longSampleRawCsv)); assertLongSampleParsedCorrectly(Papa.parse(longSampleRawCsv));
}); });
it('Pause and resume works (Regression Test for Bug #636)', function(done) {
this.timeout(30000);
var mod200Rows = [
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Lorem ipsum dolor sit","42","ABC"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Etiam a dolor vitae est vestibulum","84"],
["Lorem ipsum dolor sit","42","ABC"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Lorem ipsum dolor sit","42","ABC"],
["Lorem ipsum dolor sit","42"]
];
var stepped = 0;
var dataRows = [];
Papa.parse(fs.createReadStream(__dirname + '/verylong-sample.csv'), {
step: function(results, parser) {
stepped++;
if (results)
{
parser.pause();
parser.resume();
if (results.data && stepped % 200 === 0) {
dataRows.push(results.data);
}
}
},
complete: function() {
assert.strictEqual(2001, stepped);
assert.deepEqual(mod200Rows, dataRows);
done();
}
});
});
it('asynchronously parsed CSV should be correctly parsed', function(done) { it('asynchronously parsed CSV should be correctly parsed', function(done) {
Papa.parse(longSampleRawCsv, { Papa.parse(longSampleRawCsv, {
complete: function(parsedCsv) { complete: function(parsedCsv) {

533
tests/test-cases.js

@ -7,6 +7,7 @@ if (typeof module !== 'undefined' && module.exports) {
var assert = chai.assert; var assert = chai.assert;
var BASE_PATH = (typeof document === 'undefined') ? './' : document.getElementById('test-cases').src.replace(/test-cases\.js$/, '');
var RECORD_SEP = String.fromCharCode(30); var RECORD_SEP = String.fromCharCode(30);
var UNIT_SEP = String.fromCharCode(31); var UNIT_SEP = String.fromCharCode(31);
var FILES_ENABLED = false; var FILES_ENABLED = false;
@ -322,6 +323,14 @@ var CORE_PARSER_TESTS = [
errors: [] errors: []
} }
}, },
{
description: "Line starts with unquoted empty field",
input: ',b,c\n"d",e,f',
expected: {
data: [['', 'b', 'c'], ['d', 'e', 'f']],
errors: []
}
},
{ {
description: "Line ends with quoted field", description: "Line ends with quoted field",
input: 'a,b,c\nd,e,f\n"g","h","i"\n"j","k","l"', input: 'a,b,c\nd,e,f\n"g","h","i"\n"j","k","l"',
@ -661,6 +670,14 @@ var PARSE_TESTS = [
errors: [] errors: []
} }
}, },
{
description: "Misplaced quotes in data twice, not as opening quotes",
input: 'A,B",C\nD,E",F',
expected: {
data: [['A', 'B"', 'C'], ['D', 'E"', 'F']],
errors: []
}
},
{ {
description: "Mixed slash n and slash r should choose first as precident", description: "Mixed slash n and slash r should choose first as precident",
input: 'a,b,c\nd,e,f\rg,h,i\n', input: 'a,b,c\nd,e,f\rg,h,i\n',
@ -725,11 +742,11 @@ var PARSE_TESTS = [
} }
}, },
{ {
description: "Header rows are trimmed when trimHeaders is set", description: "Header rows are transformed when transformHeader function is provided",
input: ' A , B , C \r\na,b ,c', input: 'A,B,C\r\na,b,c',
config: { header: true, trimHeaders: true }, config: { header: true, transformHeader: function(header) { return header.toLowerCase(); } },
expected: { expected: {
data: [{"A": "a", "B": "b ", "C": "c"}], data: [{"a": "a", "b": "b", "c": "c"}],
errors: [] errors: []
} }
}, },
@ -814,11 +831,11 @@ var PARSE_TESTS = [
} }
}, },
{ {
description: "Dynamic typing converts numeric literals", description: "Dynamic typing converts numeric literals and maintains precision",
input: '1,2.2,1e3\r\n-4,-4.5,-4e-5\r\n-,5a,5-2', input: '1,2.2,1e3\r\n-4,-4.5,-4e-5\r\n-,5a,5-2\r\n16142028098527942586,9007199254740991,-9007199254740992',
config: { dynamicTyping: true }, config: { dynamicTyping: true },
expected: { expected: {
data: [[1, 2.2, 1000], [-4, -4.5, -0.00004], ["-", "5a", "5-2"]], data: [[1, 2.2, 1000], [-4, -4.5, -0.00004], ["-", "5a", "5-2"], ["16142028098527942586", 9007199254740991, "-9007199254740992"]],
errors: [] errors: []
} }
}, },
@ -912,6 +929,39 @@ var PARSE_TESTS = [
errors: [] errors: []
} }
}, },
{
description: "Custom transform accepts column number also",
input: 'A,B,C\r\nd,e,f',
config: {
transform: function(value, column) {
if (column % 2) {
value = value.toLowerCase();
}
return value;
}
},
expected: {
data: [["A","b","C"], ["d","e","f"]],
errors: []
}
},
{
description: "Custom transform accepts header name when using header",
input: 'A,B,C\r\nd,e,f',
config: {
header: true,
transform: function(value, name) {
if (name === 'B') {
value = value.toUpperCase();
}
return value;
}
},
expected: {
data: [{'A': "d", 'B': "E", 'C': "f"}],
errors: []
}
},
{ {
description: "Dynamic typing converts ISO date strings to Dates", description: "Dynamic typing converts ISO date strings to Dates",
input: 'ISO date,long date\r\n2018-05-04T21:08:03.269Z,Fri May 04 2018 14:08:03 GMT-0700 (PDT)\r\n2018-05-08T15:20:22.642Z,Tue May 08 2018 08:20:22 GMT-0700 (PDT)', input: 'ISO date,long date\r\n2018-05-04T21:08:03.269Z,Fri May 04 2018 14:08:03 GMT-0700 (PDT)\r\n2018-05-08T15:20:22.642Z,Tue May 08 2018 08:20:22 GMT-0700 (PDT)',
@ -1159,6 +1209,46 @@ var PARSE_TESTS = [
errors: [] errors: []
} }
}, },
{
description: "Pipe delimiter is guessed correctly when mixed with comas",
notes: "Guessing the delimiter should work even if there are many lines of comments at the start of the file",
input: 'one|two,two|three\nfour|five,five|six',
config: {},
expected: {
data: [['one','two,two','three'],['four','five,five','six']],
errors: []
}
},
{
description: "Pipe delimiter is guessed correctly choose avgFildCount max one",
notes: "Guessing the delimiter should work choose the min delta one and the max one",
config: {},
input: 'a,b,c\na,b,c|d|e|f',
expected: {
data: [['a', 'b', 'c'], ['a','b','c|d|e|f']],
errors: []
}
},
{
description: "Pipe delimiter is guessed correctly when first field are enclosed in quotes and contain delimiter characters",
notes: "Guessing the delimiter should work if the first field is enclosed in quotes, but others are not",
input: '"Field1,1,1";Field2;"Field3";Field4;Field5;Field6',
config: {},
expected: {
data: [['Field1,1,1','Field2','Field3', 'Field4', 'Field5', 'Field6']],
errors: []
}
},
{
description: "Pipe delimiter is guessed correctly when some fields are enclosed in quotes and contain delimiter characters and escaoped quotes",
notes: "Guessing the delimiter should work even if the first field is not enclosed in quotes, but others are",
input: 'Field1;Field2;"Field,3,""3,3";Field4;Field5;"Field6,6"',
config: {},
expected: {
data: [['Field1','Field2','Field,3,"3,3', 'Field4', 'Field5', 'Field6,6']],
errors: []
}
},
{ {
description: "Single quote as quote character", description: "Single quote as quote character",
notes: "Must parse correctly when single quote is specified as a quote character", notes: "Must parse correctly when single quote is specified as a quote character",
@ -1208,6 +1298,172 @@ var PARSE_TESTS = [
data: [{'a': 'c', 'b': 'd'}], data: [{'a': 'c', 'b': 'd'}],
errors: [] errors: []
} }
},
{
description: "Carriage return in header inside quotes, with line feed endings",
input: '"a\r\na","b"\n"c","d"\n"e","f"\n"g","h"\n"i","j"',
config: {},
expected: {
data: [['a\r\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: []
}
},
{
description: "Line feed in header inside quotes, with carriage return + line feed endings",
input: '"a\na","b"\r\n"c","d"\r\n"e","f"\r\n"g","h"\r\n"i","j"',
config: {},
expected: {
data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: []
}
},
{
description: "Using \\r\\n endings uses \\r\\n linebreak",
input: 'a,b\r\nc,d\r\ne,f\r\ng,h\r\ni,j',
config: {},
expected: {
data: [['a', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\r\n',
delimiter: ',',
cursor: 23,
aborted: false,
truncated: false
}
}
},
{
description: "Using \\n endings uses \\n linebreak",
input: 'a,b\nc,d\ne,f\ng,h\ni,j',
config: {},
expected: {
data: [['a', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\n',
delimiter: ',',
cursor: 19,
aborted: false,
truncated: false
}
}
},
{
description: "Using \\r\\n endings with \\r\\n in header field uses \\r\\n linebreak",
input: '"a\r\na",b\r\nc,d\r\ne,f\r\ng,h\r\ni,j',
config: {},
expected: {
data: [['a\r\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\r\n',
delimiter: ',',
cursor: 28,
aborted: false,
truncated: false
}
}
},
{
description: "Using \\r\\n endings with \\n in header field uses \\r\\n linebreak",
input: '"a\na",b\r\nc,d\r\ne,f\r\ng,h\r\ni,j',
config: {},
expected: {
data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\r\n',
delimiter: ',',
cursor: 27,
aborted: false,
truncated: false
}
}
},
{
description: "Using \\r\\n endings with \\n in header field with skip empty lines uses \\r\\n linebreak",
input: '"a\na",b\r\nc,d\r\ne,f\r\ng,h\r\ni,j\r\n',
config: {skipEmptyLines: true},
expected: {
data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\r\n',
delimiter: ',',
cursor: 29,
aborted: false,
truncated: false
}
}
},
{
description: "Using \\n endings with \\r\\n in header field uses \\n linebreak",
input: '"a\r\na",b\nc,d\ne,f\ng,h\ni,j',
config: {},
expected: {
data: [['a\r\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\n',
delimiter: ',',
cursor: 24,
aborted: false,
truncated: false
}
}
},
{
description: "Using reserved regex character . as quote character",
input: '.a\na.,b\r\nc,d\r\ne,f\r\ng,h\r\ni,j',
config: { quoteChar: '.' },
expected: {
data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\r\n',
delimiter: ',',
cursor: 27,
aborted: false,
truncated: false
}
}
},
{
description: "Using reserved regex character | as quote character",
input: '|a\na|,b\r\nc,d\r\ne,f\r\ng,h\r\ni,j',
config: { quoteChar: '|' },
expected: {
data: [['a\na', 'b'], ['c', 'd'], ['e', 'f'], ['g', 'h'], ['i', 'j']],
errors: [],
meta: {
linebreak: '\r\n',
delimiter: ',',
cursor: 27,
aborted: false,
truncated: false
}
}
},
{
description: "Parsing with skipEmptyLines set to 'greedy'",
notes: "Must parse correctly without lines with no content",
input: 'a,b\n\n,\nc,d\n , \n""," "\n , \n,,,,\n',
config: { skipEmptyLines: 'greedy' },
expected: {
data: [['a', 'b'], ['c', 'd']],
errors: []
}
},
{
description: "Parsing with skipEmptyLines set to 'greedy' with quotes and delimiters as content",
notes: "Must include lines with escaped delimiters and quotes",
input: 'a,b\n\n,\nc,d\n" , ",","\n""" """,""""""\n\n\n',
config: { skipEmptyLines: 'greedy' },
expected: {
data: [['a', 'b'], ['c', 'd'], [' , ', ','], ['" "', '""']],
errors: []
}
} }
]; ];
@ -1215,6 +1471,10 @@ describe('Parse Tests', function() {
function generateTest(test) { function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function() { (test.disabled ? it.skip : it)(test.description, function() {
var actual = Papa.parse(test.input, test.config); var actual = Papa.parse(test.input, test.config);
// allows for testing the meta object if present in the test
if (test.expected.meta) {
assert.deepEqual(actual.meta, test.expected.meta);
}
assert.deepEqual(JSON.stringify(actual.errors), JSON.stringify(test.expected.errors)); assert.deepEqual(JSON.stringify(actual.errors), JSON.stringify(test.expected.errors));
assert.deepEqual(actual.data, test.expected.data); assert.deepEqual(actual.data, test.expected.data);
}); });
@ -1242,7 +1502,7 @@ var PARSE_ASYNC_TESTS = [
}, },
{ {
description: "Simple download", description: "Simple download",
input: "sample.csv", input: BASE_PATH + "sample.csv",
config: { config: {
download: true download: true
}, },
@ -1254,7 +1514,7 @@ var PARSE_ASYNC_TESTS = [
}, },
{ {
description: "Simple download + worker", description: "Simple download + worker",
input: "tests/sample.csv", input: BASE_PATH + "sample.csv",
config: { config: {
worker: true, worker: true,
download: true download: true
@ -1451,6 +1711,30 @@ var UNPARSE_TESTS = [
config: { quotes: [true, false, true] }, config: { quotes: [true, false, true] },
expected: '"Col1",Col2,"Col3"\r\n"a",b,"c"\r\n"d",e,"f"' expected: '"Col1",Col2,"Col3"\r\n"a",b,"c"\r\n"d",e,"f"'
}, },
{
description: "Force quotes around string fields only",
input: [['a', 'b', 'c'], ['d', 10, true]],
config: { quotes: function(value) { return typeof value === 'string'; } },
expected: '"a","b","c"\r\n"d",10,true'
},
{
description: "Force quotes around string fields only (with header row)",
input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": 10, "Col3": true }],
config: { quotes: function(value) { return typeof value === 'string'; } },
expected: '"Col1","Col2","Col3"\r\n"a","b","c"\r\n"d",10,true'
},
{
description: "Force quotes around values in first column only",
input: [['a', 'b', 'c'], ['d', 10, true]],
config: { quotes: function(value, col) { return col === 0; } },
expected: '"a",b,c\r\n"d",10,true'
},
{
description: "Force quotes around values in first column only (with header row)",
input: [{ "Col1": "a", "Col2": "b", "Col3": "c" }, { "Col1": "d", "Col2": 10, "Col3": true }],
config: { quotes: function(value, col) { return col === 0; } },
expected: '"Col1",Col2,Col3\r\n"a",b,c\r\n"d",10,true'
},
{ {
description: "Empty input", description: "Empty input",
input: [], input: [],
@ -1482,6 +1766,61 @@ var UNPARSE_TESTS = [
description: "Date objects are exported in its ISO representation", description: "Date objects are exported in its ISO representation",
input: [{date: new Date("2018-05-04T21:08:03.269Z"), "not a date": 16}, {date: new Date("Tue May 08 2018 08:20:22 GMT-0700 (PDT)"), "not a date": 32}], input: [{date: new Date("2018-05-04T21:08:03.269Z"), "not a date": 16}, {date: new Date("Tue May 08 2018 08:20:22 GMT-0700 (PDT)"), "not a date": 32}],
expected: 'date,not a date\r\n2018-05-04T21:08:03.269Z,16\r\n2018-05-08T15:20:22.000Z,32' expected: 'date,not a date\r\n2018-05-04T21:08:03.269Z,16\r\n2018-05-08T15:20:22.000Z,32'
},
{
description: "Returns empty rows when empty rows are passed and skipEmptyLines is false",
input: [[null, ' '], [], ['1', '2']],
config: {skipEmptyLines: false},
expected: '," "\r\n\r\n1,2'
},
{
description: "Returns without empty rows when skipEmptyLines is true",
input: [[null, ' '], [], ['1', '2']],
config: {skipEmptyLines: true},
expected: '," "\r\n1,2'
},
{
description: "Returns without rows with no content when skipEmptyLines is 'greedy'",
input: [[null, ' '], [], ['1', '2']],
config: {skipEmptyLines: 'greedy'},
expected: '1,2'
},
{
description: "Returns empty rows when empty rows are passed and skipEmptyLines is false with headers",
input: [{a: null, b: ' '}, {}, {a: '1', b: '2'}],
config: {skipEmptyLines: false, header: true},
expected: 'a,b\r\n," "\r\n\r\n1,2'
},
{
description: "Returns without empty rows when skipEmptyLines is true with headers",
input: [{a: null, b: ' '}, {}, {a: '1', b: '2'}],
config: {skipEmptyLines: true, header: true},
expected: 'a,b\r\n," "\r\n1,2'
},
{
description: "Returns without rows with no content when skipEmptyLines is 'greedy' with headers",
input: [{a: null, b: ' '}, {}, {a: '1', b: '2'}],
config: {skipEmptyLines: 'greedy', header: true},
expected: 'a,b\r\n1,2'
},
{
description: "Column option used to manually specify keys",
notes: "Should not throw any error when attempting to serialize key not present in object. Columns are different than keys of the first object. When an object is missing a key then the serialized value should be an empty string.",
input: [{a: 1, b: '2'}, {}, {a: 3, d: 'd', c: 4,}],
config: {columns: ['a', 'b', 'c']},
expected: 'a,b,c\r\n1,2,\r\n\r\n3,,4'
},
{
description: "Use different escapeChar",
input: [{a: 'foo', b: '"quoted"'}],
config: {header: false, escapeChar: '\\'},
expected: 'foo,"\\"quoted\\""'
},
{
description: "test defeault escapeChar",
input: [{a: 'foo', b: '"quoted"'}],
config: {header: false},
expected: 'foo,"""quoted"""'
} }
]; ];
@ -1511,6 +1850,49 @@ describe('Unparse Tests', function() {
var CUSTOM_TESTS = [ var CUSTOM_TESTS = [
{
description: "Pause and resume works (Regression Test for Bug #636)",
disabled: !XHR_ENABLED,
timeout: 30000,
expected: [2001, [
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Lorem ipsum dolor sit","42","ABC"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Etiam a dolor vitae est vestibulum","84"],
["Lorem ipsum dolor sit","42","ABC"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Etiam a dolor vitae est vestibulum","84","DEF"],
["Lorem ipsum dolor sit","42","ABC"],
["Lorem ipsum dolor sit","42"]
], 0],
run: function(callback) {
var stepped = 0;
var dataRows = [];
var errorCount = 0;
var output = [];
Papa.parse(BASE_PATH + "verylong-sample.csv", {
download: true,
step: function(results, parser) {
stepped++;
if (results)
{
parser.pause();
parser.resume();
if (results.data && stepped % 200 === 0) {
dataRows.push(results.data);
}
}
},
complete: function() {
output.push(stepped);
output.push(dataRows);
output.push(errorCount);
callback(output);
}
});
}
},
{ {
description: "Complete is called with all results if neither step nor chunk is defined", description: "Complete is called with all results if neither step nor chunk is defined",
expected: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']], expected: [['A', 'b', 'c'], ['d', 'E', 'f'], ['G', 'h', 'i']],
@ -1539,13 +1921,77 @@ var CUSTOM_TESTS = [
}); });
} }
}, },
{
description: "Data is correctly parsed with steps",
expected: [['A', 'b', 'c'], ['d', 'E', 'f']],
run: function(callback) {
var data = [];
Papa.parse('A,b,c\nd,E,f', {
step: function(results) {
data.push(results.data);
},
complete: function() {
callback(data);
}
});
}
},
{
description: "Data is correctly parsed with steps (headers)",
expected: [{One: 'A', Two: 'b', Three: 'c'}, {One: 'd', Two: 'E', Three: 'f'}],
run: function(callback) {
var data = [];
Papa.parse('One,Two,Three\nA,b,c\nd,E,f', {
header: true,
step: function(results) {
data.push(results.data);
},
complete: function() {
callback(data);
}
});
}
},
{
description: "Data is correctly parsed with steps and worker (headers)",
expected: [{One: 'A', Two: 'b', Three: 'c'}, {One: 'd', Two: 'E', Three: 'f'}],
run: function(callback) {
var data = [];
Papa.parse('One,Two,Three\nA,b,c\nd,E,f', {
header: true,
worker: true,
step: function(results) {
data.push(results.data);
},
complete: function() {
callback(data);
}
});
}
},
{
description: "Data is correctly parsed with steps and worker",
expected: [['A', 'b', 'c'], ['d', 'E', 'f']],
run: function(callback) {
var data = [];
Papa.parse('A,b,c\nd,E,f', {
worker: true,
step: function(results) {
data.push(results.data);
},
complete: function() {
callback(data);
}
});
}
},
{ {
description: "Step is called with the contents of the row", description: "Step is called with the contents of the row",
expected: ['A', 'b', 'c'], expected: ['A', 'b', 'c'],
run: function(callback) { run: function(callback) {
Papa.parse('A,b,c', { Papa.parse('A,b,c', {
step: function(response) { step: function(response) {
callback(response.data[0]); callback(response.data);
} }
}); });
} }
@ -1571,7 +2017,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
step: function(response) { step: function(response) {
updates.push(response.meta.cursor); updates.push(response.meta.cursor);
@ -1588,7 +2034,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
chunkSize: 500, chunkSize: 500,
step: function(response) { step: function(response) {
@ -1606,7 +2052,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
chunkSize: 500, chunkSize: 500,
worker: true, worker: true,
@ -1625,7 +2071,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
chunkSize: 500, chunkSize: 500,
chunk: function(response) { chunk: function(response) {
@ -1643,7 +2089,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = []; var updates = [];
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
chunkSize: 500, chunkSize: 500,
chunk: function(response) { chunk: function(response) {
@ -1767,7 +2213,7 @@ var CUSTOM_TESTS = [
Papa.parse(new File(['A,B,C\nX,"Y\n1\n2\n3",Z'], 'sample.csv'), { Papa.parse(new File(['A,B,C\nX,"Y\n1\n2\n3",Z'], 'sample.csv'), {
chunkSize: 3, chunkSize: 3,
step: function(response) { step: function(response) {
updates.push(response.data[0]); updates.push(response.data);
}, },
complete: function() { complete: function() {
callback(updates); callback(updates);
@ -1782,7 +2228,7 @@ var CUSTOM_TESTS = [
var updates = []; var updates = [];
Papa.parse('A,b,c\nd,E,f\nG,h,i', { Papa.parse('A,b,c\nd,E,f\nG,h,i', {
step: function(response, handle) { step: function(response, handle) {
updates.push(response.data[0]); updates.push(response.data);
handle.abort(); handle.abort();
callback(updates); callback(updates);
}, },
@ -1812,7 +2258,7 @@ var CUSTOM_TESTS = [
var updates = []; var updates = [];
Papa.parse('A,b,c\nd,E,f\nG,h,i', { Papa.parse('A,b,c\nd,E,f\nG,h,i', {
step: function(response, handle) { step: function(response, handle) {
updates.push(response.data[0]); updates.push(response.data);
handle.pause(); handle.pause();
callback(updates); callback(updates);
}, },
@ -1831,7 +2277,7 @@ var CUSTOM_TESTS = [
var first = true; var first = true;
Papa.parse('A,b,c\nd,E,f\nG,h,i', { Papa.parse('A,b,c\nd,E,f\nG,h,i', {
step: function(response, h) { step: function(response, h) {
updates.push(response.data[0]); updates.push(response.data);
if (!first) return; if (!first) return;
handle = h; handle = h;
handle.pause(); handle.pause();
@ -1852,7 +2298,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = 0; var updates = 0;
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
worker: true, worker: true,
download: true, download: true,
chunkSize: 500, chunkSize: 500,
@ -1872,7 +2318,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = 0; var updates = 0;
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
chunkSize: 500, chunkSize: 500,
beforeFirstChunk: function(chunk) { beforeFirstChunk: function(chunk) {
@ -1893,7 +2339,7 @@ var CUSTOM_TESTS = [
disabled: !XHR_ENABLED, disabled: !XHR_ENABLED,
run: function(callback) { run: function(callback) {
var updates = 0; var updates = 0;
Papa.parse("/tests/long-sample.csv", { Papa.parse(BASE_PATH + "long-sample.csv", {
download: true, download: true,
chunkSize: 500, chunkSize: 500,
beforeFirstChunk: function(chunk) { beforeFirstChunk: function(chunk) {
@ -1908,42 +2354,31 @@ var CUSTOM_TESTS = [
} }
}, },
{ {
description: "Should not assume we own the worker unless papaworker is in the search string", description: "Should correctly guess custom delimiter when passed delimiters to guess.",
disabled: typeof Worker === 'undefined', expected: "~",
expected: [false, true, true, true, true],
run: function(callback) { run: function(callback) {
var searchStrings = [ var results = Papa.parse('"A"~"B"~"C"~"D"', {
'', delimitersToGuess: ['~', '@', '%']
'?papaworker',
'?x=1&papaworker',
'?x=1&papaworker&y=1',
'?x=1&papaworker=1'
];
var results = searchStrings.map(function() { return false; });
var workers = [];
// Give it .5s to do something
setTimeout(function() {
workers.forEach(function(w) { w.terminate(); });
callback(results);
}, 500);
searchStrings.forEach(function(searchString, idx) {
var w = new Worker('../papaparse.js' + searchString);
workers.push(w);
w.addEventListener('message', function() {
results[idx] = true;
});
w.postMessage({input: 'a,b,c\n1,2,3'});
}); });
callback(results.meta.delimiter);
}
},
{
description: "Should still correctly guess default delimiters when delimiters to guess are not given.",
expected: ",",
run: function(callback) {
var results = Papa.parse('"A","B","C","D"');
callback(results.meta.delimiter);
} }
} }
]; ];
describe('Custom Tests', function() { describe('Custom Tests', function() {
function generateTest(test) { function generateTest(test) {
(test.disabled ? it.skip : it)(test.description, function(done) { (test.disabled ? it.skip : it)(test.description, function(done) {
if(test.timeout) {
this.timeout(test.timeout);
}
test.run(function(actual) { test.run(function(actual) {
assert.deepEqual(JSON.stringify(actual), JSON.stringify(test.expected)); assert.deepEqual(JSON.stringify(actual), JSON.stringify(test.expected));
done(); done();

4
tests/test.js

@ -5,8 +5,8 @@ var path = require('path');
var childProcess = require('child_process'); var childProcess = require('child_process');
var server = connect().use(serveStatic(path.join(__dirname, '/..'))).listen(8071, function() { var server = connect().use(serveStatic(path.join(__dirname, '/..'))).listen(8071, function() {
if (process.argv.indexOf('--phantomjs') !== -1) { if (process.argv.indexOf('--mocha-headless-chrome') !== -1) {
childProcess.spawn('node_modules/.bin/mocha-phantomjs', ['http://localhost:8071/tests/tests.html'], { childProcess.spawn('node_modules/.bin/mocha-headless-chrome', ['-f', 'http://localhost:8071/tests/tests.html'], {
stdio: 'inherit' stdio: 'inherit'
}).on('exit', function(code) { }).on('exit', function(code) {
server.close(); server.close();

13
tests/tests.html

@ -6,22 +6,17 @@
<link rel="stylesheet" href="../node_modules/mocha/mocha.css" /> <link rel="stylesheet" href="../node_modules/mocha/mocha.css" />
<script src="../papaparse.js"></script> <script src="../papaparse.js"></script>
<script src="http://chaijs.com/chai.js"></script> <script src="../node_modules/chai/chai.js"></script>
<script>mocha.setup('bdd')</script> <script>mocha.setup('bdd')</script>
<script src="test-cases.js"></script> <script src="test-cases.js" id="test-cases"></script>
</head> </head>
<body> <body>
<div id="mocha"></div> <div id="mocha"></div>
<script> <script>
if (window.mochaPhantomJS) { mocha.checkLeaks();
mochaPhantomJS.run(); mocha.run();
} else {
mocha.checkLeaks();
mocha.run();
}
</script> </script>
</body> </body>
</html> </html>

Loading…
Cancel
Save