@node-red/nodes 3.1.6 → 4.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,6 +14,7 @@
14
14
  <option value="html" data-i18n="html.output.html"></option>
15
15
  <option value="text" data-i18n="html.output.text"></option>
16
16
  <option value="attr" data-i18n="html.output.attr"></option>
17
+ <option value="compl" data-i18n="html.output.compl"></option>
17
18
  <!-- <option value="val">return the value from a form element</option> -->
18
19
  </select>
19
20
  </div>
@@ -28,6 +29,10 @@
28
29
  <label for="node-input-outproperty">&nbsp;</label>
29
30
  <span data-i18n="html.label.in" style="padding-left:8px; padding-right:2px; vertical-align:-1px;"></span> <input type="text" id="node-input-outproperty" style="width:64%">
30
31
  </div>
32
+ <div id='html-prefix-row' class="form-row" style="display: none;">
33
+ <label for="node-input-chr" style="width: 230px;"><i class="fa fa-tag"></i> <span data-i18n="html.label.prefix"></span></label>
34
+ <input type="text" id="node-input-chr" style="text-align:center; width: 40px;" placeholder="_">
35
+ </div>
31
36
  <br/>
32
37
  <div class="form-row">
33
38
  <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
@@ -45,7 +50,8 @@
45
50
  outproperty: {value:"payload", validate: RED.validators.typedInput({ type: 'msg', allowUndefined: true }) },
46
51
  tag: {value:""},
47
52
  ret: {value:"html"},
48
- as: {value:"single"}
53
+ as: {value:"single"},
54
+ chr: { value: "_" }
49
55
  },
50
56
  inputs:1,
51
57
  outputs:1,
@@ -59,6 +65,13 @@
59
65
  oneditprepare: function() {
60
66
  $("#node-input-property").typedInput({default:'msg',types:['msg']});
61
67
  $("#node-input-outproperty").typedInput({default:'msg',types:['msg']});
68
+ $('#node-input-ret').on( 'change', () => {
69
+ if ( $('#node-input-ret').val() == "compl" ) {
70
+ $('#html-prefix-row').show()
71
+ } else {
72
+ $('#html-prefix-row').hide()
73
+ }
74
+ });
62
75
  }
63
76
  });
64
77
  </script>
@@ -25,6 +25,7 @@ module.exports = function(RED) {
25
25
  this.tag = n.tag;
26
26
  this.ret = n.ret || "html";
27
27
  this.as = n.as || "single";
28
+ this.chr = n.chr || "_";
28
29
  var node = this;
29
30
  this.on("input", function(msg,send,done) {
30
31
  var value = RED.util.getMessageProperty(msg,node.property);
@@ -47,6 +48,11 @@ module.exports = function(RED) {
47
48
  if (node.ret === "attr") {
48
49
  pay2 = Object.assign({},this.attribs);
49
50
  }
51
+ if (node.ret === "compl") {
52
+ var bse = {}
53
+ bse[node.chr] = $(this).html().trim()
54
+ pay2 = Object.assign(bse, this.attribs);
55
+ }
50
56
  //if (node.ret === "val") { pay2 = $(this).val(); }
51
57
  /* istanbul ignore else */
52
58
  if (pay2) {
@@ -69,6 +75,11 @@ module.exports = function(RED) {
69
75
  var attribs = Object.assign({},this.attribs);
70
76
  pay.push( attribs );
71
77
  }
78
+ if (node.ret === "compl") {
79
+ var bse = {}
80
+ bse[node.chr] = $(this).html().trim()
81
+ pay.push( Object.assign(bse, this.attribs) )
82
+ }
72
83
  //if (node.ret === "val") { pay.push( $(this).val() ); }
73
84
  }
74
85
  index++;
@@ -0,0 +1,324 @@
1
+
2
+ /**
3
+ * @typedef {Object} CSVParseOptions
4
+ * @property {number} [cursor=0] - an index into the CSV to start parsing from
5
+ * @property {string} [separator=','] - the separator character
6
+ * @property {string} [quote='"'] - the quote character
7
+ * @property {boolean} [headersOnly=false] - only parse the headers and return them
8
+ * @property {string[]} [headers=[]] - an array of headers to use instead of the first row of the CSV data
9
+ * @property {boolean} [dataHasHeaderRow=true] - whether the CSV data to parse has a header row
10
+ * @property {boolean} [outputHeader=true] - whether the output data should include a header row (only applies to array output)
11
+ * @property {boolean} [parseNumeric=false] - parse numeric values into numbers
12
+ * @property {boolean} [includeNullValues=false] - include null values in the output
13
+ * @property {boolean} [includeEmptyStrings=true] - include empty strings in the output
14
+ * @property {string} [outputStyle='object'] - output an array of arrays or an array of objects
15
+ * @property {boolean} [strict=false] - throw an error if the CSV is malformed
16
+ */
17
+
18
+ /**
19
+ * Parses a CSV string into an array of arrays or an array of objects.
20
+ *
21
+ * NOTES:
22
+ * * Deviations from the RFC4180 spec (for the sake of user fiendliness, system implementations and flexibility), this parser will:
23
+ * * accept any separator character, not just `,`
24
+ * * accept any quote character, not just `"`
25
+ * * parse `\r`, `\n` or `\r\n` as line endings (RRFC4180 2.1 states lines are separated by CRLF)
26
+ * * Only single character `quote` is supported
27
+ * * `quote` is `"` by default
28
+ * * Any cell that contains a `quote` or `separator` will be quoted
29
+ * * Any `quote` characters inside a cell will be escaped as per RFC 4180 2.6
30
+ * * Only single character `separator` is supported
31
+ * * Only `array` and `object` output styles are supported
32
+ * * `array` output style is an array of arrays [[],[],[]]
33
+ * * `object` output style is an array of objects [{},{},{}]
34
+ * * Only `headers` or `dataHasHeaderRow` are supported, not both
35
+ * @param {string} csvIn - the CSV string to parse
36
+ * @param {CSVParseOptions} parseOptions - options
37
+ * @throws {Error}
38
+ */
39
+ function parse(csvIn, parseOptions) {
40
+ /* Normalise options */
41
+ parseOptions = parseOptions || {};
42
+ const separator = parseOptions.separator ?? ',';
43
+ const quote = parseOptions.quote ?? '"';
44
+ const headersOnly = parseOptions.headersOnly ?? false;
45
+ const headers = Array.isArray(parseOptions.headers) ? parseOptions.headers : []
46
+ const dataHasHeaderRow = parseOptions.dataHasHeaderRow ?? true;
47
+ const outputHeader = parseOptions.outputHeader ?? true;
48
+ const parseNumeric = parseOptions.parseNumeric ?? false;
49
+ const includeNullValues = parseOptions.includeNullValues ?? false;
50
+ const includeEmptyStrings = parseOptions.includeEmptyStrings ?? true;
51
+ const outputStyle = ['array', 'object'].includes(parseOptions.outputStyle) ? parseOptions.outputStyle : 'object'; // 'array [[],[],[]]' or 'object [{},{},{}]
52
+ const strict = parseOptions.strict ?? false
53
+
54
+ /* Local variables */
55
+ const cursorMax = csvIn.length;
56
+ const ouputArrays = outputStyle === 'array';
57
+ const headersSupplied = headers.length > 0
58
+ // The original regex was an "is-a-number" positive logic test. /^ *[-]?(?!E)(?!0\d)\d*\.?\d*(E-?\+?)?\d+ *$/i;
59
+ // Below, is less strict and inverted logic but coupled with +cast it is 13%+ faster than original regex+parsefloat
60
+ // and has the benefit of understanding hexadecimals, binary and octal numbers.
61
+ const skipNumberConversion = /^ *(\+|-0\d|0\d)/
62
+ const cellBuilder = []
63
+ let rowBuilder = []
64
+ let cursor = typeof parseOptions.cursor === 'number' ? parseOptions.cursor : 0;
65
+ let newCell = true, inQuote = false, closed = false, output = [];
66
+
67
+ /* inline helper functions */
68
+ const finaliseCell = () => {
69
+ let cell = cellBuilder.join('')
70
+ cellBuilder.length = 0
71
+ // push the cell:
72
+ // NOTE: if cell is empty but newCell==true, then this cell had zero chars - push `null`
73
+ // otherwise push empty string
74
+ return rowBuilder.push(cell || (newCell ? null : ''))
75
+ }
76
+ const finaliseRow = () => {
77
+ if (cellBuilder.length) {
78
+ finaliseCell()
79
+ }
80
+ if (rowBuilder.length) {
81
+ output.push(rowBuilder)
82
+ rowBuilder = []
83
+ }
84
+ }
85
+
86
+ /* Main parsing loop */
87
+ while (cursor < cursorMax) {
88
+ const char = csvIn[cursor]
89
+ if (inQuote) {
90
+ if (char === quote && csvIn[cursor + 1] === quote) {
91
+ cellBuilder.push(quote)
92
+ cursor += 2;
93
+ newCell = false;
94
+ closed = false;
95
+ } else if (char === quote) {
96
+ inQuote = false;
97
+ cursor += 1;
98
+ newCell = false;
99
+ closed = true;
100
+ } else {
101
+ cellBuilder.push(char)
102
+ newCell = false;
103
+ closed = false;
104
+ cursor++;
105
+ }
106
+ } else {
107
+ if (char === separator) {
108
+ finaliseCell()
109
+ cursor += 1;
110
+ newCell = true;
111
+ closed = false;
112
+ } else if (char === quote) {
113
+ if (newCell) {
114
+ inQuote = true;
115
+ cursor += 1;
116
+ newCell = false;
117
+ closed = false;
118
+ }
119
+ else if (strict) {
120
+ throw new UnquotedQuoteError(cursor)
121
+ } else {
122
+ // not strict, keep 1 quote if the next char is not a cell/record separator
123
+ cursor++
124
+ if (csvIn[cursor] && csvIn[cursor] !== '\n' && csvIn[cursor] !== '\r' && csvIn[cursor] !== separator) {
125
+ cellBuilder.push(char)
126
+ if (csvIn[cursor] === quote) {
127
+ cursor++ // skip the next quote
128
+ }
129
+ }
130
+ }
131
+ } else {
132
+ if (char === '\n' || char === '\r') {
133
+ finaliseRow()
134
+ if (csvIn[cursor + 1] === '\n') {
135
+ cursor += 2;
136
+ } else {
137
+ cursor++
138
+ }
139
+ newCell = true;
140
+ closed = false;
141
+ if (headersOnly) {
142
+ break
143
+ }
144
+ } else {
145
+ if (closed) {
146
+ if (strict) {
147
+ throw new DataAfterCloseError(cursor)
148
+ } else {
149
+ cursor--; // move back to grab the previously discarded char
150
+ closed = false
151
+ }
152
+ } else {
153
+ cellBuilder.push(char)
154
+ newCell = false;
155
+ cursor++;
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ if (strict && inQuote) {
162
+ throw new ParseError(`Missing quote, unclosed cell`, cursor)
163
+ }
164
+ // finalise the last cell/row
165
+ finaliseRow()
166
+ let firstRowIsHeader = false
167
+ // if no headers supplied, generate them
168
+ if (output.length >= 1) {
169
+ if (headersSupplied) {
170
+ // headers already supplied
171
+ } else if (dataHasHeaderRow) {
172
+ // take the first row as the headers
173
+ headers.push(...output[0])
174
+ firstRowIsHeader = true
175
+ } else {
176
+ // generate headers col1, col2, col3, etc
177
+ for (let i = 0; i < output[0].length; i++) {
178
+ headers.push("col" + (i + 1))
179
+ }
180
+ }
181
+ }
182
+
183
+ const finalResult = {
184
+ /** @type {String[]} headers as an array of string */
185
+ headers: headers,
186
+ /** @type {String} headers as a comma-separated string */
187
+ header: null,
188
+ /** @type {Any[]} Result Data (may include header row: check `firstRowIsHeader` flag) */
189
+ data: [],
190
+ /** @type {Boolean|undefined} flag to indicate if the first row is a header row (only applies when `outputStyle` is 'array') */
191
+ firstRowIsHeader: undefined,
192
+ /** @type {'array'|'object'} flag to indicate the output style */
193
+ outputStyle: outputStyle,
194
+ /** @type {Number} The current cursor position */
195
+ cursor: cursor,
196
+ }
197
+
198
+ const quotedHeaders = []
199
+ for (let i = 0; i < headers.length; i++) {
200
+ if (!headers[i]) {
201
+ continue
202
+ }
203
+ quotedHeaders.push(quoteCell(headers[i], { quote, separator: ',' }))
204
+ }
205
+ finalResult.header = quotedHeaders.join(',') // always quote headers and join with comma
206
+
207
+ // output is an array of arrays [[],[],[]]
208
+ if (ouputArrays || headersOnly) {
209
+ if (!firstRowIsHeader && !headersOnly && outputHeader && headers.length > 0) {
210
+ if (output.length > 0) {
211
+ output.unshift(headers)
212
+ } else {
213
+ output = [headers]
214
+ }
215
+ firstRowIsHeader = true
216
+ }
217
+ if (headersOnly) {
218
+ delete finalResult.firstRowIsHeader
219
+ return finalResult
220
+ }
221
+ finalResult.firstRowIsHeader = firstRowIsHeader
222
+ finalResult.data = (firstRowIsHeader && !outputHeader) ? output.slice(1) : output
223
+ return finalResult
224
+ }
225
+
226
+ // output is an array of objects [{},{},{}]
227
+ const outputObjects = []
228
+ let i = firstRowIsHeader ? 1 : 0
229
+ for (; i < output.length; i++) {
230
+ const rowObject = {}
231
+ let isEmpty = true
232
+ for (let j = 0; j < headers.length; j++) {
233
+ if (!headers[j]) {
234
+ continue
235
+ }
236
+ let v = output[i][j] === undefined ? null : output[i][j]
237
+ if (v === null && !includeNullValues) {
238
+ continue
239
+ } else if (v === "" && !includeEmptyStrings) {
240
+ continue
241
+ } else if (parseNumeric === true && v && !skipNumberConversion.test(v)) {
242
+ const vTemp = +v
243
+ const isNumber = !isNaN(vTemp)
244
+ if(isNumber) {
245
+ v = vTemp
246
+ }
247
+ }
248
+ rowObject[headers[j]] = v
249
+ isEmpty = false
250
+ }
251
+ // determine if this row is empty
252
+ if (!isEmpty) {
253
+ outputObjects.push(rowObject)
254
+ }
255
+ }
256
+ finalResult.data = outputObjects
257
+ delete finalResult.firstRowIsHeader
258
+ return finalResult
259
+ }
260
+
261
+ /**
262
+ * Quotes a cell in a CSV string if necessary. Addiionally, any double quotes inside the cell will be escaped as per RFC 4180 2.6 (https://datatracker.ietf.org/doc/html/rfc4180#section-2).
263
+ * @param {string} cell - the string to quote
264
+ * @param {*} options - options
265
+ * @param {string} [options.quote='"'] - the quote character
266
+ * @param {string} [options.separator=','] - the separator character
267
+ * @param {string[]} [options.quoteables] - an array of characters that, when encountered, will trigger the application of outer quotes
268
+ * @returns
269
+ */
270
+ function quoteCell(cell, { quote = '"', separator = ",", quoteables } = {
271
+ quote: '"',
272
+ separator: ",",
273
+ quoteables: [quote, separator, '\r', '\n']
274
+ }) {
275
+ quoteables = quoteables || [quote, separator, '\r', '\n'];
276
+
277
+ let doubleUp = false;
278
+ if (cell.indexOf(quote) !== -1) { // add double quotes if any quotes
279
+ doubleUp = true;
280
+ }
281
+ const quoteChar = quoteables.some(q => cell.includes(q)) ? quote : '';
282
+ return quoteChar + (doubleUp ? cell.replace(/"/g, '""') : cell) + quoteChar;
283
+ }
284
+
285
+ // #region Custom Error Classes
286
+ class ParseError extends Error {
287
+ /**
288
+ * @param {string} message - the error message
289
+ * @param {number} cursor - the cursor index where the error occurred
290
+ */
291
+ constructor(message, cursor) {
292
+ super(message)
293
+ this.name = 'ParseError'
294
+ this.cursor = cursor
295
+ }
296
+ }
297
+
298
+ class UnquotedQuoteError extends ParseError {
299
+ /**
300
+ * @param {number} cursor - the cursor index where the error occurred
301
+ */
302
+ constructor(cursor) {
303
+ super('Quote found in the middle of an unquoted field', cursor)
304
+ this.name = 'UnquotedQuoteError'
305
+ }
306
+ }
307
+
308
+ class DataAfterCloseError extends ParseError {
309
+ /**
310
+ * @param {number} cursor - the cursor index where the error occurred
311
+ */
312
+ constructor(cursor) {
313
+ super('Data found after closing quote', cursor)
314
+ this.name = 'DataAfterCloseError'
315
+ }
316
+ }
317
+
318
+ // #endregion
319
+
320
+ exports.parse = parse
321
+ exports.quoteCell = quoteCell
322
+ exports.ParseError = ParseError
323
+ exports.UnquotedQuoteError = UnquotedQuoteError
324
+ exports.DataAfterCloseError = DataAfterCloseError
@@ -15,7 +15,11 @@
15
15
  -->
16
16
 
17
17
  <script type="text/html" data-template-name="split">
18
- <div class="form-row"><span data-i18n="[html]split.intro"></span></div>
18
+ <!-- <div class="form-row"><span data-i18n="[html]split.intro"></span></div> -->
19
+ <div class="form-row">
20
+ <label for="node-input-property"><i class="fa fa-forward"></i> <span data-i18n="split.split"></span></label>
21
+ <input type="text" id="node-input-property" style="width:70%;"/>
22
+ </div>
19
23
  <div class="form-row"><span data-i18n="[html]split.strBuff"></span></div>
20
24
  <div class="form-row">
21
25
  <label for="node-input-splt" style="padding-left:10px; margin-right:-10px;" data-i18n="split.splitUsing"></label>
@@ -39,10 +43,9 @@
39
43
  <label for="node-input-addname-cb" style="width:auto;" data-i18n="split.addname"></label>
40
44
  <input type="text" id="node-input-addname" style="width:70%">
41
45
  </div>
42
- <hr/>
43
46
  <div class="form-row">
44
- <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="common.label.name"></span></label>
45
- <input type="text" id="node-input-name" data-i18n="[placeholder]common.label.name">
47
+ <label for="node-input-name"><i class="fa fa-tag"></i> <span data-i18n="node-red:common.label.name"></span></label>
48
+ <input type="text" id="node-input-name" data-i18n="[placeholder]node-red:common.label.name">
46
49
  </div>
47
50
  </script>
48
51
 
@@ -57,7 +60,8 @@
57
60
  arraySplt: {value:1},
58
61
  arraySpltType: {value:"len"},
59
62
  stream: {value:false},
60
- addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })}
63
+ addname: {value:"", validate: RED.validators.typedInput({ type: 'msg', allowBlank: true })},
64
+ property: {value:"payload",required:true}
61
65
  },
62
66
  inputs:1,
63
67
  outputs:1,
@@ -69,6 +73,10 @@
69
73
  return this.name?"node_label_italic":"";
70
74
  },
71
75
  oneditprepare: function() {
76
+ if (this.property === undefined) {
77
+ $("#node-input-property").val("payload");
78
+ }
79
+ $("#node-input-property").typedInput({default:'msg',types:['msg']});
72
80
  $("#node-input-splt").typedInput({
73
81
  default: 'str',
74
82
  typeField: $("#node-input-spltType"),