From 03e21aa2cf00289e116e4e24ef973540ee87493f Mon Sep 17 00:00:00 2001 From: cheton Date: Tue, 19 Nov 2024 21:23:15 +0800 Subject: [PATCH] feat: add line mode option --- README.md | 25 ++++++++++---- src/__tests__/index.test.js | 63 +++++++++++++++++----------------- src/index.js | 67 ++++++++++++++++++++++--------------- 3 files changed, 89 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index c1f54d4..e2cd9cd 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ parser.parseFile('example.nc', function(err, results) { ### batchSize Type: `Number` -Default: `1000` +Default: 1000 The batch size. @@ -99,16 +99,27 @@ parser.parseLine('G0 X0 Y0', { flatten: true }); // => { line: 'G0 X0 Y0', words: [ 'G0', 'X0', 'Y0' ] } ``` -### noParseLine +### lineMode -Type: `Boolean` -Default: `false` +Type: `String` +Default: `'original'` -True to not parse line, false otherwise. +The `lineMode` option specifies how the parsed line should be formatted. The following values are supported: +- `'original'`: Keeps the line unchanged, including comments and whitespace. (Default) +- `'minimal'`: Removes comments, trims leading and trailing whitespace, but preserves inner whitespace. +- `'compact'`: Removes both comments and all whitespace. + +Example usage: ```js -parser.parseFile('/path/to/file', { noParseLine: true }, function(err, results) { -}); +parser.parseLine('G0 X0 Y0 ; comment', { lineMode: 'original' }); +// => { line: 'G0 X0 Y0 ; comment', words: [ [ 'G', 0 ], [ 'X', 0 ], [ 'Y', 0 ] ] } + +parser.parseLine('G0 X0 Y0 ; comment', { lineMode: 'minimal' }); +// => { line: 'G0 X0 Y0', words: [ [ 'G', 0 ], [ 'X', 0 ], [ 'Y', 0 ] ] } + +parser.parseLine('G0 X0 Y0 ; comment', { lineMode: 'compact' }); +// => { line: 'G0X0Y0', words: [ [ 'G', 0 ], [ 'X', 0 ], [ 'Y', 0 ] ] } ``` ## G-code Interpreter diff --git a/src/__tests__/index.test.js b/src/__tests__/index.test.js index 73630bf..909c615 100644 --- a/src/__tests__/index.test.js +++ b/src/__tests__/index.test.js @@ -43,44 +43,48 @@ describe('Pass an empty text as the first argument', () => { }); }); -describe('Contains only lines', () => { - it('should not parse G-code commands.', (done) => { - const filepath = path.resolve(__dirname, 'fixtures/circle.gcode'); - parseFile(filepath, { noParseLine: true }, (err, results) => { - expect(results.length).toBe(7); - done(); - }) - .on('data', (data) => { - expect(typeof data).toBe('object'); - expect(typeof data.line).toBe('string'); - expect(data.words).toBe(undefined); - }) - .on('end', (results) => { - expect(results.length).toBe(7); - }); - }); -}); - describe('Invalid G-code words', () => { - it('should ignore invalid g-code words', (done) => { + it('should ignore invalid g-code words', () => { const data = parseLine('messed up'); expect(typeof data).toBe('object'); expect(data.line).toBe('messed up'); expect(data.words).toHaveLength(0); - done(); + }); +}); + +describe('Using the `lineMode` option', () => { + it('should return the original line with comments and whitespace in original mode', () => { + const line = 'M6 (tool change;) T1 ; comment'; + const result = parseLine(line, { lineMode: 'original' }); + expect(result.line).toBe('M6 (tool change;) T1 ; comment'); + expect(result.words).toEqual([['M', 6], ['T', 1]]); + }); + + it('should return the line without comments but with whitespace in minimal mode', () => { + const line = 'M6 (tool change;) T1 ; comment'; + const result = parseLine(line, { lineMode: 'minimal' }); + expect(result.line).toBe('M6 T1'); + expect(result.words).toEqual([['M', 6], ['T', 1]]); + }); + + it('should return the line without comments and whitespace in compact mode', () => { + const line = 'M6 (tool change;) T1 ; comment'; + const result = parseLine(line, { lineMode: 'compact' }); + expect(result.line).toBe('M6T1'); + expect(result.words).toEqual([['M', 6], ['T', 1]]); }); }); describe('Commands', () => { - it('should be able to parse $ command (e.g. Grbl).', (done) => { + it('should be able to parse $ command (e.g. Grbl).', () => { const data = parseLine('$H $C'); expect(typeof data).toBe('object'); expect(typeof data.line).toBe('string'); expect(data.words).toHaveLength(0); expect(data.cmds).toEqual(['$H', '$C']); - done(); }); - it('should be able to parse JSON command (e.g. TinyG, g2core).', (done) => { + + it('should be able to parse JSON command (e.g. TinyG, g2core).', () => { { // {sr:{spe:t,spd,sps:t}} const data = parseLine('{sr:{spe:t,spd:t,sps:t}}'); expect(typeof data).toBe('object'); @@ -95,10 +99,9 @@ describe('Commands', () => { expect(data.words).toHaveLength(0); expect(data.cmds).toEqual(['{mt:n}']); } - - done(); }); - it('should be able to parse % command (e.g. bCNC, CNCjs).', (done) => { + + it('should be able to parse % command (e.g. bCNC, CNCjs).', () => { { // %wait const data = parseLine('%wait'); expect(typeof data).toBe('object'); @@ -138,8 +141,6 @@ describe('Commands', () => { expect(data.words).toHaveLength(0); expect(data.cmds).toEqual(['%x0=posx,y0=posy,z0=posz']); } - - done(); }); }); @@ -323,7 +324,7 @@ describe('Event listeners', () => { }); describe('parseLine()', () => { - it('should return expected results.', (done) => { + it('should return expected results.', () => { expect(parseLine('G0 X0 Y0')).toEqual({ line: 'G0 X0 Y0', words: [['G', 0], ['X', 0], ['Y', 0]] @@ -332,7 +333,6 @@ describe('parseLine()', () => { line: 'G0 X0 Y0', words: ['G0', 'X0', 'Y0'] }); - done(); }); }); @@ -452,12 +452,11 @@ describe('parseStringSync()', () => { } ]; - it('should return expected results.', (done) => { + it('should return expected results.', () => { const filepath = path.resolve(__dirname, 'fixtures/circle.gcode'); const str = fs.readFileSync(filepath, 'utf8'); const results = parseStringSync(str); expect(results).toEqual(expectedResults); - done(); }); }); diff --git a/src/index.js b/src/index.js index 2afc098..0100bf9 100644 --- a/src/index.js +++ b/src/index.js @@ -66,53 +66,71 @@ const parseLine = (() => { }; // http://linuxcnc.org/docs/html/gcode/overview.html#gcode:comments // 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. - const stripAndExtractComments = (() => { + const stripComments = (() => { // eslint-disable-next-line no-useless-escape const re1 = new RegExp(/\(([^\)]*)\)/g); // Match anything inside parentheses const re2 = new RegExp(/;(.*)$/g); // Match anything after a semi-colon to the end of the line - const re3 = new RegExp(/\s+/g); return (line) => { const comments = []; // Extract comments from parentheses line = line.replace(re1, (match, p1) => { - const strippedLine = p1.trim(); - comments.push(strippedLine); // Add the match to comments + const lineWithoutComments = p1.trim(); + comments.push(lineWithoutComments); // Add the match to comments return ''; }); // Extract comments after a semi-colon line = line.replace(re2, (match, p1) => { - const strippedLine = p1.trim(); - comments.push(strippedLine); // Add the match to comments + const lineWithoutComments = p1.trim(); + comments.push(lineWithoutComments); // Add the match to comments return ''; }); - // Remove whitespace characters - line = line.replace(re3, ''); + line = line.trim(); return [line, comments]; }; })(); + + const stripWhitespace = (line) => { + // Remove whitespace characters + const re = new RegExp(/\s+/g); + return line.replace(re, ''); + }; + // eslint-disable-next-line no-useless-escape const re = /(%.*)|({.*)|((?:\$\$)|(?:\$[a-zA-Z0-9#]*))|([a-zA-Z][0-9\+\-\.]+)|(\*[0-9]+)/igm; - return (line, options) => { - options = options || {}; - options.flatten = !!options.flatten; - options.noParseLine = !!options.noParseLine; - - const result = { - line: line - }; + return (line, options = {}) => { + options.flatten = !!options?.flatten; - if (options.noParseLine) { - return result; + const validLineModes = [ + 'original', // Keeps the line unchanged, including comments and whitespace. (Default) + 'minimal', // Removes comments, trims leading and trailing whitespace, but preserves inner whitespace. + 'compact', // Removes both comments and all whitespace. + ]; + if (!validLineModes.includes(options?.lineMode)) { + options.lineMode = validLineModes[0]; } - result.words = []; + const result = { + line: '', + words: [], + }; let ln; // Line number let cs; // Checksum - const [strippedLine, comments] = stripAndExtractComments(line); - const words = strippedLine.match(re) || []; + const originalLine = line; + const [minimalLine, comments] = stripComments(line); + const compactLine = stripWhitespace(minimalLine); + + if (options.lineMode === 'compact') { + result.line = compactLine; + } else if (options.lineMode === 'minimal') { + result.line = minimalLine; + } else { + result.line = originalLine; + } + + const words = compactLine.match(re) || []; if (comments.length > 0) { result.comments = comments; @@ -243,7 +261,7 @@ const parseString = (str, options, callback = noop) => { }; const parseStringSync = (str, options) => { - const { flatten = false, noParseLine = false } = { ...options }; + const { flatten = false } = { ...options }; const results = []; const lines = str.split('\n'); @@ -254,7 +272,6 @@ const parseStringSync = (str, options) => { } const result = parseLine(line, { flatten, - noParseLine }); results.push(result); } @@ -272,7 +289,6 @@ class GCodeLineStream extends Transform { options = { batchSize: 1000, - noParseLine: false }; lineBuffer = ''; @@ -282,7 +298,6 @@ class GCodeLineStream extends Transform { // @param {object} [options] The options object // @param {number} [options.batchSize] The batch size. // @param {boolean} [options.flatten] True to flatten the array, false otherwise. - // @param {boolean} [options.noParseLine] True to not parse line, false otherwise. constructor(options = {}) { super({ objectMode: true }); @@ -336,7 +351,6 @@ class GCodeLineStream extends Transform { if (line.length > 0) { const result = parseLine(line, { flatten: this.options.flatten, - noParseLine: this.options.noParseLine }); this.push(result); } @@ -349,7 +363,6 @@ class GCodeLineStream extends Transform { if (line.length > 0) { const result = parseLine(line, { flatten: this.options.flatten, - noParseLine: this.options.noParseLine }); this.push(result); }