Skip to content

Commit 6fcdd7d

Browse files
authored
feat: add lineMode option to parser for flexible line formatting (#7)
1 parent dccc721 commit 6fcdd7d

File tree

3 files changed

+89
-66
lines changed

3 files changed

+89
-66
lines changed

README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ parser.parseFile('example.nc', function(err, results) {
8080
### batchSize
8181

8282
Type: `Number`
83-
Default: `1000`
83+
Default: 1000
8484

8585
The batch size.
8686

@@ -99,16 +99,27 @@ parser.parseLine('G0 X0 Y0', { flatten: true });
9999
// => { line: 'G0 X0 Y0', words: [ 'G0', 'X0', 'Y0' ] }
100100
```
101101

102-
### noParseLine
102+
### lineMode
103103

104-
Type: `Boolean`
105-
Default: `false`
104+
Type: `String`
105+
Default: `'original'`
106106

107-
True to not parse line, false otherwise.
107+
The `lineMode` option specifies how the parsed line should be formatted. The following values are supported:
108+
- `'original'`: Keeps the line unchanged, including comments and whitespace. (Default)
109+
- `'minimal'`: Removes comments, trims leading and trailing whitespace, but preserves inner whitespace.
110+
- `'compact'`: Removes both comments and all whitespace.
111+
112+
Example usage:
108113

109114
```js
110-
parser.parseFile('/path/to/file', { noParseLine: true }, function(err, results) {
111-
});
115+
parser.parseLine('G0 X0 Y0 ; comment', { lineMode: 'original' });
116+
// => { line: 'G0 X0 Y0 ; comment', words: [ [ 'G', 0 ], [ 'X', 0 ], [ 'Y', 0 ] ] }
117+
118+
parser.parseLine('G0 X0 Y0 ; comment', { lineMode: 'minimal' });
119+
// => { line: 'G0 X0 Y0', words: [ [ 'G', 0 ], [ 'X', 0 ], [ 'Y', 0 ] ] }
120+
121+
parser.parseLine('G0 X0 Y0 ; comment', { lineMode: 'compact' });
122+
// => { line: 'G0X0Y0', words: [ [ 'G', 0 ], [ 'X', 0 ], [ 'Y', 0 ] ] }
112123
```
113124

114125
## G-code Interpreter

src/__tests__/index.test.js

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -43,44 +43,48 @@ describe('Pass an empty text as the first argument', () => {
4343
});
4444
});
4545

46-
describe('Contains only lines', () => {
47-
it('should not parse G-code commands.', (done) => {
48-
const filepath = path.resolve(__dirname, 'fixtures/circle.gcode');
49-
parseFile(filepath, { noParseLine: true }, (err, results) => {
50-
expect(results.length).toBe(7);
51-
done();
52-
})
53-
.on('data', (data) => {
54-
expect(typeof data).toBe('object');
55-
expect(typeof data.line).toBe('string');
56-
expect(data.words).toBe(undefined);
57-
})
58-
.on('end', (results) => {
59-
expect(results.length).toBe(7);
60-
});
61-
});
62-
});
63-
6446
describe('Invalid G-code words', () => {
65-
it('should ignore invalid g-code words', (done) => {
47+
it('should ignore invalid g-code words', () => {
6648
const data = parseLine('messed up');
6749
expect(typeof data).toBe('object');
6850
expect(data.line).toBe('messed up');
6951
expect(data.words).toHaveLength(0);
70-
done();
52+
});
53+
});
54+
55+
describe('Using the `lineMode` option', () => {
56+
it('should return the original line with comments and whitespace in original mode', () => {
57+
const line = 'M6 (tool change;) T1 ; comment';
58+
const result = parseLine(line, { lineMode: 'original' });
59+
expect(result.line).toBe('M6 (tool change;) T1 ; comment');
60+
expect(result.words).toEqual([['M', 6], ['T', 1]]);
61+
});
62+
63+
it('should return the line without comments but with whitespace in minimal mode', () => {
64+
const line = 'M6 (tool change;) T1 ; comment';
65+
const result = parseLine(line, { lineMode: 'minimal' });
66+
expect(result.line).toBe('M6 T1');
67+
expect(result.words).toEqual([['M', 6], ['T', 1]]);
68+
});
69+
70+
it('should return the line without comments and whitespace in compact mode', () => {
71+
const line = 'M6 (tool change;) T1 ; comment';
72+
const result = parseLine(line, { lineMode: 'compact' });
73+
expect(result.line).toBe('M6T1');
74+
expect(result.words).toEqual([['M', 6], ['T', 1]]);
7175
});
7276
});
7377

7478
describe('Commands', () => {
75-
it('should be able to parse $ command (e.g. Grbl).', (done) => {
79+
it('should be able to parse $ command (e.g. Grbl).', () => {
7680
const data = parseLine('$H $C');
7781
expect(typeof data).toBe('object');
7882
expect(typeof data.line).toBe('string');
7983
expect(data.words).toHaveLength(0);
8084
expect(data.cmds).toEqual(['$H', '$C']);
81-
done();
8285
});
83-
it('should be able to parse JSON command (e.g. TinyG, g2core).', (done) => {
86+
87+
it('should be able to parse JSON command (e.g. TinyG, g2core).', () => {
8488
{ // {sr:{spe:t,spd,sps:t}}
8589
const data = parseLine('{sr:{spe:t,spd:t,sps:t}}');
8690
expect(typeof data).toBe('object');
@@ -95,10 +99,9 @@ describe('Commands', () => {
9599
expect(data.words).toHaveLength(0);
96100
expect(data.cmds).toEqual(['{mt:n}']);
97101
}
98-
99-
done();
100102
});
101-
it('should be able to parse % command (e.g. bCNC, CNCjs).', (done) => {
103+
104+
it('should be able to parse % command (e.g. bCNC, CNCjs).', () => {
102105
{ // %wait
103106
const data = parseLine('%wait');
104107
expect(typeof data).toBe('object');
@@ -138,8 +141,6 @@ describe('Commands', () => {
138141
expect(data.words).toHaveLength(0);
139142
expect(data.cmds).toEqual(['%x0=posx,y0=posy,z0=posz']);
140143
}
141-
142-
done();
143144
});
144145
});
145146

@@ -323,7 +324,7 @@ describe('Event listeners', () => {
323324
});
324325

325326
describe('parseLine()', () => {
326-
it('should return expected results.', (done) => {
327+
it('should return expected results.', () => {
327328
expect(parseLine('G0 X0 Y0')).toEqual({
328329
line: 'G0 X0 Y0',
329330
words: [['G', 0], ['X', 0], ['Y', 0]]
@@ -332,7 +333,6 @@ describe('parseLine()', () => {
332333
line: 'G0 X0 Y0',
333334
words: ['G0', 'X0', 'Y0']
334335
});
335-
done();
336336
});
337337
});
338338

@@ -452,12 +452,11 @@ describe('parseStringSync()', () => {
452452
}
453453
];
454454

455-
it('should return expected results.', (done) => {
455+
it('should return expected results.', () => {
456456
const filepath = path.resolve(__dirname, 'fixtures/circle.gcode');
457457
const str = fs.readFileSync(filepath, 'utf8');
458458
const results = parseStringSync(str);
459459
expect(results).toEqual(expectedResults);
460-
done();
461460
});
462461
});
463462

src/index.js

Lines changed: 40 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -66,53 +66,71 @@ const parseLine = (() => {
6666
};
6767
// http://linuxcnc.org/docs/html/gcode/overview.html#gcode:comments
6868
// Comments can be embedded in a line using parentheses () or for the remainder of a lineusing a semi-colon. The semi-colon is not treated as the start of a comment when enclosed in parentheses.
69-
const stripAndExtractComments = (() => {
69+
const stripComments = (() => {
7070
// eslint-disable-next-line no-useless-escape
7171
const re1 = new RegExp(/\(([^\)]*)\)/g); // Match anything inside parentheses
7272
const re2 = new RegExp(/;(.*)$/g); // Match anything after a semi-colon to the end of the line
73-
const re3 = new RegExp(/\s+/g);
7473

7574
return (line) => {
7675
const comments = [];
7776
// Extract comments from parentheses
7877
line = line.replace(re1, (match, p1) => {
79-
const strippedLine = p1.trim();
80-
comments.push(strippedLine); // Add the match to comments
78+
const lineWithoutComments = p1.trim();
79+
comments.push(lineWithoutComments); // Add the match to comments
8180
return '';
8281
});
8382
// Extract comments after a semi-colon
8483
line = line.replace(re2, (match, p1) => {
85-
const strippedLine = p1.trim();
86-
comments.push(strippedLine); // Add the match to comments
84+
const lineWithoutComments = p1.trim();
85+
comments.push(lineWithoutComments); // Add the match to comments
8786
return '';
8887
});
89-
// Remove whitespace characters
90-
line = line.replace(re3, '');
88+
line = line.trim();
9189
return [line, comments];
9290
};
9391
})();
92+
93+
const stripWhitespace = (line) => {
94+
// Remove whitespace characters
95+
const re = new RegExp(/\s+/g);
96+
return line.replace(re, '');
97+
};
98+
9499
// eslint-disable-next-line no-useless-escape
95100
const re = /(%.*)|({.*)|((?:\$\$)|(?:\$[a-zA-Z0-9#]*))|([a-zA-Z][0-9\+\-\.]+)|(\*[0-9]+)/igm;
96101

97-
return (line, options) => {
98-
options = options || {};
99-
options.flatten = !!options.flatten;
100-
options.noParseLine = !!options.noParseLine;
101-
102-
const result = {
103-
line: line
104-
};
102+
return (line, options = {}) => {
103+
options.flatten = !!options?.flatten;
105104

106-
if (options.noParseLine) {
107-
return result;
105+
const validLineModes = [
106+
'original', // Keeps the line unchanged, including comments and whitespace. (Default)
107+
'minimal', // Removes comments, trims leading and trailing whitespace, but preserves inner whitespace.
108+
'compact', // Removes both comments and all whitespace.
109+
];
110+
if (!validLineModes.includes(options?.lineMode)) {
111+
options.lineMode = validLineModes[0];
108112
}
109113

110-
result.words = [];
114+
const result = {
115+
line: '',
116+
words: [],
117+
};
111118

112119
let ln; // Line number
113120
let cs; // Checksum
114-
const [strippedLine, comments] = stripAndExtractComments(line);
115-
const words = strippedLine.match(re) || [];
121+
const originalLine = line;
122+
const [minimalLine, comments] = stripComments(line);
123+
const compactLine = stripWhitespace(minimalLine);
124+
125+
if (options.lineMode === 'compact') {
126+
result.line = compactLine;
127+
} else if (options.lineMode === 'minimal') {
128+
result.line = minimalLine;
129+
} else {
130+
result.line = originalLine;
131+
}
132+
133+
const words = compactLine.match(re) || [];
116134

117135
if (comments.length > 0) {
118136
result.comments = comments;
@@ -243,7 +261,7 @@ const parseString = (str, options, callback = noop) => {
243261
};
244262

245263
const parseStringSync = (str, options) => {
246-
const { flatten = false, noParseLine = false } = { ...options };
264+
const { flatten = false } = { ...options };
247265
const results = [];
248266
const lines = str.split('\n');
249267

@@ -254,7 +272,6 @@ const parseStringSync = (str, options) => {
254272
}
255273
const result = parseLine(line, {
256274
flatten,
257-
noParseLine
258275
});
259276
results.push(result);
260277
}
@@ -272,7 +289,6 @@ class GCodeLineStream extends Transform {
272289

273290
options = {
274291
batchSize: 1000,
275-
noParseLine: false
276292
};
277293

278294
lineBuffer = '';
@@ -282,7 +298,6 @@ class GCodeLineStream extends Transform {
282298
// @param {object} [options] The options object
283299
// @param {number} [options.batchSize] The batch size.
284300
// @param {boolean} [options.flatten] True to flatten the array, false otherwise.
285-
// @param {boolean} [options.noParseLine] True to not parse line, false otherwise.
286301
constructor(options = {}) {
287302
super({ objectMode: true });
288303

@@ -336,7 +351,6 @@ class GCodeLineStream extends Transform {
336351
if (line.length > 0) {
337352
const result = parseLine(line, {
338353
flatten: this.options.flatten,
339-
noParseLine: this.options.noParseLine
340354
});
341355
this.push(result);
342356
}
@@ -349,7 +363,6 @@ class GCodeLineStream extends Transform {
349363
if (line.length > 0) {
350364
const result = parseLine(line, {
351365
flatten: this.options.flatten,
352-
noParseLine: this.options.noParseLine
353366
});
354367
this.push(result);
355368
}

0 commit comments

Comments
 (0)