@@ -8,35 +8,121 @@ interface Props {
88 onClose : ( ) => void
99}
1010
11+ const PGN_HEADER_LINE_REGEX = / ^ \s * \[ [ ^ \] ] + \] \s * $ /
12+
13+ const ensureBlankLineAfterPgnHeaders = ( pgn : string ) : string => {
14+ const normalizedNewlines = pgn . replace ( / \r \n / g, '\n' )
15+ const lines = normalizedNewlines . split ( '\n' )
16+
17+ let firstContentLine = 0
18+ while (
19+ firstContentLine < lines . length &&
20+ lines [ firstContentLine ] . trim ( ) . length === 0
21+ ) {
22+ firstContentLine ++
23+ }
24+
25+ let headerEndLine = firstContentLine
26+ while (
27+ headerEndLine < lines . length &&
28+ PGN_HEADER_LINE_REGEX . test ( lines [ headerEndLine ] )
29+ ) {
30+ headerEndLine ++
31+ }
32+
33+ const hasHeaderBlock = headerEndLine > firstContentLine
34+ const hasMovetextAfterHeaders = headerEndLine < lines . length
35+ const needsSeparator =
36+ hasHeaderBlock &&
37+ hasMovetextAfterHeaders &&
38+ lines [ headerEndLine ] . trim ( ) . length > 0
39+
40+ if ( needsSeparator ) {
41+ lines . splice ( headerEndLine , 0 , '' )
42+ }
43+
44+ return lines . join ( '\n' ) . trim ( )
45+ }
46+
47+ const formatMoveHistoryAsPgn = ( moves : string [ ] ) : string => {
48+ const pgnTokens : string [ ] = [ ]
49+
50+ for ( let i = 0 ; i < moves . length ; i += 2 ) {
51+ pgnTokens . push ( `${ Math . floor ( i / 2 ) + 1 } . ${ moves [ i ] } ` )
52+ if ( moves [ i + 1 ] ) {
53+ pgnTokens . push ( moves [ i + 1 ] )
54+ }
55+ }
56+
57+ return pgnTokens . join ( ' ' ) . trim ( )
58+ }
59+
60+ const normalizePgnForAnalysis = ( input : string ) : string => {
61+ const trimmed = input . trim ( )
62+ const candidates = Array . from (
63+ new Set ( [ trimmed , ensureBlankLineAfterPgnHeaders ( trimmed ) ] ) ,
64+ )
65+
66+ for ( const candidate of candidates ) {
67+ for ( const sloppy of [ false , true ] ) {
68+ const chess = new Chess ( )
69+ const loaded = chess . loadPgn ( candidate , { sloppy } )
70+
71+ if ( ! loaded ) continue
72+
73+ const header = chess . header ( )
74+
75+ // Preserve SetUp/FEN PGNs since the initial position cannot be represented
76+ // by a move list alone.
77+ if ( header . SetUp === '1' && header . FEN ) {
78+ return chess . pgn ( )
79+ }
80+
81+ const moveTextOnly = formatMoveHistoryAsPgn ( chess . history ( ) )
82+ if ( ! moveTextOnly ) {
83+ throw new Error ( 'PGN must contain at least one move' )
84+ }
85+
86+ return moveTextOnly
87+ }
88+ }
89+
90+ throw new Error (
91+ 'Unable to parse PGN. If using [Tag "..."] headers, include a blank line before the moves.' ,
92+ )
93+ }
94+
1195export const CustomAnalysisModal : React . FC < Props > = ( { onSubmit, onClose } ) => {
1296 const [ mode , setMode ] = useState < 'pgn' | 'fen' > ( 'pgn' )
1397 const [ input , setInput ] = useState ( '' )
1498 const [ name , setName ] = useState ( '' )
1599
16100 const validateAndSubmit = ( ) => {
101+ const trimmedInput = input . trim ( )
102+
17103 if ( ! input . trim ( ) ) {
18104 toast . error ( 'Please enter some data' )
19105 return
20106 }
21107
22108 if ( mode === 'fen' ) {
23109 const chess = new Chess ( )
24- const validation = chess . validateFen ( input . trim ( ) )
110+ const validation = chess . validateFen ( trimmedInput )
25111 if ( ! validation . valid ) {
26112 toast . error ( 'Invalid FEN position: ' + validation . error )
27113 return
28114 }
115+
116+ onSubmit ( mode , trimmedInput , name . trim ( ) || undefined )
29117 } else {
30118 try {
31- const chess = new Chess ( )
32- chess . loadPgn ( input . trim ( ) )
119+ const normalizedPgn = normalizePgnForAnalysis ( trimmedInput )
120+ onSubmit ( mode , normalizedPgn , name . trim ( ) || undefined )
33121 } catch ( error ) {
34122 toast . error ( 'Invalid PGN format: ' + ( error as Error ) . message )
35123 return
36124 }
37125 }
38-
39- onSubmit ( mode , input . trim ( ) , name . trim ( ) || undefined )
40126 }
41127
42128 const examplePGN = `1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Bb7 10. d4 Re8`
0 commit comments