UNPKG

@tracespace/parser

Version:
1 lines 93.9 kB
{"version":3,"file":"index.mjs","sources":["../src/constants.ts","../src/lexer/tokens.ts","../src/lexer/rules.ts","../src/lexer/index.ts","../src/tree.ts","../src/syntax/rules.ts","../src/syntax/map-tokens.ts","../src/syntax/drill.ts","../src/syntax/match-syntax.ts","../src/syntax/macro.ts","../src/syntax/gerber.ts","../src/syntax/index.ts","../src/parser.ts"],"sourcesContent":["// common constants\n\n// filetype constants\nexport const GERBER = 'gerber'\nexport const DRILL = 'drill'\n\n// units constants\nexport const MM = 'mm'\nexport const IN = 'in'\n\n// format constants\nexport const LEADING = 'leading'\nexport const TRAILING = 'trailing'\nexport const ABSOLUTE = 'absolute'\nexport const INCREMENTAL = 'incremental'\n\n// tool constants\nexport const CIRCLE = 'circle'\nexport const RECTANGLE = 'rectangle'\nexport const OBROUND = 'obround'\nexport const POLYGON = 'polygon'\nexport const MACRO_SHAPE = 'macroShape'\n\n// macro primitive codes\nexport const MACRO_CIRCLE = '1'\nexport const MACRO_VECTOR_LINE = '20'\nexport const MACRO_CENTER_LINE = '21'\nexport const MACRO_OUTLINE = '4'\nexport const MACRO_POLYGON = '5'\nexport const MACRO_MOIRE = '6'\nexport const MACRO_THERMAL = '7'\n\n// drawing constants\nexport const SHAPE = 'shape'\nexport const MOVE = 'move'\nexport const SEGMENT = 'segment'\nexport const SLOT = 'slot'\n\n// interpolation / routing constants\nexport const LINE = 'line'\nexport const CW_ARC = 'cwArc'\nexport const CCW_ARC = 'ccwArc'\n\n// quadrant mode\nexport const SINGLE = 'single'\nexport const MULTI = 'multi'\n\n// load polarity\nexport const DARK = 'dark'\nexport const CLEAR = 'clear'\n","import {Token as MooToken} from 'moo'\n\n/**\n * T-code token type\n *\n * @category Lexer\n */\nexport const T_CODE = 'T_CODE'\n\n/**\n * G-code token type\n *\n * @category Lexer\n */\nexport const G_CODE = 'G_CODE'\n\n/**\n * M-code token type\n *\n * @category Lexer\n */\nexport const M_CODE = 'M_CODE'\n\n/**\n * D-code token type\n *\n * @category Lexer\n */\nexport const D_CODE = 'D_CODE'\n\n/**\n * Asterisk token type\n *\n * @category Lexer\n */\nexport const ASTERISK = 'ASTERISK'\n\n/**\n * Percent sign token type\n *\n * @category Lexer\n */\nexport const PERCENT = 'PERCENT'\n\n/**\n * Equals sign token type\n *\n * @category Lexer\n */\nexport const EQUALS = 'EQUALS'\n\n/**\n * Comma token type\n *\n * @category Lexer\n */\nexport const COMMA = 'COMMA'\n\n/**\n * Arithmatic operator token type\n *\n * @category Lexer\n */\nexport const OPERATOR = 'OPERATOR'\n\n/**\n * Gerber format specification token type\n *\n * @category Lexer\n */\nexport const GERBER_FORMAT = 'GERBER_FORMAT'\n\n/**\n * Gerber units specification token type\n *\n * @category Lexer\n */\nexport const GERBER_UNITS = 'GERBER_UNITS'\n\n/**\n * Gerber tool macro token type\n *\n * @category Lexer\n */\nexport const GERBER_TOOL_MACRO = 'GERBER_TOOL_MACRO'\n\n/**\n * Gerber tool definition token type\n *\n * @category Lexer\n */\nexport const GERBER_TOOL_DEF = 'GERBER_TOOL_DEF'\n\n/**\n * Gerber load polarity token type\n *\n * @category Lexer\n */\nexport const GERBER_LOAD_POLARITY = 'GERBER_LOAD_POLARITY'\n\n/**\n * Gerber step repear token type\n *\n * @category Lexer\n */\nexport const GERBER_STEP_REPEAT = 'GERBER_STEP_REPEAT'\n\n/**\n * Gerber macro variable token type\n *\n * @category Lexer\n */\nexport const GERBER_MACRO_VARIABLE = 'GERBER_MACRO_VARIABLE'\n\n/**\n * Semicolor token type\n *\n * @category Lexer\n */\nexport const SEMICOLON = 'SEMICOLON'\n\n/**\n * Drill file units token type\n *\n * @category Lexer\n */\nexport const DRILL_UNITS = 'DRILL_UNITS'\n\n/**\n * Drill zero-inclusion token type\n *\n * @category Lexer\n */\nexport const DRILL_ZERO_INCLUSION = 'DRILL_ZERO_INCLUSION'\n\n/**\n * Coordinate axis character token type\n *\n * @category Lexer\n */\nexport const COORD_CHAR = 'COORD_CHAR'\n\n/**\n * Number token type\n *\n * @category Lexer\n */\nexport const NUMBER = 'NUMBER'\n\n/**\n * Word token type\n *\n * @category Lexer\n */\nexport const WORD = 'WORD'\n\n/**\n * Whitespace token type\n *\n * @category Lexer\n */\nexport const WHITESPACE = 'WHITESPACE'\n\n/**\n * Newline token type\n *\n * @category Lexer\n */\nexport const NEWLINE = 'NEWLINE'\n\n/**\n * Catchall token type\n *\n * @category Lexer\n */\nexport const CATCHALL = 'CATCHALL'\n\n/**\n * Error token type\n *\n * @category Lexer\n */\nexport const ERROR = 'ERROR'\n\n/**\n * Union of all available token types\n *\n * @category Lexer\n */\nexport type TokenType =\n | typeof T_CODE\n | typeof G_CODE\n | typeof M_CODE\n | typeof D_CODE\n | typeof ASTERISK\n | typeof PERCENT\n | typeof EQUALS\n | typeof COMMA\n | typeof OPERATOR\n | typeof GERBER_FORMAT\n | typeof GERBER_UNITS\n | typeof GERBER_TOOL_MACRO\n | typeof GERBER_TOOL_DEF\n | typeof GERBER_LOAD_POLARITY\n | typeof GERBER_STEP_REPEAT\n | typeof GERBER_MACRO_VARIABLE\n | typeof SEMICOLON\n | typeof DRILL_UNITS\n | typeof DRILL_ZERO_INCLUSION\n | typeof COORD_CHAR\n | typeof NUMBER\n | typeof WORD\n | typeof WHITESPACE\n | typeof NEWLINE\n | typeof CATCHALL\n | typeof ERROR\n\n/**\n * {@linkcode Lexer} token\n *\n * @category Lexer\n */\nexport interface Token extends MooToken {\n /** Token identifier */\n type: TokenType\n}\n","import Moo from 'moo'\nimport * as Tokens from './tokens'\n\nexport type Rules = {\n [t in Tokens.TokenType]:\n | RegExp\n | string\n | Array<string>\n | Moo.Rule\n | Array<Moo.Rule>\n}\n\nconst RE_STRIP_LEADING_ZEROS = /^0*/\n\nconst stripLeadingZeros = (text: string): string => {\n return text.replace(RE_STRIP_LEADING_ZEROS, '')\n}\n\nconst getCodeValue = (text: string): string => {\n return stripLeadingZeros(text.slice(1)) || '0'\n}\n\nexport const rules: Rules = {\n [Tokens.T_CODE]: {\n match: /T\\d+/,\n value: getCodeValue,\n },\n [Tokens.G_CODE]: {\n match: /G\\d+/,\n value: getCodeValue,\n },\n [Tokens.M_CODE]: {\n match: /M\\d+/,\n value: getCodeValue,\n },\n [Tokens.D_CODE]: {\n match: /D\\d+/,\n value: getCodeValue,\n },\n [Tokens.ASTERISK]: '*',\n [Tokens.PERCENT]: '%',\n [Tokens.EQUALS]: '=',\n [Tokens.GERBER_FORMAT]: {\n match: /FS[LTDAI]+/,\n value: (text: string): string => text.slice(2),\n },\n [Tokens.GERBER_UNITS]: {\n match: /MO(?:IN|MM)/,\n value: (text: string): string => text.slice(2),\n },\n [Tokens.GERBER_TOOL_MACRO]: {\n // \"-\" in a tool name is illegal, but some gerber writers misbehave\n // https://github.com/mcous/gerber-parser/pull/13\n match: /AM[a-zA-Z_.$][\\w.-]*/,\n value: (text: string): string => text.slice(2),\n },\n [Tokens.GERBER_TOOL_DEF]: {\n match: /ADD\\d+[a-zA-Z_.$][\\w.-]*/,\n value: (text: string): string => stripLeadingZeros(text.slice(3)),\n },\n [Tokens.GERBER_LOAD_POLARITY]: {\n match: /LP[DC]/,\n value: (text: string): string => text.slice(2),\n },\n [Tokens.GERBER_STEP_REPEAT]: 'SR',\n [Tokens.GERBER_MACRO_VARIABLE]: /\\$\\d+/,\n [Tokens.SEMICOLON]: ';',\n [Tokens.DRILL_UNITS]: /^(?:METRIC|INCH)/,\n [Tokens.DRILL_ZERO_INCLUSION]: {\n match: /,(?:TZ|LZ)/,\n value: (text: string): string => text.slice(1),\n },\n [Tokens.COORD_CHAR]: /[XYIJACFSBHZN]/,\n [Tokens.NUMBER]: /(?:[+-])?[\\d.]+/,\n [Tokens.OPERATOR]: ['x', '/', '+', '-', '(', ')'],\n [Tokens.COMMA]: ',',\n [Tokens.WORD]: /[a-zA-Z]+/,\n [Tokens.WHITESPACE]: /[ \\t]+/,\n [Tokens.NEWLINE]: {\n match: /\\r?\\n/,\n lineBreaks: true,\n },\n [Tokens.CATCHALL]: /[\\S]/,\n [Tokens.ERROR]: Moo.error,\n}\n","// gerber and drill file lexer + tokenizer\nimport Moo from 'moo'\nimport {Token} from './tokens'\nimport {rules} from './rules'\n\nexport type LexerState = Moo.LexerState\n\nexport * from './tokens'\n\n/**\n * {@linkcode Lexer} factory\n *\n * @example\n * ```ts\n * import {createLexer} from '@tracespace/parser'\n *\n * const lexer = createLexer()\n *\n * lexer.reset('G04 gerber string*\\nM02*\\n')\n *\n * Array.from(lexer).forEach(token => {\n * console.log(`${token.type}: ${token.value}`)\n * })\n * ```\n *\n * @category Lexer\n */\nexport function createLexer(): Lexer {\n return Moo.compile(rules) as Lexer\n}\n\n/**\n * The lexing module of the parser. The Lexer is generated by\n * {@link https://github.com/no-context/moo | Moo}, which determines its API.\n *\n * @category Lexer\n */\nexport interface Lexer extends Moo.Lexer {\n /** Cursor position in the current chunk */\n index?: number\n /** Retrieve the next token from the chunk if available */\n next(): Token | undefined\n /** The Lexer may be treated as an iterator to get tokens */\n [Symbol.iterator](): Iterator<Token>\n}\n","import {Position} from 'unist'\n\nimport * as Types from './types'\n\n/**\n * {@linkcode Root} node type\n *\n * @category Node\n */\nexport const ROOT = 'root'\n\n/**\n * {@linkcode Comment} node type\n *\n * @category Node\n */\nexport const COMMENT = 'comment'\n\n/**\n * {@linkcode Done} node type\n *\n * @category Node\n */\nexport const DONE = 'done'\n\n/**\n * {@linkcode Units} node type\n *\n * @category Node\n */\nexport const UNITS = 'units'\n\n/**\n * {@linkcode CoordinateFormat} node type\n *\n * @category Node\n */\nexport const COORDINATE_FORMAT = 'coordinateFormat'\n\n/**\n * {@linkcode ToolDefinition} node type\n *\n * @category Node\n */\nexport const TOOL_DEFINITION = 'toolDefinition'\n\n/**\n * {@linkcode ToolMacro} node type\n *\n * @category Node\n */\nexport const TOOL_MACRO = 'toolMacro'\n\n/**\n * {@linkcode ToolChange} node type\n *\n * @category Node\n */\nexport const TOOL_CHANGE = 'toolChange'\n\n/**\n * {@linkcode LoadPolarity} node type\n *\n * @category Node\n */\nexport const LOAD_POLARITY = 'loadPolarity'\n\n/**\n * {@linkcode StepRepeat} node type\n *\n * @category Node\n */\nexport const STEP_REPEAT = 'stepRepeat'\n\n/**\n * {@linkcode Graphic} node type\n *\n * @category Node\n */\nexport const GRAPHIC = 'graphic'\n\n/**\n * {@linkcode InterpolateMode} node type\n *\n * @category Node\n */\nexport const INTERPOLATE_MODE = 'interpolateMode'\n\n/**\n * {@linkcode RegionMode} node type\n *\n * @category Node\n */\nexport const REGION_MODE = 'regionMode'\n\n/**\n * {@linkcode QuadrantMode} node type\n *\n * @category Node\n */\nexport const QUADRANT_MODE = 'quadrantMode'\n\n/**\n * {@linkcode Unimplemented} node type\n *\n * @category Node\n */\nexport const UNIMPLEMENTED = 'unimplemented'\n\n/**\n * {@linkcode MacroComment} node type\n *\n * @category Macro\n */\nexport const MACRO_COMMENT = 'macroComment'\n\n/**\n * {@linkcode MacroVariable} node type\n *\n * @category Macro\n */\nexport const MACRO_VARIABLE = 'macroVariable'\n\n/**\n * {@linkcode MacroPrimitive} node type\n *\n * @category Macro\n */\nexport const MACRO_PRIMITIVE = 'macroPrimitive'\n\ninterface BaseNode {\n type: string\n /** Location in the source file the node was parsed from */\n position?: Position\n}\n\ninterface BaseParent extends BaseNode {\n children: Array<BaseNode>\n}\n\n/**\n * Syntax tree node\n *\n * @category Node\n */\nexport type Node = Root | ChildNode\n\n/**\n * Child of the tree's {@linkcode Root} node\n *\n * @category Node\n */\nexport type ChildNode =\n | Comment\n | Done\n | Units\n | CoordinateFormat\n | ToolDefinition\n | ToolMacro\n | ToolChange\n | InterpolateMode\n | RegionMode\n | QuadrantMode\n | LoadPolarity\n | StepRepeat\n | Graphic\n | Unimplemented\n\n/**\n * Child of a {@linkcode ToolMacro} node\n *\n * @category Macro\n */\nexport type MacroBlock = MacroComment | MacroVariable | MacroPrimitive\n\n/**\n * Root node of the syntax tree, identifying the filetype and whether or not\n * the entire file seems to have been read. Filetype, if known, will be\n * either {@linkcode GERBER | gerber} or {@linkcode DRILL | drill}.\n *\n * If `filetype` is `null` or `done` is `false`, this may be a sign that\n *\n * 1. the parser has not finished parsing the file or\n * 2. the file is invalid and/or not a Gerber or drill file.\n *\n * @category Node\n */\nexport interface Root extends BaseParent {\n /** Node type */\n type: typeof ROOT\n /** The parsed file's type (Gerber or NC drill), if known */\n filetype: Types.Filetype | null\n /** Whether the parser has encountered a \"done\" command in the file */\n done: boolean\n /** Parse results */\n children: ChildNode[]\n}\n\n/**\n * Node representing a comment in the file. Usually, comment nodes can be\n * ignored, but in NC drill files, they may contain important format\n * specifications that are not able to be expressed in the file itself.\n *\n * @category Node\n */\nexport interface Comment extends BaseNode {\n /** Node type */\n type: typeof COMMENT\n /** Contents of the comment as a string */\n comment: string\n}\n\n/**\n * Node representing a done command. This represents an `M02` or `M00` command\n * in a Gerber file or an `M00` or `M30` in an NC drill file. Its presence in a\n * tree means the full source file was parsed.\n *\n * @category Node\n */\nexport interface Done extends BaseNode {\n /** Node type */\n type: typeof DONE\n}\n\n/**\n * A `Units` node specifies the units used for the file. Units may be\n * {@linkcode IN | in} or {@linkcode MM | mm}.\n *\n * @category Node\n */\nexport interface Units extends BaseNode {\n /** Node type */\n type: typeof UNITS\n /** Inches or millimeters */\n units: Types.UnitsType\n}\n\n/**\n * A `CoordinateFormat` node specifies the format of coordinate value strings.\n *\n * In Gerber and drill files, coordinates are (almost always) specified as\n * strings of digits without decimal points. `format` is a tuple where the\n * first element is the number of integer places in the string and the second\n * is the number of decimal places. Leading or trailing zeros may also be\n * omitted from the coordinate strings.\n *\n * For example, with `format` set to `[2, 4]`, some (intermediate) coordinate\n * strings could be:\n *\n * - `0.012` > `000120`\n * - `3.45` > `034500`\n * - `67` > `670000`\n *\n * With those same numbers, either {@linkcode LEADING | leading} or\n * {@linkcode TRAILING | trailing} zeros may be omitted depending on the\n * `zeroSuppression` setting:\n *\n * - `0.012` > `000120` > leading omitted: `120`, trailing omitted: `00012`\n * - `3.45` > `024500` > leading omitted: `24500`, trailing omitted: `0245`\n * - `67` > `670000` > leading omitted: `670000`, trailing omitted: `67`\n *\n * Some important things to keep in mind when processing coordinate strings\n * according to the `CoordinateFormat` node:\n *\n * - `format`, `zeroSuppression`, and/or `mode` could be left unspecified\n * - In this case, `format` and `zeroSuppression` should be assumed or\n * inferred, if possible\n * - It's very safe to assume `mode` is always {@linkcode ABSOLUTE | absolute};\n * {@linkcode INCREMENTAL | incremental} mode is deprecated and very rare\n * - Just because `zeroSuppression` is set doesn't mean zeros are dropped\n * - It's common to see Gerber writers keep all zeros to ensure there is no\n * ambiguity, but `zeroSuppression` still needs to be set to _something_\n * - A Gerber or NC drill file may choose to include decimal points! This is\n * not necessarily \"in spec\", but it is unambiguous and easy to parse\n *\n * @category Node\n */\nexport interface CoordinateFormat extends BaseNode {\n /** Node type */\n type: typeof COORDINATE_FORMAT\n /** Integer/decimal format setting, if known */\n format: Types.Format | null\n /** Zero suppression setting, if known */\n zeroSuppression: Types.ZeroSuppression | null\n /** Absolute or incremental coordinate system, if known */\n mode: Types.Mode | null\n}\n\n/**\n * A `ToolDefinition` node defines a \"tool\" that may be used to either create a\n * shape (\"pad\" or \"drill hit\") or a stroke (\"trace\" or \"route\") in a later\n * graphic command.\n *\n * A tool shape may be one of:\n *\n * - {@linkcode Circle} - A circle defined by a diameter\n * - {@linkcode Rectangle} - A rectangle defined by sizes in the x and y axis\n * - {@linkcode Obround} - A \"pill\" rectangle, with a border-radius equal to half of its shorter side\n * - {@linkcode Polygon} - A regular polygon defined by its diameter, number of vertices, and rotation\n * - {@linkcode MacroShape} - A shape defined by a previous {@linkcode ToolMacro}\n *\n * A tool may have a hole in its center; the `hole`, if not `null`, may be a:\n *\n * - {@linkcode Circle}\n * - {@linkcode Rectangle} (deprecated by the Gerber specification)\n *\n * Only `Circle` or `Rectangle` tools without a `hole` may create strokes.\n * `MacroShape` tools may not have a `hole` defined.\n *\n * @category Node\n */\nexport interface ToolDefinition extends BaseNode {\n /** Node type */\n type: typeof TOOL_DEFINITION\n /** Unique tool identifier */\n code: string\n /** Tool shape */\n shape: Types.ToolShape\n /** Hole shape, if applicable */\n hole: Types.HoleShape | null\n}\n\n/**\n * A `ToolMacro` node describes a complex shape in a Gerber file that can use a\n * variety of \"primitives\", simple arithmetic, and differing polarities to lay\n * out an image that will later be repeated.\n *\n * See the {@link https://www.ucamco.com/gerber | Gerber file specification}\n * for an in-depth description of how macros function.\n *\n * @category Node\n */\nexport interface ToolMacro extends BaseNode {\n /** Node type */\n type: typeof TOOL_MACRO\n /** Unique macro identifier */\n name: string\n /** Macro definition blocks */\n children: MacroBlock[]\n}\n\n/**\n * A `MacroComment` represents a comment in a macro and can be safely ignored\n *\n * @category Macro\n */\nexport interface MacroComment extends BaseNode {\n /** Node type */\n type: typeof MACRO_COMMENT\n /** Comment string */\n comment: string\n}\n\n/**\n * A `MacroVariable` node assigns a value to the `name` variable in a macro,\n * where that value may be a number or an arithmetic expression.\n *\n * @category Macro\n */\nexport interface MacroVariable extends BaseNode {\n /** Node type */\n type: typeof MACRO_VARIABLE\n /** Variable name */\n name: string\n /** Concrete value or expression to assign to variable */\n value: Types.MacroValue\n}\n\n/**\n * A `MacroPrimitive` node describes a shape to add to the overall macro shape.\n *\n * @category Macro\n */\nexport interface MacroPrimitive extends BaseNode {\n /** Node type */\n type: typeof MACRO_PRIMITIVE\n /** Primitive shape type */\n code: Types.MacroPrimitiveCode | string\n /** Shape modifier values or expressions */\n modifiers: Types.MacroValue[]\n}\n\n/**\n * A `ToolChange` node sets the current active \"tool\". At a given point in the\n * file, the active tool determines the image that graphical operations produce.\n *\n * @category Node\n */\nexport interface ToolChange extends BaseNode {\n /** Node type */\n type: typeof TOOL_CHANGE\n /** Tool identifier */\n code: string\n}\n\n/**\n * A `LoadPolarity` node sets the current polarity to {@linkcode DARK | dark}\n * or {@linkcode CLEAR | clear}. Subsequent {@linkcode Graphic} operations\n * add to the overall image if the polarity is \"dark\", or remove from the image\n * if the polarity is \"clear\".\n *\n * @category Node\n */\nexport interface LoadPolarity extends BaseNode {\n /** Node type */\n type: typeof LOAD_POLARITY\n /** Polarity */\n polarity: Types.Polarity\n}\n\n/**\n * A `StepRepeat` node starts or ends a step repeat block.\n *\n * See the {@link https://www.ucamco.com/gerber | Gerber file specification}\n * for an in-depth description of step repeat blocks.\n *\n * @category Node\n */\nexport interface StepRepeat extends BaseNode {\n /** Node type */\n type: typeof STEP_REPEAT\n /** Step repeat parameters */\n stepRepeat: Types.StepRepeatParameters\n}\n\n/**\n * A `Graphic` node that represents an image being draw to the active layer.\n * The type of image \"drawn\" is dependent on the value of `graphic`:\n *\n * - {@linkcode SHAPE | shape} - the shape of the current tool is added to the\n * image at `coordinates`\n * - {@linkcode MOVE | move} - the plotter is \"moved\" to `coordinates` **without\n * drawing anything to the image**\n * - {@linkcode SEGMENT | segment} - the tool is \"stroked\" from the plotter's\n * current location to `coordinates`\n * - The path the tool takes is determined by the current {@linkcode IterpolateMode}\n * - The segment may be a standalone path, or it may be a part of a region\n * fill set by a {@linkcode RegionMode}\n * - Only {@linkcode Circle} or {@linkcode Rectangle} tools may create\n * standalone paths\n * - {@linkcode SLOT | slot} - a drill-file-specific graphic that creates a\n * slot from `(coordinates.x1, coordinates.y1)` to `(coordinates.x2, coordinates.y2)`\n * - `null` - The graphic type was not explicitly specified in the source file\n * - This is deprecated syntax in Gerber files, but if present the last\n * used graphic type should be repeated\n * - In a drill file, this means `shape` if in `drill` mode (default),\n * `move` if in `move` mode, or `segment` if in a routing mode\n *\n * @category Node\n */\nexport interface Graphic extends BaseNode {\n /** Node type */\n type: typeof GRAPHIC\n /** Graphical operation */\n graphic: Types.GraphicType\n /** Coordinates where the graphic will be applied */\n coordinates: Types.Coordinates\n}\n\n/**\n * An `InterpolateMode` node is a command to define how subsequent `segment`\n * (or `null`, if you're processing a drill file) graphic nodes are rendered.\n * The `mode` may be one of:\n *\n * - {@linkcode LINE | line} - Draw a straight line segment\n * - {@linkcode CW_ARC | cwArc} - Draw a clockwise arc segment\n * - {@linkcode CCW_ARC | ccwArc} - Draw a counterclockwise arc segment\n * - {@linkcode MOVE | move } - (Drill file only) Move the current coordinate without drawing\n * - {@linkcode DRILL | drill} - (Drill file only) Draw a shape with the current tool\n *\n * @category Node\n */\nexport interface InterpolateMode extends BaseNode {\n type: typeof INTERPOLATE_MODE\n mode: Types.InterpolateModeType\n}\n\n/**\n * A `RegionMode` node is a command to treat subsequent graphics as part of a\n * region definition. Regions are typically used to describe things like\n * copper fills, and only occur in Gerber files. In region mode:\n *\n * - `segment` graphics define the edges of the region\n * - `move` graphics end the current region and start a new one\n * - Other grahpics are disallowed\n *\n * @category Node\n */\nexport interface RegionMode extends BaseNode {\n type: typeof REGION_MODE\n region: boolean\n}\n\n/**\n * A `QuadrantMode` node determines how subsequent arc segments are drawn.\n *\n * See the {@link https://www.ucamco.com/gerber | Gerber file specification}\n * for an in-depth description of arc plotting.\n *\n * @category Node\n */\nexport interface QuadrantMode extends BaseNode {\n type: typeof QUADRANT_MODE\n quadrant: Types.QuadrantModeType\n}\n\n/**\n * An `Unimplemented` node is a chunk that the parser recognizes as part of a\n * Gerber file, but that it doesn't know how to process. These Nodes may be\n * implemented in a future minor release of the parser and should be used with\n * caution.\n *\n * Most unimplemented nodes will either be deprecated commands or valid\n * commands in the {@link https://www.ucamco.com/gerber | Gerber specification}\n * that we don't yet support.\n *\n * @category Node\n */\nexport interface Unimplemented extends BaseNode {\n /** Node type */\n type: typeof UNIMPLEMENTED\n /** String value of chunk */\n value: string\n}\n","import {Token} from '../lexer'\n\nexport const SINGLE_TOKEN = 'TOKEN'\nexport const MIN_TO_MAX = 'MIN_TO_MAX'\n\nexport interface SingleTokenRule {\n rule: typeof SINGLE_TOKEN\n type: Token['type']\n value: Token['value'] | RegExp | null | undefined\n negate?: boolean\n}\n\nexport interface MinToMaxRule {\n rule: typeof MIN_TO_MAX\n min: number\n max: number\n match: Array<SingleTokenRule>\n}\n\nexport type TokenRule = SingleTokenRule | MinToMaxRule\n\nexport function token(\n type: Token['type'],\n value?: Token['value'] | RegExp\n): SingleTokenRule {\n return {rule: SINGLE_TOKEN, type, value}\n}\n\nexport function notToken(\n type: Token['type'],\n value?: Token['value']\n): SingleTokenRule {\n return {rule: SINGLE_TOKEN, type, value, negate: true}\n}\n\nexport function one(match: Array<SingleTokenRule>): MinToMaxRule {\n return {rule: MIN_TO_MAX, min: 1, max: 1, match}\n}\n\nexport function zeroOrOne(match: Array<SingleTokenRule>): MinToMaxRule {\n return {rule: MIN_TO_MAX, min: 0, max: 1, match}\n}\n\nexport function zeroOrMore(match: Array<SingleTokenRule>): MinToMaxRule {\n return {rule: MIN_TO_MAX, min: 0, max: Infinity, match}\n}\n\nexport function oneOrMore(match: Array<SingleTokenRule>): MinToMaxRule {\n return {rule: MIN_TO_MAX, min: 1, max: Infinity, match}\n}\n\nexport function minToMax(\n min: number,\n max: number,\n match: Array<SingleTokenRule>\n): MinToMaxRule {\n return {rule: MIN_TO_MAX, min, max, match}\n}\n","import {Position} from 'unist'\nimport {Token, NUMBER, COORD_CHAR, G_CODE, D_CODE} from '../lexer'\nimport {Coordinates, InterpolateModeType, GraphicType} from '../types'\nimport {SEGMENT, MOVE, SHAPE, LINE, CW_ARC, CCW_ARC, DRILL} from '../constants'\n\nexport function tokensToCoordinates(tokens: Array<Token>): Coordinates {\n return tokens.reduce<Coordinates>((coords, token, i) => {\n const prev = tokens[i - 1]\n\n if (token.type === NUMBER && prev?.type === COORD_CHAR) {\n coords[prev.value.toLowerCase()] = token.value\n }\n\n return coords\n }, {})\n}\n\nexport function tokensToMode(tokens: Token[]): InterpolateModeType {\n return tokens\n .filter(t => t.type === G_CODE)\n .reduce<InterpolateModeType>((m, t) => {\n if (t.value === '0') return MOVE\n if (t.value === '1') return LINE\n if (t.value === '2') return CW_ARC\n if (t.value === '3') return CCW_ARC\n if (t.value === '5') return DRILL\n return m\n }, null)\n}\n\nexport function tokensToGraphic(tokens: Array<Token>): GraphicType {\n return tokens\n .filter(t => t.type === D_CODE)\n .reduce<GraphicType>((g, t) => {\n if (t.value === '1') return SEGMENT\n if (t.value === '2') return MOVE\n if (t.value === '3') return SHAPE\n return g\n }, null)\n}\n\nexport function tokensToString(tokens: Token[]): string {\n return tokens\n .map(t => t.value)\n .join('')\n .trim()\n}\n\nexport function tokensToPosition(\n tokens: Token[],\n options: Partial<{head: Token; length: number}> = {}\n): Position {\n const head = options.head ?? tokens[0]\n const tail = options.length\n ? tokens[tokens.indexOf(head) + options.length - 1]\n : tokens[tokens.length - 1]\n\n return {\n start: {line: head.line, column: head.col, offset: head.offset},\n end: {line: tail.line, column: tail.col, offset: tail.offset},\n }\n}\n","// drill file grammar\nimport * as Lexer from '../lexer'\nimport * as Tree from '../tree'\nimport * as Constants from '../constants'\nimport * as Types from '../types'\nimport {token, notToken, one, zeroOrOne, zeroOrMore, minToMax} from './rules'\nimport {SyntaxRule} from './types'\n\nimport {\n tokensToCoordinates,\n tokensToMode,\n tokensToString,\n tokensToPosition,\n} from './map-tokens'\n\nconst units: SyntaxRule = {\n rules: [\n one([\n token(Lexer.DRILL_UNITS),\n token(Lexer.M_CODE, '71'),\n token(Lexer.M_CODE, '72'),\n ]),\n zeroOrMore([\n token(Lexer.COMMA),\n token(Lexer.DRILL_ZERO_INCLUSION),\n token(Lexer.NUMBER, /^0{1,8}\\.0{1,8}$/),\n ]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => {\n const units =\n tokens[0].value === 'INCH' || tokens[0].value === '72'\n ? Constants.IN\n : Constants.MM\n\n const zeroSuppression = tokens\n .filter(t => t.type === Lexer.DRILL_ZERO_INCLUSION)\n .reduce<Types.ZeroSuppression | null>((z, t) => {\n if (t.value === 'LZ') return Constants.TRAILING\n if (t.value === 'TZ') return Constants.LEADING\n return z\n }, null)\n\n const format = tokens\n .filter(t => t.type === Lexer.NUMBER)\n .reduce<Types.Format | null>((_, t) => {\n const [integer = '', decimal = ''] = t.value.split('.')\n return [integer.length, decimal.length]\n }, null)\n\n const nodes: Tree.ChildNode[] = [\n {type: Tree.UNITS, position: tokensToPosition(tokens.slice(0, 2)), units},\n ]\n\n if (zeroSuppression || format) {\n nodes.push({\n type: Tree.COORDINATE_FORMAT,\n position: tokensToPosition(tokens.slice(1)),\n mode: null,\n format,\n zeroSuppression,\n })\n }\n\n return nodes\n },\n}\n\nconst tool: SyntaxRule = {\n rules: [\n token(Lexer.T_CODE),\n minToMax(0, 12, [\n token(Lexer.COORD_CHAR, 'C'),\n token(Lexer.COORD_CHAR, 'F'),\n token(Lexer.COORD_CHAR, 'S'),\n token(Lexer.COORD_CHAR, 'B'),\n token(Lexer.COORD_CHAR, 'H'),\n token(Lexer.COORD_CHAR, 'Z'),\n token(Lexer.NUMBER),\n ]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => {\n const code = tokens[0].value\n const position = tokensToPosition(tokens)\n const {c = null} = tokensToCoordinates(tokens.slice(1, -1))\n const shape: Types.ToolShape | null =\n c !== null ? {type: Constants.CIRCLE, diameter: Number(c)} : null\n\n return shape\n ? [{type: Tree.TOOL_DEFINITION, hole: null, position, shape, code}]\n : [{type: Tree.TOOL_CHANGE, position, code}]\n },\n}\n\nconst mode: SyntaxRule = {\n rules: [\n one([\n token(Lexer.G_CODE, '0'),\n token(Lexer.G_CODE, '1'),\n token(Lexer.G_CODE, '2'),\n token(Lexer.G_CODE, '3'),\n token(Lexer.G_CODE, '5'),\n ]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => [\n {\n type: Tree.INTERPOLATE_MODE,\n position: tokensToPosition(tokens),\n mode: tokensToMode(tokens),\n },\n ],\n}\n\nconst operation: SyntaxRule = {\n rules: [\n minToMax(0, 2, [\n token(Lexer.T_CODE),\n token(Lexer.G_CODE, '0'),\n token(Lexer.G_CODE, '1'),\n token(Lexer.G_CODE, '2'),\n token(Lexer.G_CODE, '3'),\n token(Lexer.G_CODE, '5'),\n ]),\n minToMax(2, 8, [token(Lexer.COORD_CHAR), token(Lexer.NUMBER)]),\n zeroOrOne([token(Lexer.T_CODE)]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => {\n const graphicTokens = tokens.filter(\n t => t.type === Lexer.COORD_CHAR || t.type === Lexer.NUMBER\n )\n const modeToken = tokens.find(t => t.type === Lexer.G_CODE)\n const toolToken = tokens.find(t => t.type === Lexer.T_CODE)\n const coordinates = tokensToCoordinates(graphicTokens)\n const code = toolToken ? toolToken.value : null\n const mode = tokensToMode(tokens)\n\n const graphicPosition = tokensToPosition(tokens, {\n head: graphicTokens[0],\n length: graphicTokens.length + 1,\n })\n const modePosition = tokensToPosition(tokens, {head: modeToken, length: 2})\n const toolPosition = tokensToPosition(tokens, {head: toolToken, length: 2})\n\n const nodes: Tree.ChildNode[] = [\n {\n type: Tree.GRAPHIC,\n position: graphicPosition,\n graphic: null,\n coordinates,\n },\n ]\n\n if (mode) {\n nodes.unshift({type: Tree.INTERPOLATE_MODE, position: modePosition, mode})\n }\n if (code) {\n nodes.unshift({type: Tree.TOOL_CHANGE, position: toolPosition, code})\n }\n return nodes\n },\n}\n\nconst slot: SyntaxRule = {\n rules: [\n minToMax(2, 4, [token(Lexer.COORD_CHAR), token(Lexer.NUMBER)]),\n token(Lexer.G_CODE, '85'),\n minToMax(2, 4, [token(Lexer.COORD_CHAR), token(Lexer.NUMBER)]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => {\n const gCode = tokens.find(t => t.type === Lexer.G_CODE)\n const splitIdx = gCode ? tokens.indexOf(gCode) : -1\n const start = tokensToCoordinates(tokens.slice(0, splitIdx))\n const end = tokensToCoordinates(tokens.slice(splitIdx))\n const coordinates: Types.Coordinates = {}\n\n Object.keys(start).forEach(k => (coordinates[`${k}1`] = start[k]))\n Object.keys(end).forEach(k => (coordinates[`${k}2`] = end[k]))\n\n return [\n {\n type: Tree.GRAPHIC,\n position: tokensToPosition(tokens),\n graphic: Constants.SLOT,\n coordinates,\n },\n ]\n },\n}\n\nconst done: SyntaxRule = {\n rules: [\n one([token(Lexer.M_CODE, '30'), token(Lexer.M_CODE, '0')]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => [\n {type: Tree.DONE, position: tokensToPosition(tokens)},\n ],\n}\n\nconst comment: SyntaxRule = {\n rules: [\n token(Lexer.SEMICOLON),\n zeroOrMore([notToken(Lexer.NEWLINE)]),\n token(Lexer.NEWLINE),\n ],\n createNodes: tokens => [\n {\n type: Tree.COMMENT,\n comment: tokensToString(tokens.slice(1, -1)),\n position: tokensToPosition(tokens),\n },\n ],\n}\n\nexport const drillSyntax: Array<SyntaxRule> = [\n tool,\n mode,\n operation,\n slot,\n comment,\n units,\n done,\n].map(r => ({...r, filetype: Constants.DRILL}))\n","import {Token} from '../lexer'\nimport {TokenRule, SINGLE_TOKEN, MIN_TO_MAX} from './rules'\nimport {SyntaxRule, MatchState} from './types'\n\nconst FULL_MATCH = 'FULL_MATCH'\nconst PARTIAL_MATCH = 'PARTIAL_MATCH'\nconst NO_MATCH = 'NO_MATCH'\n\ntype ListMatch = typeof FULL_MATCH | typeof PARTIAL_MATCH | typeof NO_MATCH\n\nexport function createMatchSyntax<M>(\n ...grammar: SyntaxRule<M>[]\n): (state: MatchState<M> | null, token: Token) => MatchState<M> {\n return (state, token) => matchSyntax(state, token, grammar)\n}\n\nexport function matchSyntax<M>(\n state: MatchState<M> | null,\n token: Token,\n grammar: SyntaxRule<M>[]\n): MatchState<M> {\n if (state === null) state = {candidates: grammar, tokens: []}\n const {candidates: prevCandidates} = state\n const candidates = []\n const tokens = [...state.tokens, token]\n\n let i\n for (i = 0; i < prevCandidates.length; i++) {\n const rule = prevCandidates[i]\n const result = tokenListMatches(rule.rules, tokens)\n\n if (result === FULL_MATCH) {\n const nodes = rule.createNodes(tokens)\n return {candidates: [], tokens, nodes, filetype: rule.filetype}\n }\n\n if (result === PARTIAL_MATCH) {\n candidates.push(rule)\n }\n }\n\n return {candidates, tokens}\n}\n\nfunction tokenListMatches(\n rules: Array<TokenRule>,\n tokens: Array<Token>\n): ListMatch {\n let i = 0\n let j = 0\n let multiMatchCount = 0\n\n while (i < rules.length && j < tokens.length) {\n const rule = rules[i]\n const token = tokens[j]\n const match = tokenMatches(rule, token)\n\n if (match) {\n if (\n rule.rule === SINGLE_TOKEN ||\n (rule.rule === MIN_TO_MAX && multiMatchCount >= rule.max - 1)\n ) {\n i++\n j++\n multiMatchCount = 0\n } else if (rule.rule === MIN_TO_MAX) {\n j++\n multiMatchCount++\n }\n } else if (rule.rule === MIN_TO_MAX && multiMatchCount >= rule.min) {\n multiMatchCount = 0\n i++\n } else {\n return NO_MATCH\n }\n }\n\n if (i < rules.length) return PARTIAL_MATCH\n return FULL_MATCH\n}\n\nfunction tokenMatches(rule: TokenRule, token: Token): boolean {\n if (rule.rule === SINGLE_TOKEN) {\n const typeResult = rule.type === token.type\n const valueResult =\n rule.value == null ||\n (typeof rule.value === 'string' && rule.value === token.value) ||\n (rule.value instanceof RegExp && rule.value.test(token.value))\n\n const result = typeResult && valueResult\n\n return rule.negate ? !result : result\n }\n\n if (Array.isArray(rule.match)) {\n return rule.match.some(match => tokenMatches(match, token))\n }\n\n return false\n}\n","// gerber aperture macro syntax\nimport * as Lexer from '../lexer'\nimport * as Tree from '../tree'\nimport {MacroValue} from '../types'\nimport {SyntaxRule, MatchState} from './types'\nimport {token, notToken, zeroOrMore, oneOrMore} from './rules'\nimport {tokensToPosition} from './map-tokens'\nimport {matchSyntax} from './match-syntax'\n\nconst macroComment: SyntaxRule<Tree.MacroBlock> = {\n rules: [\n token(Lexer.NUMBER, '0'),\n zeroOrMore([notToken(Lexer.ASTERISK)]),\n token(Lexer.ASTERISK),\n ],\n createNodes: createMacroComment,\n}\n\nconst macroVariable: SyntaxRule<Tree.MacroBlock> = {\n rules: [\n token(Lexer.GERBER_MACRO_VARIABLE),\n token(Lexer.EQUALS),\n oneOrMore([\n token(Lexer.NUMBER),\n token(Lexer.OPERATOR),\n token(Lexer.GERBER_MACRO_VARIABLE),\n token(Lexer.COORD_CHAR, 'X'),\n ]),\n token(Lexer.ASTERISK),\n ],\n createNodes: createMacroVariable,\n}\n\nconst macroPrimitive: SyntaxRule<Tree.MacroBlock> = {\n rules: [\n token(Lexer.NUMBER),\n token(Lexer.COMMA),\n oneOrMore([\n token(Lexer.COMMA),\n token(Lexer.NUMBER),\n token(Lexer.OPERATOR),\n token(Lexer.GERBER_MACRO_VARIABLE),\n token(Lexer.COORD_CHAR, 'X'),\n ]),\n token(Lexer.ASTERISK),\n ],\n createNodes: createMacroPrimitive,\n}\n\nfunction createMacroComment(tokens: Lexer.Token[]): Tree.MacroComment[] {\n const comment = tokens\n .slice(1, -1)\n .map(t => t.text)\n .join('')\n .trim()\n\n return [\n {type: Tree.MACRO_COMMENT, position: tokensToPosition(tokens), comment},\n ]\n}\n\nfunction createMacroPrimitive(tokens: Lexer.Token[]): Tree.MacroPrimitive[] {\n const code = tokens[0].value\n const modifiers = tokens\n .slice(2, -1)\n .reduce<Lexer.Token[][]>(\n (groups, token) => {\n const current = groups[groups.length - 1]\n if (token.type !== Lexer.COMMA) {\n current.push(token)\n } else {\n groups.push([])\n }\n\n return groups\n },\n [[]]\n )\n .map(parseMacroExpression)\n\n return [\n {\n type: Tree.MACRO_PRIMITIVE,\n position: tokensToPosition(tokens),\n code,\n modifiers,\n },\n ]\n}\n\nfunction createMacroVariable(tokens: Lexer.Token[]): Tree.MacroVariable[] {\n const name = tokens[0].value\n const value = parseMacroExpression(tokens.slice(2, -1))\n\n return [\n {\n type: Tree.MACRO_VARIABLE,\n position: tokensToPosition(tokens),\n name,\n value,\n },\n ]\n}\n\nfunction parseMacroExpression(tokens: Lexer.Token[]): MacroValue {\n const toParse = tokens.map<Lexer.Token>(token => {\n return token.type === Lexer.COORD_CHAR\n ? {...token, type: Lexer.OPERATOR, value: 'x'}\n : token\n })\n\n return parseAddition()\n\n function peekNextToken(): Lexer.Token | null {\n return toParse[0] ?? null\n }\n\n // parse numbers, variables, and parenthesis\n function parsePrimary(): MacroValue {\n const token = toParse.shift() as Lexer.Token\n\n if (token.type === Lexer.NUMBER) return Number(token.value)\n if (token.type === Lexer.GERBER_MACRO_VARIABLE) return token.value\n\n // else, we've got a parentheses group, so parse it and consume the \")\"\n const expression = parseAddition()\n toParse.shift()\n return expression\n }\n\n // parse multiplication and division operations\n function parseMultiplication(): MacroValue {\n let expression = parsePrimary()\n let nextToken = peekNextToken()\n\n while (\n nextToken?.type === Lexer.OPERATOR &&\n (nextToken.value === 'x' || nextToken.value === '/')\n ) {\n toParse.shift()\n expression = {\n left: expression,\n right: parsePrimary(),\n operator: nextToken.value,\n }\n nextToken = peekNextToken()\n }\n\n return expression\n }\n\n function parseAddition(): MacroValue {\n let expression = parseMultiplication()\n let nextToken = peekNextToken()\n\n while (\n (nextToken?.type === Lexer.OPERATOR &&\n (nextToken.value === '+' || nextToken.value === '-')) ||\n nextToken?.type === Lexer.NUMBER\n ) {\n let operator: '+' | '-' = '+'\n if (nextToken.type === Lexer.OPERATOR) {\n toParse.shift()\n operator = nextToken.value as '+' | '-'\n }\n const right = parseMultiplication()\n expression = {left: expression, right, operator}\n nextToken = peekNextToken()\n }\n\n return expression\n }\n}\n\nconst MACRO_GRAMMAR = [macroPrimitive, macroVariable, macroComment]\n\nexport function parseMacroBlocks(tokens: Lexer.Token[]): Tree.MacroBlock[] {\n let matchState: MatchState<Tree.MacroBlock> | null = null\n const blocks: Tree.MacroBlock[] = []\n\n tokens.forEach(token => {\n matchState = matchSyntax(matchState, token, MACRO_GRAMMAR)\n if (matchState.nodes) blocks.push(...matchState.nodes)\n if (matchState.candidates.length === 0) matchState = null\n })\n\n return blocks\n}\n","// gerber file syntax\nimport * as Lexer from '../lexer'\nimport * as Constants from '../constants'\nimport * as Types from '../types'\nimport * as Tree from '../tree'\nimport {token, notToken, one, zeroOrMore, zeroOrOne, minToMax} from './rules'\nimport {parseMacroBlocks} from './macro'\nimport {SyntaxRule} from './types'\n\nimport {\n tokensToCoordinates,\n tokensToMode,\n tokensToGraphic,\n tokensToString,\n tokensToPosition,\n} from './map-tokens'\n\nconst holeParamsToShape = (params: number[]): Types.HoleShape | null => {\n if (params.length === 1) {\n const [diameter] = params\n return {type: Constants.CIRCLE, diameter}\n }\n\n if (params.length === 2) {\n const [xSize, ySize] = params\n return {type: Constants.RECTANGLE, xSize, ySize}\n }\n\n return null\n}\n\nconst done: SyntaxRule = {\n rules: [\n one([token(Lexer.M_CODE, '0'), token(Lexer.M_CODE, '2')]),\n token(Lexer.ASTERISK),\n ],\n createNodes: tokens => [\n {type: Tree.DONE, position: tokensToPosition(tokens)},\n ],\n}\n\nconst comment: SyntaxRule = {\n rules: [\n token(Lexer.G_CODE, '4'),\n zeroOrMore([notToken(Lexer.ASTERISK)]),\n token(Lexer.ASTERISK),\n ],\n createNodes: tokens => [\n {\n type: Tree.COMMENT,\n position: tokensToPosition(tokens),\n comment: tokensToString(tokens.slice(1, -1)),\n },\n ],\n}\n\nconst format: SyntaxRule = {\n rules: [\n token(Lexer.PERCENT),\n token(Lexer.GERBER_FORMAT),\n zeroOrMore([notToken(Lexer.COORD_CHAR, 'X')]),\n token(Lexer.COORD_CHAR, 'X'),\n token(Lexer.NUMBER),\n token(Lexer.COORD_CHAR, 'Y'),\n token(Lexer.NUMBER),\n zeroOrMore([notToken(Lexer.ASTERISK)]),\n token(Lexer.ASTERISK),\n // including units here is invalid syntax, but Cadence Allegro does it\n // https://github.com/tracespace/tracespace/issues/234\n minToMax(0, 2, [token(Lexer.GERBER_UNITS), token(Lexer.ASTERISK)]),\n token(Lexer.PERCENT),\n ],\n createNodes: tokens => {\n let format: Types.Format | null = null\n let zeroSuppression = null\n let mode = null\n const coords = tokensToCoordinates(tokens)\n const formatEndIdx = tokens.findIndex(t => t.type === Lexer.ASTERISK)\n const unitsToken = tokens.find(t => t.type === Lexer.GERBER_UNITS)\n\n tokens\n .filter(t => t.type === Lexer.GERBER_FORMAT)\n .forEach(t => {\n if (t.value.indexOf('T') >= 0) zeroSuppression = Constants.TRAILING\n if (t.value.indexOf('L') >= 0) zeroSuppression = Constants.LEADING\n if (t.value.indexOf('I') >= 0) mode = Constants.INCREMENTAL\n if (t.value.indexOf('A') >= 0) mode = Constants.ABSOLUTE\n })\n\n if (coords.x === coords.y && coords.x?.length === 2) {\n const integers = Number(coords.x[0])\n const decimals = Number(coords.x[1])\n if (integers && decimals) format = [integers, decimals]\n }\n\n const nodes: Array<Tree.ChildNode> = [\n {\n type: Tree.COORDINATE_FORMAT,\n position: tokensToPosition(tokens.slice(1, formatEndIdx + 1)),\n zeroSuppression,\n format,\n mode,\n },\n ]\n\n if (unitsToken) {\n nodes.push({\n type: Tree.UNITS,\n position: tokensToPosition(tokens.slice(1, -1), {head: unitsToken}),\n units: unitsToken.value === 'MM' ? Constants.MM : Constants.IN,\n })\n }\n\n return nodes\n },\n}\n\nconst units: SyntaxRule = {\n rules: [\n token(Lexer.PERCENT),\n token(Lexer.GERBER_UNITS),\n token(Lexer.ASTERISK),\n token(Lexer.PERCENT),\n ],\n createNodes: tokens => [\n {\n type: Tree.UNITS,\n position: tokensToPosition(tokens.slice(1, -1)),\n units: tokens[1].value === 'MM' ? Constants.MM : Constants.IN,\n },\n ],\n}\n\nconst toolMacro: SyntaxRule = {\n rules: [\n token(Lexer.PERCENT),\n token(Lexer.GERBER_TOOL_MACRO),\n token(Lexer.ASTERISK),\n zeroOrMore([notToken(Lexer.PERCENT)]),\n token(Lexer.PERCENT),\n ],\n createNodes: tokens => {\n const name = tokens[1].value\n const position = tokensToPosition(tokens.slice(1, -1))\n const blockTokens = tokens.slice(3, -1)\n\n return [\n {\n type: Tree.TOOL_MACRO,\n position,\n children: parseMacroBlocks(blockTokens),\n name,\n },\n ]\n },\n}\n\nconst toolDefinition: SyntaxRule = {\n rules: [\n token(Lexer.PERCENT),\n token(Lexer.GERBER_TOOL_DEF),\n zeroOrMore([\n token(Lexer.COMMA),\n token(Lexer.NUMBER),\n token(Lexer.COORD_CHAR, 'X'),\n ]),\n token(Lexer.ASTERISK),\n token(Lexer.PERCENT),\n ],\n createNodes: tokens => {\n let shape: Types.ToolShape\n let hole: Types.HoleShape | null = null\n\n const toolProps = tokens[1].value.match(/(\\d+)(.+)/)\n const [, code = '', name = ''] = toolProps ?? []\n const params: Array<number> = tokens\n .slice(3, -2)\n .filter(t => t.type === Lexer.NUMBER)\n .map(t => Number(t.value))\n\n if (name === 'C') {\n const [diameter, ...holeParams] = params\n shape = {type: Constants.CIRCLE, diameter}\n hole = holeParamsToShape(holeParams)\n } else if (name === 'R' || name === 'O') {\n const [xSize, ySize, ...holeParams] = params\n const type = name === 'R' ? Constants.RECTANGLE : Constants.OBROUND\n shape = {type, xSize, ySize}\n hole = holeParamsToShape(holeParams)\n } else if (name === 'P') {\n const [diameter, vertices, rotation = null, ...holeParams] = params\n shape = {type: Constants.POLYGON, diameter, vertices, rotation}\n hole = holeParamsToShape(holeParams)\n } else {\n shape = {type: Constants.MACRO_SHAPE, name, params}\n }\n\n return [\n {\n type: Tree.TOOL_DEFINITION,\n position: tokensToPosition(tokens.slice(1, -1)),\n code,\n shape,\n hole,\n },\n ]\n },\n}\n\nconst toolChange: SyntaxRule = {\n rules: [\n zeroOrOne([token(Lexer.G_CODE, '54')]),\n token(Lexer.D_CODE),\n token(Lexer.ASTERISK),\n ],\n createNodes: tokens => [\n {\n type: Tree.TOOL_CHANGE,\n position: tokensToPosition(tokens),\n code: tokens.find(t => t.type === Lexer.D_CODE)?.value as string,\n },\n ],\n}\n\nconst createOperationNodes = (tokens: Lexer.Token[]): Tree.ChildNode[] => {\n const graphic = tokensToGraphic(tokens)\n const coordinates = tokensToCoordinates(tokens)\n const mode = tokensToMode(tokens)\n const position = tokensToPosition(tokens, {\n head: mode ? tokens[1] : tokens[0],\n })\n const nodes: Tree.ChildNode[] = [\n {type: Tree.GRAPHIC, position, graphic, coordinates},\n ]\n if (mode) {\n const modePosition = tokensToPosition(tokens, {head: tokens[0], length: 2})\n nodes.unshift({type: Tree.INTERPOLATE_MODE, position: modePosition, mode})\n }\n return nodes\n}\n\nconst operation: SyntaxRule = {\n rules: [\n zeroOrOne([\n token(Lexer.G_CODE, '1'),\n token(Lexer.G_CODE, '2'),\n token(Lexer.G_CODE, '3'),\n ]),\n minToMax(2, 8, [token(Lexer.COORD_CHAR), token(Lexer.NUMBER)]),\n zeroOrOne([\n token(Lexer.D_CODE, '1'),\n token(Lexer.D_CODE, '2'),\n token(Lexer.D_CODE, '3'),\n ]),\n token(Lexer.ASTERISK),\n ],\n createNodes: createOperationNodes,\n}\n\nconst operationWithoutCoords: SyntaxRule = {\n rules: [\n zeroOrOne([\n token(Lexer.G_CODE, '1'),\n token(Lexer.G_CODE, '2'),\n token(Lexer.G_CODE, '3'),\n ]),\n one([\n token(Lexer.D_CODE, '1'),\n token(Lexer.D_CODE, '2'),\n token(Lexer.D_CODE, '3'),\n ]),\n token(Lexer.ASTERISK),\n ],\n createNodes: createOperationNodes,\n}\n\nconst interpolationMode: SyntaxRule = {\n rules: [\n one([\n token(Lexer.G_CODE, '1'),\n token(Lexer.G_CODE, '2'),\n token(Lexer.G_CODE, '3'),\n ]),\n token(Lexer.ASTERISK),\n ],\n createNodes: tokens => [\n {\n type: Tree.INTERPOLATE_MODE,\n position: tokensToPosition(tokens),\n mode: tokensToMode(tokens),\n },\n ],\n}\n\nconst regionMode: SyntaxRule = {\n rules: [\n one([token(Lexer.G_CODE, '36'), token(Lexer.G_CODE, '37')]),\n token(Lexer.ASTERISK),\n ],\n createNodes: tokens => [\n {\n type: Tree.REGION_MODE,\n position: tokensToPosition(tokens),\n region: tokens[0].value === '36',\n },\n ],\n}\n\nconst quadrantMode: SyntaxRule = {\n rules: [\n one([token(Lexer.G_CODE, '74'), token(Lexer.G_CODE, '75')]),\n token(Lexer.ASTERISK),\n ],\n createNodes: tokens => [\n {\n type: Tree.QUADRANT_MODE,\n position: tokensToPosition(tokens),\n quadrant: tokens[0].value === '74' ? Constants.SINGLE : Constants.MULTI,\n },\n ],\n}\n\nconst loadPolarity: SyntaxRule = {\n rules: [\n token(Lexer.PERCENT),\n token(Lexer.GERBER_LOAD_POLARITY),\n token(Lexer.ASTERISK),\n token(Lexer.PERCENT),\n ],\n createNodes: tokens => [\n {\n type: Tree.LOAD_POLARITY,\n position: toke