@@ -3,13 +3,7 @@ import { motion } from 'framer-motion'
33import { Chess } from 'chess.ts'
44import toast from 'react-hot-toast'
55
6- interface Props {
7- onSubmit : ( type : 'pgn' | 'fen' , data : string , name ?: string ) => void
8- onClose : ( ) => void
9- }
10-
116const PGN_HEADER_LINE_REGEX = / ^ \s * \[ [ ^ \] ] + \] \s * $ /
12- const PGN_RESULT_TOKENS = new Set ( [ '1-0' , '0-1' , '1/2-1/2' , '*' ] )
137
148const ensureBlankLineAfterPgnHeaders = ( pgn : string ) : string => {
159 const normalizedNewlines = pgn . replace ( / \r \n / g, '\n' )
@@ -42,82 +36,12 @@ const ensureBlankLineAfterPgnHeaders = (pgn: string): string => {
4236 lines . splice ( headerEndLine , 0 , '' )
4337 }
4438
45- return lines . join ( '\n' ) . trim ( )
46- }
47-
48- const formatMoveHistoryAsPgn = ( moves : string [ ] ) : string => {
49- const pgnTokens : string [ ] = [ ]
50-
51- for ( let i = 0 ; i < moves . length ; i += 2 ) {
52- pgnTokens . push ( `${ Math . floor ( i / 2 ) + 1 } . ${ moves [ i ] } ` )
53- if ( moves [ i + 1 ] ) {
54- pgnTokens . push ( moves [ i + 1 ] )
55- }
56- }
57-
58- return pgnTokens . join ( ' ' ) . trim ( )
59- }
60-
61- const extractPgnResultToken = ( pgn : string ) : string | undefined => {
62- const headerResultMatch = pgn . match (
63- / \[ \s * R e s u l t \s + " ( 1 - 0 | 0 - 1 | 1 \/ 2 - 1 \/ 2 | \* ) " \s * \] / i,
64- )
65- if ( headerResultMatch ) {
66- return headerResultMatch [ 1 ]
67- }
68-
69- // Strip comments and variations before scanning for a terminal result token.
70- let movetext = pgn . replace ( / \{ [ ^ } ] * \} / g, ' ' ) . replace ( / ; [ ^ \r \n ] * / g, ' ' )
71-
72- const ravRegex = / \( [ ^ ( ) ] * \) / g
73- while ( ravRegex . test ( movetext ) ) {
74- movetext = movetext . replace ( ravRegex , ' ' )
75- }
76-
77- const tailMatch = movetext . match ( / (?: ^ | \s ) ( 1 - 0 | 0 - 1 | 1 \/ 2 - 1 \/ 2 | \* ) (?: \s * ) $ / )
78- return tailMatch ?. [ 1 ]
39+ return lines . join ( '\n' )
7940}
8041
81- const normalizePgnForAnalysis = ( input : string ) : string => {
82- const trimmed = input . trim ( )
83- const candidates = Array . from (
84- new Set ( [ trimmed , ensureBlankLineAfterPgnHeaders ( trimmed ) ] ) ,
85- )
86-
87- for ( const candidate of candidates ) {
88- for ( const sloppy of [ false , true ] ) {
89- const chess = new Chess ( )
90- const loaded = chess . loadPgn ( candidate , { sloppy } )
91-
92- if ( ! loaded ) continue
93-
94- const header = chess . header ( )
95-
96- // Preserve SetUp/FEN PGNs since the initial position cannot be represented
97- // by a move list alone.
98- if ( header . SetUp === '1' && header . FEN ) {
99- return chess . pgn ( )
100- }
101-
102- const moveTextOnly = formatMoveHistoryAsPgn ( chess . history ( ) )
103- if ( ! moveTextOnly ) {
104- throw new Error ( 'PGN must contain at least one move' )
105- }
106-
107- const resultToken =
108- ( header . Result && PGN_RESULT_TOKENS . has ( header . Result )
109- ? header . Result
110- : extractPgnResultToken ( candidate ) ) || undefined
111-
112- return resultToken && resultToken !== '*'
113- ? `${ moveTextOnly } ${ resultToken } `
114- : moveTextOnly
115- }
116- }
117-
118- throw new Error (
119- 'Unable to parse PGN. If using [Tag "..."] headers, include a blank line before the moves.' ,
120- )
42+ interface Props {
43+ onSubmit : ( type : 'pgn' | 'fen' , data : string , name ?: string ) => void
44+ onClose : ( ) => void
12145}
12246
12347export const CustomAnalysisModal : React . FC < Props > = ( { onSubmit, onClose } ) => {
@@ -128,7 +52,7 @@ export const CustomAnalysisModal: React.FC<Props> = ({ onSubmit, onClose }) => {
12852 const validateAndSubmit = ( ) => {
12953 const trimmedInput = input . trim ( )
13054
131- if ( ! input . trim ( ) ) {
55+ if ( ! trimmedInput ) {
13256 toast . error ( 'Please enter some data' )
13357 return
13458 }
@@ -140,17 +64,28 @@ export const CustomAnalysisModal: React.FC<Props> = ({ onSubmit, onClose }) => {
14064 toast . error ( 'Invalid FEN position: ' + validation . error )
14165 return
14266 }
143-
144- onSubmit ( mode , trimmedInput , name . trim ( ) || undefined )
14567 } else {
14668 try {
147- const normalizedPgn = normalizePgnForAnalysis ( trimmedInput )
148- onSubmit ( mode , normalizedPgn , name . trim ( ) || undefined )
69+ const candidates = Array . from (
70+ new Set ( [ trimmedInput , ensureBlankLineAfterPgnHeaders ( trimmedInput ) ] ) ,
71+ )
72+ const isValid = candidates . some ( ( candidate ) => {
73+ const chess = new Chess ( )
74+ return chess . loadPgn ( candidate , { sloppy : true } )
75+ } )
76+
77+ if ( ! isValid ) {
78+ throw new Error (
79+ 'Unable to parse PGN. If using [Tag \"...\"] headers, include a blank line before the moves.' ,
80+ )
81+ }
14982 } catch ( error ) {
15083 toast . error ( 'Invalid PGN format: ' + ( error as Error ) . message )
15184 return
15285 }
15386 }
87+
88+ onSubmit ( mode , trimmedInput , name . trim ( ) || undefined )
15489 }
15590
15691 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