├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── example ├── example.js ├── output │ ├── example.sgf │ └── simple-example.sgf └── sgf │ ├── example.sgf │ └── simple_example.sgf ├── index.js ├── package-lock.json ├── package.json └── test ├── test-load.js └── test-methods.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "camelcase": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "forin": true, 7 | "immed": true, 8 | "indent": true, 9 | "latedef": true, 10 | "newcap": true, 11 | "noarg": true, 12 | "noempty": true, 13 | "nonew": true, 14 | "plusplus": true, 15 | "quotmark": "single", 16 | "undef": true, 17 | "unused": true, 18 | "strict": true, 19 | "trailing": true, 20 | "maxparams": 4, 21 | "maxdepth": false, 22 | "maxstatements": false, 23 | "maxcomplexity": false, 24 | "maxlen": 120, 25 | "asi": false, 26 | "boss": false, 27 | "debug": false, 28 | "eqnull": true, 29 | "es5": false, 30 | "esnext": false, 31 | "evil": false, 32 | "expr": false, 33 | "funcscope": false, 34 | "globalstrict": false, 35 | "iterator": false, 36 | "lastsemic": false, 37 | "laxbreak": false, 38 | "laxcomma": false, 39 | "loopfunc": false, 40 | "multistr": false, 41 | "proto": false, 42 | "scripturl": false, 43 | "smarttabs": false, 44 | "shadow": false, 45 | "sub": false, 46 | "supernew": false, 47 | "validthis": false, 48 | "nomen": true, 49 | "onevar": false, 50 | "passfail": false, 51 | "white": true, 52 | "browser": false, 53 | "node": true 54 | } 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '0.10' 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Nate Eagle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smartgame [![Build Status](https://api.travis-ci.org/neagle/smartgame.svg?branch=master)](https://travis-ci.org/neagle/smartgame) 2 | 3 | A node library for parsing [SGF format game records](http://www.red-bean.com/sgf/index.html) into JavaScript and back again. Then use [smartgamer](https://www.npmjs.com/package/smartgamer) to navigate and manipulate those game records. 4 | 5 | Installation 6 | ============ 7 | 8 | For most projects, you'll just want to install smartgame locally and add it to your project's dependencies in `package.json`: 9 | 10 | ``` 11 | $ npm install --save smartgame 12 | ``` 13 | 14 | If you want (for whatever reason) to use smartgame anywhere, you can install it globally. 15 | 16 | ``` 17 | $ npm install -g smartgame 18 | ``` 19 | 20 | Usage 21 | ===== 22 | 23 | var sgf = require('smartgame'); 24 | var fs = require('fs'); 25 | 26 | // Grab an SGF file from somewhere 27 | var example = fs.readFileSync('sgf/example.sgf', { encoding: 'utf8' }); 28 | 29 | var collection = sgf.parse(example); 30 | 31 | // ... use the collection object however you want! 32 | 33 | // ... when collection has been modified and you want to save it as an .sgf file 34 | var collectionSGF = sgf.generate(collection); 35 | fs.writeFileSync('new-example.sgf', collectionSGF, { encoding: 'utf8' }); 36 | 37 | Example JS Game Record 38 | ====================== 39 | 40 | Let's take a very simple SGF file as an example: 41 | 42 | (;GM[1]FF[4]CA[UTF-8]SZ[19];B[pd];W[dp];B[pp](;W[dd])(;W[dc];B[ce];W[ed](;B[ch];W[jc])(;B[ci]))) 43 | 44 | The parse function would turn this into a JS Object that looked like this: 45 | 46 | { 47 | gameTrees: [ 48 | { 49 | parent: , 50 | nodes: [ 51 | { GM: '1', FF: '4', CA: 'UTF-8', SZ: '19' }, 52 | { B: 'pd' }, 53 | { W: 'dp' }, 54 | { B: 'pp' } 55 | ], 56 | sequences: [ 57 | { 58 | parent: , 59 | nodes: [ 60 | { W: 'dd' } 61 | ] 62 | }, 63 | { 64 | parent: , 65 | nodes: [ 66 | { W: 'dc' }, 67 | { B: 'ce' }, 68 | { W: 'ed' } 69 | ], 70 | sequences: [ 71 | { 72 | parent: , 73 | nodes: [ 74 | { B: 'ch' }, 75 | { W: 'jc' } 76 | ] 77 | }, 78 | { 79 | parent: , 80 | nodes: [ 81 | { B: 'ci' } 82 | ] 83 | } 84 | ] 85 | } 86 | ] 87 | } 88 | ] 89 | } 90 | 91 | You'll still have to [read up a little bit on the way SGFs work](http://www.red-bean.com/sgf/index.html), but the structure is a simple and straightforward representation of the SGF in JS form. 92 | 93 | Want an easy way to navigate and manipulate that game? Check out [smartgamer](https://www.npmjs.com/package/smartgamer). 94 | 95 | License 96 | ======= 97 | 98 | MIT 99 | 100 | History 101 | ======= 102 | 103 | This parser began life as part of another project, but I thought it was useful enough to become its own module. Breaking it and the other components of that project out into individual modules has helped me improve the separation of concerns and make the project more approachable. It's the first NPM package I've written that's not a plugin for something else (ie, a Grunt plugin or Yeoman generator) and any criticisms or suggestions are welcome. 104 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A script with some example uses of smartgame. 3 | * It may or may not be hijacked on occasion to debug new issues. 4 | * I'll never tell. 5 | */ 6 | 7 | var sgf = require('..'); 8 | var fs = require('fs'); 9 | var util = require('util'); 10 | 11 | // Grab example SGF files 12 | var simpleExample = fs.readFileSync('sgf/simple_example.sgf', { encoding: 'utf8' }); 13 | var example = fs.readFileSync('sgf/example.sgf', { encoding: 'utf8' }); 14 | 15 | // Parse them into JS Game Records 16 | var parsedSimpleExample = sgf.parse(simpleExample); 17 | var parsedExample = sgf.parse(example); 18 | 19 | // Show our JS Game Records 20 | console.log('A simple example:', util.inspect(parsedSimpleExample, false, null)); 21 | console.log('An official example:', util.inspect(parsedExample, false, null)); 22 | 23 | // Turn JS Game Records into SGF files 24 | //var simpleExampleSGF = sgf.generate(parsedSimpleExample); 25 | //fs.writeFileSync('output/simple-example.sgf', simpleExampleSGF, { encoding: 'utf8' }); 26 | 27 | //var exampleSGF = sgf.generate(parsedExample); 28 | //fs.writeFileSync('output/example.sgf', exampleSGF, { encoding: 'utf8' }); 29 | -------------------------------------------------------------------------------- /example/output/example.sgf: -------------------------------------------------------------------------------- 1 | (;FF[4]AP[Primiview:3.1]GM[1]SZ[19]GN[Gametree 1: properties]US[Arno Hollosi](;B[pd]N[Moves, comments, annotations]C[Nodename set to: "Moves, comments, annotations"];W[dp]GW[1]C[Marked as "Good for White"];B[pp]GB[2]C[Marked as "Very good for Black"];W[dc]GW[2]C[Marked as "Very good for White"];B[pj]DM[1]C[Marked as "Even position"];W[ci]UC[1]C[Marked as "Unclear position"];B[jd]TE[1]C[Marked as "Tesuji" or "Good move"];W[jp]BM[2]C[Marked as "Very bad move"];B[gd]DO[]C[Marked as "Doubtful move"];W[de]IT[]C[Marked as "Interesting move"];B[jj];C[White "Pass" move]W[];C[Black "Pass" move]B[tt])(;AB[dd][de][df][dg][do:gq]AW[jd][je][jf][jg][kn:lq][pn:pq]N[Setup]C[Black & white stones at the top are added as single stones. 2 | 3 | Black & white stones at the bottom are added using compressed point lists.];AE[ep][fp][kn][lo][lq][pn:pq]C[AddEmpty 4 | 5 | Black stones & stones of left white group are erased in FF3 way. 6 | 7 | White stones at bottom right were erased using compressed point list.];AB[pd]AW[pp]PL[B]C[Added two stones. 8 | 9 | Node marked with "Black to play".];PL[W]C[Node marked with "White to play"])(;AB[dd][de][df][dg][dh][di][dj][nj][ni][nh][nf][ne][nd][ij][ii][ih][hq][gq][fq][eq][dr][ds][dq][dp][cp][bp][ap][iq][ir][is][bo][bn][an][ms][mr]AW[pd][pe][pf][pg][ph][pi][pj][fd][fe][ff][fh][fi][fj][kh][ki][kj][os][or][oq][op][pp][qp][rp][sp][ro][rn][sn][nq][mq][lq][kq][kr][ks][fs][gs][gr][er]N[Markup]C[Position set up without compressed point lists.];TR[dd][de][df][ed][ee][ef][fd:ff]MA[dh][di][dj][ej][ei][eh][fh:fj]CR[nd][ne][nf][od][oe][of][pd:pf]SQ[nh][ni][nj][oh][oi][oj][ph:pj]SL[ih][ii][ij][jj][ji][jh][kh:kj]TW[pq:ss][so][lr:ns]TB[aq:cs][er:hs][ao]C[Markup at top partially using compressed point lists (for markup on white stones); listed clockwise, starting at upper left: 10 | - TR (triangle) 11 | - CR (circle) 12 | - SQ (square) 13 | - SL (selected points) 14 | - MA ('X') 15 | 16 | Markup at bottom: black & white territory (using compressed point lists)];LB[dc:1][fc:2][nc:3][pc:4][dj:a][fj:b][nj:c][pj:d][gs:ABCDEFGH][gr:ABCDEFG][gq:ABCDEF][gp:ABCDE][go:ABCD][gn:ABC][gm:AB][mm:12][mn:123][mo:1234][mp:12345][mq:123456][mr:1234567][ms:12345678]C[Label (LB property) 17 | 18 | Top: 8 single char labels (1-4, a-d) 19 | 20 | Bottom: Labels up to 8 char length.];DD[kq:os][dq:hs]AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj][gd:md][fh:ij][kj:nh]LN[pj:pd][nf:ff][ih:fj][kh:nj]C[Arrows, lines and dimmed points.])(;B[qd]N[Style & text type]C[There are hard linebreaks & soft linebreaks. 21 | Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ 22 | k<. Hard line breaks are all other linebreaks. 23 | Soft linebreaks are converted to >nothing<, i.e. removed. 24 | 25 | Note that linebreaks are coded differently on different systems. 26 | 27 | Examples (>ok< shouldn't be split): 28 | 29 | linebreak 1 "\\n": >o\ 30 | k< 31 | linebreak 2 "\\n\\r": >o\ 32 | k< 33 | linebreak 3 "\\r\\n": >o\ 34 | k< 35 | linebreak 4 "\\r": >o\ k<](;W[dd]N[W d16]C[Variation C is better.](;B[pp]N[B q4])(;B[dp]N[B d4])(;B[pq]N[B q3])(;B[oq]N[B p3]))(;W[dp]N[W d4])(;W[pp]N[W q4])(;W[cc]N[W c17])(;W[cq]N[W c3])(;W[qq]N[W r3]))(;B[qr]N[Time limits, captures & move numbers]BL[120.0]C[Black time left: 120 sec];W[rr]WL[300]C[White time left: 300 sec];B[rq]BL[105.6]OB[10]C[Black time left: 105.6 sec 36 | Black stones left (in this byo-yomi period): 10];W[qq]WL[200]OW[2]C[White time left: 200 sec 37 | White stones left: 2];B[sr]BL[87.00]OB[9]C[Black time left: 87 sec 38 | Black stones left: 9];W[qs]WL[13.20]OW[1]C[White time left: 13.2 sec 39 | White stones left: 1];B[rs]C[One white stone at s2 captured];W[ps];B[pr];W[or]MN[2]C[Set move number to 2];B[os]C[Two white stones captured 40 | (at q1 & r1)];MN[112]W[pq]C[Set move number to 112];B[sq];W[rp];B[ps];W[ns];B[ss];W[nr];B[rr];W[sp];B[qs]C[Suicide move 41 | (all B stones get captured)]))(;FF[4]AP[Primiview:3.1]GM[1]SZ[19]C[Gametree 2: game-info 42 | 43 | Game-info properties are usually stored in the root node. 44 | If games are merged into a single game-tree, they are stored in the node\ 45 | where the game first becomes distinguishable from all other games in\ 46 | the tree.];B[pd](;PW[W. Hite]WR[6d]RO[2]RE[W+3.5]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[dp]C[Game-info: 47 | Black: B. Lack, 5d 48 | White: W. Hite, 6d 49 | Place: London 50 | Event: Go Congress 51 | Round: 2 52 | Result: White wins by 3.5])(;PW[T. Suji]WR[7d]RO[1]RE[W+Resign]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cp]C[Game-info: 53 | Black: B. Lack, 5d 54 | White: T. Suji, 7d 55 | Place: London 56 | Event: Go Congress 57 | Round: 1 58 | Result: White wins by resignation])(;W[ep];B[pp](;PW[S. Abaki]WR[1d]RO[3]RE[B+63.5]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[ed]C[Game-info: 59 | Black: B. Lack, 5d 60 | White: S. Abaki, 1d 61 | Place: London 62 | Event: Go Congress 63 | Round: 3 64 | Result: Balck wins by 63.5])(;PW[A. Tari]WR[12k]KM[-59.5]RO[4]RE[B+R]PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cd]C[Game-info: 65 | Black: B. Lack, 5d 66 | White: A. Tari, 12k 67 | Place: London 68 | Event: Go Congress 69 | Round: 4 70 | Komi: -59.5 points 71 | Result: Black wins by resignation]))) -------------------------------------------------------------------------------- /example/output/simple-example.sgf: -------------------------------------------------------------------------------- 1 | (;GM[1]FF[4]CA[UTF-8]SZ[19];B[pd];W[dp];B[pp](;W[dd])(;W[dc];B[ce];W[ed](;B[ch];W[jc])(;B[ci]))) -------------------------------------------------------------------------------- /example/sgf/example.sgf: -------------------------------------------------------------------------------- 1 | (;FF[4]AP[Primiview:3.1]GM[1]SZ[19]GN[Gametree 1: properties]US[Arno Hollosi] 2 | 3 | (;B[pd]N[Moves, comments, annotations] 4 | C[Nodename set to: "Moves, comments, annotations"];W[dp]GW[1] 5 | C[Marked as "Good for White"];B[pp]GB[2] 6 | C[Marked as "Very good for Black"];W[dc]GW[2] 7 | C[Marked as "Very good for White"];B[pj]DM[1] 8 | C[Marked as "Even position"];W[ci]UC[1] 9 | C[Marked as "Unclear position"];B[jd]TE[1] 10 | C[Marked as "Tesuji" or "Good move"];W[jp]BM[2] 11 | C[Marked as "Very bad move"];B[gd]DO[] 12 | C[Marked as "Doubtful move"];W[de]IT[] 13 | C[Marked as "Interesting move"];B[jj]; 14 | C[White "Pass" move]W[]; 15 | C[Black "Pass" move]B[tt]) 16 | 17 | (;AB[dd][de][df][dg][do:gq] 18 | AW[jd][je][jf][jg][kn:lq][pn:pq] 19 | N[Setup]C[Black & white stones at the top are added as single stones. 20 | 21 | Black & white stones at the bottom are added using compressed point lists.] 22 | ;AE[ep][fp][kn][lo][lq][pn:pq] 23 | C[AddEmpty 24 | 25 | Black stones & stones of left white group are erased in FF[3\] way. 26 | 27 | White stones at bottom right were erased using compressed point list.] 28 | ;AB[pd]AW[pp]PL[B]C[Added two stones. 29 | 30 | Node marked with "Black to play".];PL[W] 31 | C[Node marked with "White to play"]) 32 | 33 | (;AB[dd][de][df][dg][dh][di][dj][nj][ni][nh][nf][ne][nd][ij][ii][ih][hq] 34 | [gq][fq][eq][dr][ds][dq][dp][cp][bp][ap][iq][ir][is][bo][bn][an][ms][mr] 35 | AW[pd][pe][pf][pg][ph][pi][pj][fd][fe][ff][fh][fi][fj][kh][ki][kj][os][or] 36 | [oq][op][pp][qp][rp][sp][ro][rn][sn][nq][mq][lq][kq][kr][ks][fs][gs][gr] 37 | [er]N[Markup]C[Position set up without compressed point lists.] 38 | 39 | ;TR[dd][de][df][ed][ee][ef][fd:ff] 40 | MA[dh][di][dj][ej][ei][eh][fh:fj] 41 | CR[nd][ne][nf][od][oe][of][pd:pf] 42 | SQ[nh][ni][nj][oh][oi][oj][ph:pj] 43 | SL[ih][ii][ij][jj][ji][jh][kh:kj] 44 | TW[pq:ss][so][lr:ns] 45 | TB[aq:cs][er:hs][ao] 46 | C[Markup at top partially using compressed point lists (for markup on white stones); listed clockwise, starting at upper left: 47 | - TR (triangle) 48 | - CR (circle) 49 | - SQ (square) 50 | - SL (selected points) 51 | - MA ('X') 52 | 53 | Markup at bottom: black & white territory (using compressed point lists)] 54 | ;LB[dc:1][fc:2][nc:3][pc:4][dj:a][fj:b][nj:c] 55 | [pj:d][gs:ABCDEFGH][gr:ABCDEFG][gq:ABCDEF][gp:ABCDE][go:ABCD][gn:ABC][gm:AB] 56 | [mm:12][mn:123][mo:1234][mp:12345][mq:123456][mr:1234567][ms:12345678] 57 | C[Label (LB property) 58 | 59 | Top: 8 single char labels (1-4, a-d) 60 | 61 | Bottom: Labels up to 8 char length.] 62 | 63 | ;DD[kq:os][dq:hs] 64 | AR[aa:sc][sa:ac][aa:sa][aa:ac][cd:cj] 65 | [gd:md][fh:ij][kj:nh] 66 | LN[pj:pd][nf:ff][ih:fj][kh:nj] 67 | C[Arrows, lines and dimmed points.]) 68 | 69 | (;B[qd]N[Style & text type] 70 | C[There are hard linebreaks & soft linebreaks. 71 | Soft linebreaks are linebreaks preceeded by '\\' like this one >o\ 72 | k<. Hard line breaks are all other linebreaks. 73 | Soft linebreaks are converted to >nothing<, i.e. removed. 74 | 75 | Note that linebreaks are coded differently on different systems. 76 | 77 | Examples (>ok< shouldn't be split): 78 | 79 | linebreak 1 "\\n": >o\ 80 | k< 81 | linebreak 2 "\\n\\r": >o\ 82 | k< 83 | linebreak 3 "\\r\\n": >o\ 84 | k< 85 | linebreak 4 "\\r": >o\ k<] 86 | 87 | (;W[dd]N[W d16]C[Variation C is better.](;B[pp]N[B q4]) 88 | (;B[dp]N[B d4]) 89 | (;B[pq]N[B q3]) 90 | (;B[oq]N[B p3]) 91 | ) 92 | (;W[dp]N[W d4]) 93 | (;W[pp]N[W q4]) 94 | (;W[cc]N[W c17]) 95 | (;W[cq]N[W c3]) 96 | (;W[qq]N[W r3]) 97 | ) 98 | 99 | (;B[qr]N[Time limits, captures & move numbers] 100 | BL[120.0]C[Black time left: 120 sec];W[rr] 101 | WL[300]C[White time left: 300 sec];B[rq] 102 | BL[105.6]OB[10]C[Black time left: 105.6 sec 103 | Black stones left (in this byo-yomi period): 10];W[qq] 104 | WL[200]OW[2]C[White time left: 200 sec 105 | White stones left: 2];B[sr] 106 | BL[87.00]OB[9]C[Black time left: 87 sec 107 | Black stones left: 9];W[qs] 108 | WL[13.20]OW[1]C[White time left: 13.2 sec 109 | White stones left: 1];B[rs] 110 | C[One white stone at s2 captured];W[ps];B[pr];W[or] 111 | MN[2]C[Set move number to 2];B[os] 112 | C[Two white stones captured 113 | (at q1 & r1)] 114 | ;MN[112]W[pq]C[Set move number to 112];B[sq];W[rp];B[ps] 115 | ;W[ns];B[ss];W[nr] 116 | ;B[rr];W[sp];B[qs]C[Suicide move 117 | (all B stones get captured)]) 118 | ) 119 | 120 | (;FF[4]AP[Primiview:3.1]GM[1]SZ[19]C[Gametree 2: game-info 121 | 122 | Game-info properties are usually stored in the root node. 123 | If games are merged into a single game-tree, they are stored in the node\ 124 | where the game first becomes distinguishable from all other games in\ 125 | the tree.] 126 | ;B[pd] 127 | (;PW[W. Hite]WR[6d]RO[2]RE[W+3.5] 128 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[dp] 129 | C[Game-info: 130 | Black: B. Lack, 5d 131 | White: W. Hite, 6d 132 | Place: London 133 | Event: Go Congress 134 | Round: 2 135 | Result: White wins by 3.5]) 136 | (;PW[T. Suji]WR[7d]RO[1]RE[W+Resign] 137 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cp] 138 | C[Game-info: 139 | Black: B. Lack, 5d 140 | White: T. Suji, 7d 141 | Place: London 142 | Event: Go Congress 143 | Round: 1 144 | Result: White wins by resignation]) 145 | (;W[ep];B[pp] 146 | (;PW[S. Abaki]WR[1d]RO[3]RE[B+63.5] 147 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[ed] 148 | C[Game-info: 149 | Black: B. Lack, 5d 150 | White: S. Abaki, 1d 151 | Place: London 152 | Event: Go Congress 153 | Round: 3 154 | Result: Balck wins by 63.5]) 155 | (;PW[A. Tari]WR[12k]KM[-59.5]RO[4]RE[B+R] 156 | PB[B. Lack]BR[5d]PC[London]EV[Go Congress]W[cd] 157 | C[Game-info: 158 | Black: B. Lack, 5d 159 | White: A. Tari, 12k 160 | Place: London 161 | Event: Go Congress 162 | Round: 4 163 | Komi: -59.5 points 164 | Result: Black wins by resignation]) 165 | )) 166 | -------------------------------------------------------------------------------- /example/sgf/simple_example.sgf: -------------------------------------------------------------------------------- 1 | ( 2 | ;GM[1]FF[4]CA[UTF-8]SZ[19] 3 | ;B[pd] 4 | ;W[dp] 5 | ;B[pp] 6 | ( 7 | ;W[dd] 8 | ) 9 | ( 10 | ;W[dc] 11 | ;B[ce] 12 | ;W[ed] 13 | ( 14 | ;B[ch] 15 | ;W[jc] 16 | ) 17 | ( 18 | ;B[ci] 19 | ) 20 | ) 21 | ) 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Convert SGF files to a JS object 3 | * @param {string} sgf A valid SGF file. 4 | * @see http://www.red-bean.com/sgf/sgf4.html 5 | * @return {object} The SGF file represented as a JS object 6 | */ 7 | exports.parse = function (sgf) { 8 | 'use strict'; 9 | 10 | var parse; 11 | var parser; 12 | var collection = {}; 13 | 14 | // tracks the current sequence 15 | var sequence; 16 | 17 | // tracks the current node 18 | var node; 19 | 20 | // tracks the last PropIdent 21 | var lastPropIdent; 22 | 23 | // A map of functions to parse the different components of an SGF file 24 | parser = { 25 | 26 | beginSequence: function (sgf) { 27 | var key = 'sequences'; 28 | 29 | // Top-level sequences are gameTrees 30 | if (!sequence) { 31 | sequence = collection; 32 | key = 'gameTrees'; 33 | } 34 | 35 | if (sequence.gameTrees) { 36 | key = 'gameTrees'; 37 | } 38 | 39 | var newSequence = { 40 | parent: sequence 41 | }; 42 | 43 | sequence[key] = sequence[key] || []; 44 | sequence[key].push(newSequence); 45 | sequence = newSequence; 46 | 47 | return sgf.substring(1); 48 | }, 49 | 50 | endSequence: function (sgf) { 51 | if (sequence.parent) { 52 | sequence = sequence.parent; 53 | } else { 54 | sequence = null; 55 | } 56 | return sgf.substring(1); 57 | }, 58 | 59 | node: function (sgf) { 60 | node = {}; 61 | sequence.nodes = sequence.nodes || []; 62 | sequence.nodes.push(node); 63 | return sgf.substring(1); 64 | }, 65 | 66 | property: function (sgf) { 67 | var propValue; 68 | 69 | // Search for the first unescaped ] 70 | var firstPropEnd = sgf.match(/([^\\\]]|\\(.|\n|\r))*\]/); 71 | 72 | if (!firstPropEnd.length) { 73 | throw new Error('malformed sgf'); 74 | } 75 | 76 | firstPropEnd = firstPropEnd[0].length; 77 | 78 | var property = sgf.substring(0, firstPropEnd); 79 | var propValueBegin = property.indexOf('['); 80 | var propIdent = property.substring(0, propValueBegin); 81 | 82 | // Point lists don't declare a PropIdent for each PropValue 83 | // Instead, they should use the last declared property 84 | // See: http://www.red-bean.com/sgf/sgf4.html#move/pos 85 | if (!propIdent) { 86 | propIdent = lastPropIdent; 87 | 88 | // If this is the first property in a list of multiple 89 | // properties, we need to wrap the PropValue in an array 90 | if (!Array.isArray(node[propIdent])) { 91 | node[propIdent] = [node[propIdent]]; 92 | } 93 | } 94 | 95 | lastPropIdent = propIdent; 96 | 97 | propValue = property.substring(propValueBegin + 1, property.length - 1); 98 | 99 | // We have no problem parsing PropIdents of any length, but the spec 100 | // says they should be no longer than two characters. 101 | // 102 | // http://www.red-bean.com/sgf/sgf4.html#2.2 103 | if (propIdent.length > 2) { 104 | // TODO: What's the best way to issue a warning? 105 | console.warn( 106 | 'SGF PropIdents should be no longer than two characters:', propIdent 107 | ); 108 | } 109 | 110 | if (Array.isArray(node[propIdent])) { 111 | node[propIdent].push(propValue); 112 | } else { 113 | node[propIdent] = propValue; 114 | } 115 | 116 | return sgf.substring(firstPropEnd); 117 | }, 118 | 119 | // Whitespace, tabs, or anything else we don't recognize 120 | unrecognized: function (sgf) { 121 | 122 | // March ahead to the next character 123 | return sgf.substring(1); 124 | } 125 | }; 126 | 127 | // Processes an SGF file character by character 128 | parse = function (sgf) { 129 | while (sgf) { 130 | var initial = sgf.substring(0, 1); 131 | var type; 132 | 133 | // Use the initial (the first character in the remaining sgf file) to 134 | // decide which parser function to use 135 | if (initial === '(') { 136 | type = 'beginSequence'; 137 | } else if (initial === ')') { 138 | type = 'endSequence'; 139 | } else if (initial === ';') { 140 | type = 'node'; 141 | } else if (initial.search(/[A-Z\[]/) !== -1) { 142 | type = 'property'; 143 | } else { 144 | type = 'unrecognized'; 145 | } 146 | 147 | sgf = parser[type](sgf); 148 | } 149 | 150 | return collection; 151 | }; 152 | 153 | // Begin parsing the SGF file 154 | return parse(sgf); 155 | }; 156 | 157 | /** 158 | * Generate an SGF file from a SmartGame Record JavaScript Object 159 | * @param {object} record A record object. 160 | * @return {string} The record as a string suitable for saving as an SGF file 161 | */ 162 | exports.generate = function (record) { 163 | 'use strict'; 164 | 165 | function stringifySequences(sequences) { 166 | var contents = ''; 167 | 168 | sequences.forEach(function (sequence) { 169 | contents += '('; 170 | 171 | // Parse all nodes in this sequence 172 | if (sequence.nodes) { 173 | sequence.nodes.forEach(function (node) { 174 | var nodeString = ';'; 175 | for (var property in node) { 176 | if (node.hasOwnProperty(property)) { 177 | var prop = node[property]; 178 | if (Array.isArray(prop)) { 179 | prop = prop.join(']['); 180 | } 181 | nodeString += property + '[' + prop + ']'; 182 | } 183 | } 184 | contents += nodeString; 185 | }); 186 | } 187 | 188 | // Call the function we're in recursively for any child sequences 189 | if (sequence.sequences) { 190 | contents += stringifySequences(sequence.sequences); 191 | } 192 | 193 | contents += ')'; 194 | }); 195 | 196 | return contents; 197 | } 198 | 199 | return stringifySequences(record.gameTrees); 200 | }; 201 | 202 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smartgame", 3 | "version": "0.1.4", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "balanced-match": { 8 | "version": "1.0.0", 9 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 10 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 11 | "dev": true 12 | }, 13 | "brace-expansion": { 14 | "version": "1.1.8", 15 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 16 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 17 | "dev": true, 18 | "requires": { 19 | "balanced-match": "1.0.0", 20 | "concat-map": "0.0.1" 21 | } 22 | }, 23 | "browser-stdout": { 24 | "version": "1.3.0", 25 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", 26 | "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", 27 | "dev": true 28 | }, 29 | "commander": { 30 | "version": "2.9.0", 31 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", 32 | "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", 33 | "dev": true, 34 | "requires": { 35 | "graceful-readlink": "1.0.1" 36 | } 37 | }, 38 | "concat-map": { 39 | "version": "0.0.1", 40 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 41 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 42 | "dev": true 43 | }, 44 | "debug": { 45 | "version": "2.6.8", 46 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", 47 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", 48 | "dev": true, 49 | "requires": { 50 | "ms": "2.0.0" 51 | } 52 | }, 53 | "diff": { 54 | "version": "3.2.0", 55 | "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", 56 | "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", 57 | "dev": true 58 | }, 59 | "escape-string-regexp": { 60 | "version": "1.0.5", 61 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 62 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 63 | "dev": true 64 | }, 65 | "fs.realpath": { 66 | "version": "1.0.0", 67 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 68 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 69 | "dev": true 70 | }, 71 | "glob": { 72 | "version": "7.1.1", 73 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", 74 | "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", 75 | "dev": true, 76 | "requires": { 77 | "fs.realpath": "1.0.0", 78 | "inflight": "1.0.6", 79 | "inherits": "2.0.3", 80 | "minimatch": "3.0.4", 81 | "once": "1.4.0", 82 | "path-is-absolute": "1.0.1" 83 | } 84 | }, 85 | "graceful-readlink": { 86 | "version": "1.0.1", 87 | "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", 88 | "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", 89 | "dev": true 90 | }, 91 | "growl": { 92 | "version": "1.9.2", 93 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 94 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 95 | "dev": true 96 | }, 97 | "has-flag": { 98 | "version": "1.0.0", 99 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", 100 | "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", 101 | "dev": true 102 | }, 103 | "he": { 104 | "version": "1.1.1", 105 | "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", 106 | "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", 107 | "dev": true 108 | }, 109 | "inflight": { 110 | "version": "1.0.6", 111 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 112 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 113 | "dev": true, 114 | "requires": { 115 | "once": "1.4.0", 116 | "wrappy": "1.0.2" 117 | } 118 | }, 119 | "inherits": { 120 | "version": "2.0.3", 121 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 122 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 123 | "dev": true 124 | }, 125 | "json3": { 126 | "version": "3.3.2", 127 | "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", 128 | "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", 129 | "dev": true 130 | }, 131 | "lodash._baseassign": { 132 | "version": "3.2.0", 133 | "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", 134 | "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", 135 | "dev": true, 136 | "requires": { 137 | "lodash._basecopy": "3.0.1", 138 | "lodash.keys": "3.1.2" 139 | } 140 | }, 141 | "lodash._basecopy": { 142 | "version": "3.0.1", 143 | "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", 144 | "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", 145 | "dev": true 146 | }, 147 | "lodash._basecreate": { 148 | "version": "3.0.3", 149 | "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", 150 | "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", 151 | "dev": true 152 | }, 153 | "lodash._getnative": { 154 | "version": "3.9.1", 155 | "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", 156 | "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", 157 | "dev": true 158 | }, 159 | "lodash._isiterateecall": { 160 | "version": "3.0.9", 161 | "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", 162 | "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", 163 | "dev": true 164 | }, 165 | "lodash.create": { 166 | "version": "3.1.1", 167 | "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", 168 | "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", 169 | "dev": true, 170 | "requires": { 171 | "lodash._baseassign": "3.2.0", 172 | "lodash._basecreate": "3.0.3", 173 | "lodash._isiterateecall": "3.0.9" 174 | } 175 | }, 176 | "lodash.isarguments": { 177 | "version": "3.1.0", 178 | "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", 179 | "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", 180 | "dev": true 181 | }, 182 | "lodash.isarray": { 183 | "version": "3.0.4", 184 | "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", 185 | "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", 186 | "dev": true 187 | }, 188 | "lodash.keys": { 189 | "version": "3.1.2", 190 | "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", 191 | "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", 192 | "dev": true, 193 | "requires": { 194 | "lodash._getnative": "3.9.1", 195 | "lodash.isarguments": "3.1.0", 196 | "lodash.isarray": "3.0.4" 197 | } 198 | }, 199 | "minimatch": { 200 | "version": "3.0.4", 201 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 202 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 203 | "dev": true, 204 | "requires": { 205 | "brace-expansion": "1.1.8" 206 | } 207 | }, 208 | "minimist": { 209 | "version": "0.0.8", 210 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 211 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 212 | "dev": true 213 | }, 214 | "mkdirp": { 215 | "version": "0.5.1", 216 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 217 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 218 | "dev": true, 219 | "requires": { 220 | "minimist": "0.0.8" 221 | } 222 | }, 223 | "mocha": { 224 | "version": "3.5.3", 225 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", 226 | "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", 227 | "dev": true, 228 | "requires": { 229 | "browser-stdout": "1.3.0", 230 | "commander": "2.9.0", 231 | "debug": "2.6.8", 232 | "diff": "3.2.0", 233 | "escape-string-regexp": "1.0.5", 234 | "glob": "7.1.1", 235 | "growl": "1.9.2", 236 | "he": "1.1.1", 237 | "json3": "3.3.2", 238 | "lodash.create": "3.1.1", 239 | "mkdirp": "0.5.1", 240 | "supports-color": "3.1.2" 241 | } 242 | }, 243 | "ms": { 244 | "version": "2.0.0", 245 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 246 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 247 | "dev": true 248 | }, 249 | "once": { 250 | "version": "1.4.0", 251 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 252 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 253 | "dev": true, 254 | "requires": { 255 | "wrappy": "1.0.2" 256 | } 257 | }, 258 | "path-is-absolute": { 259 | "version": "1.0.1", 260 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 261 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 262 | "dev": true 263 | }, 264 | "supports-color": { 265 | "version": "3.1.2", 266 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", 267 | "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", 268 | "dev": true, 269 | "requires": { 270 | "has-flag": "1.0.0" 271 | } 272 | }, 273 | "util": { 274 | "version": "0.10.3", 275 | "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", 276 | "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", 277 | "dev": true, 278 | "requires": { 279 | "inherits": "2.0.1" 280 | }, 281 | "dependencies": { 282 | "inherits": { 283 | "version": "2.0.1", 284 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", 285 | "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", 286 | "dev": true 287 | } 288 | } 289 | }, 290 | "wrappy": { 291 | "version": "1.0.2", 292 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 293 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 294 | "dev": true 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smartgame", 3 | "version": "0.1.5", 4 | "description": "Parse SGF (Smart Game Format) files into JavaScript and back again.", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/neagle/smartgame.git" 10 | }, 11 | "engines": { 12 | "node": ">=0.10.0" 13 | }, 14 | "scripts": { 15 | "test": "mocha" 16 | }, 17 | "keywords": [ 18 | "smartgame", 19 | "sgf", 20 | "go", 21 | "weiqi", 22 | "baduk" 23 | ], 24 | "author": { 25 | "name": "neagle", 26 | "email": "nate@nateeagle.com", 27 | "url": "https://github.com/neagle" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/neagle/smartgame/issues" 31 | }, 32 | "homepage": "https://github.com/neagle/smartgame", 33 | "devDependencies": { 34 | "mocha": "*", 35 | "util": "^0.10.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/test-load.js: -------------------------------------------------------------------------------- 1 | /*global describe, beforeEach, it*/ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | 5 | describe('smartgame parser', function () { 6 | it('can be imported without blowing up', function () { 7 | var sgf = require('..'); 8 | assert(sgf !== undefined); 9 | }); 10 | }); 11 | 12 | describe('parse', function () { 13 | it('provides the parse method', function () { 14 | var sgf = require('..'); 15 | assert(typeof sgf.parse === 'function'); 16 | }); 17 | }); 18 | 19 | describe('generate', function () { 20 | it('provides the generate method', function () { 21 | var sgf = require('..'); 22 | assert(typeof sgf.generate === 'function'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /test/test-methods.js: -------------------------------------------------------------------------------- 1 | /*global describe, beforeEach, it*/ 2 | 'use strict'; 3 | var assert = require('assert'); 4 | 5 | // Check if the output of a parsed and generated SGF file matches what went 6 | // into it 7 | describe('parse & generate', function () { 8 | it('generate sgf files after parsing that match the sgf files they started with', function () { 9 | var sgf = require('..'), 10 | fs = require('fs'); 11 | 12 | /** 13 | * Strip whitespace outside of node values in an SGF file 14 | */ 15 | function stripWhitespace(sgf) { 16 | var nodes = []; 17 | var placeholder = '##NODE##'; 18 | 19 | function stripNodes(sgf) { 20 | var node = sgf.match(/\[[^\]]*\]/); 21 | 22 | if (node) { 23 | nodes.push(node[0]); 24 | sgf = sgf.replace(/\[[^\]]*\]/, placeholder); 25 | return stripNodes(sgf); 26 | } else { 27 | return sgf; 28 | } 29 | } 30 | 31 | function putNodesBack(sgf) { 32 | if (sgf.match(placeholder) && nodes.length) { 33 | sgf = sgf.replace(placeholder, nodes.shift()); 34 | return putNodesBack(sgf); 35 | } else { 36 | return sgf; 37 | } 38 | } 39 | 40 | // Strip out escaped square brackets 41 | sgf = sgf.replace(/\\\]/g, '##ESCAPEDBRACKET##'); 42 | 43 | sgf = stripNodes(sgf); 44 | 45 | // Strip all whitespace 46 | sgf = sgf.replace(/[\n\r\s]/g, ''); 47 | 48 | sgf = putNodesBack(sgf); 49 | 50 | // Put escaped square brackets back in 51 | sgf = sgf.replace('##ESCAPEDBRACKET##', '\\]'); 52 | 53 | return sgf; 54 | } 55 | 56 | var files = fs.readdirSync('example/sgf'); 57 | files.forEach(function (file) { 58 | var sgfFile = fs.readFileSync('example/sgf/' + file, { encoding: 'utf-8' }); 59 | assert(stripWhitespace(sgfFile) === sgf.generate(sgf.parse(sgfFile))); 60 | }); 61 | }); 62 | }); 63 | 64 | describe('parse', function () { 65 | it('creates a collection object that has gameTrees', function () { 66 | var sgf = require('..'), 67 | fs = require('fs'); 68 | 69 | 70 | var files = fs.readdirSync('example/sgf'); 71 | files.forEach(function (file) { 72 | var sgfFile = fs.readFileSync('example/sgf/' + file, { encoding: 'utf-8' }); 73 | assert(sgf.parse(sgfFile).gameTrees.length); 74 | }); 75 | }); 76 | }); 77 | --------------------------------------------------------------------------------