Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 18 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ parser.parseFile('example.nc', function(err, results) {
### batchSize

Type: `Number`
Default: `1000`
Default: 1000

The batch size.

Expand All @@ -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
Expand Down
63 changes: 31 additions & 32 deletions src/__tests__/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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');
Expand Down Expand Up @@ -138,8 +141,6 @@ describe('Commands', () => {
expect(data.words).toHaveLength(0);
expect(data.cmds).toEqual(['%x0=posx,y0=posy,z0=posz']);
}

done();
});
});

Expand Down Expand Up @@ -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]]
Expand All @@ -332,7 +333,6 @@ describe('parseLine()', () => {
line: 'G0 X0 Y0',
words: ['G0', 'X0', 'Y0']
});
done();
});
});

Expand Down Expand Up @@ -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();
});
});

Expand Down
67 changes: 40 additions & 27 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');

Expand All @@ -254,7 +272,6 @@ const parseStringSync = (str, options) => {
}
const result = parseLine(line, {
flatten,
noParseLine
});
results.push(result);
}
Expand All @@ -272,7 +289,6 @@ class GCodeLineStream extends Transform {

options = {
batchSize: 1000,
noParseLine: false
};

lineBuffer = '';
Expand All @@ -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 });

Expand Down Expand Up @@ -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);
}
Expand All @@ -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);
}
Expand Down