Bug 11559: Rancor: advanced cataloging interface
[koha.git] / koha-tmpl / intranet-tmpl / lib / koha / cateditor / macros / rancor.js
1 /**
2  * Copyright 2015 ByWater Solutions
3  *
4  * This file is part of Koha.
5  *
6  * Koha is free software; you can redistribute it and/or modify it
7  * under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * Koha is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with Koha; if not, see <http://www.gnu.org/licenses>.
18  */
19
20 define( [ 'marc-editor' ], function( MARCEditor ) {
21     // These are the generators for targets that appear on the left-hand side of an assignment.
22     var _lhsGenerators = [
23         // Field; will replace the entire contents of the tag except for indicators.
24         // Examples:
25         //   * 245 - will return the first 245 tag it finds, or create a new one
26         //   * new 245 - will always create a new 245
27         //   * new 245 grouped - will always create a new 245, and insert it at the end of the 2xx
28         //     block
29         [ /^(new )?(\w{3})( (grouped))?$/, function( forceCreate, tag, position, positionGrouped ) {
30             if ( !forceCreate && positionGrouped ) return null;
31
32             // The extra argument allows the delete command to prevent this from needlessly creating
33             // a tag that it is about to delete.
34             return function( editor, state, extra ) {
35                 extra = extra || {};
36
37                 if ( !forceCreate ) {
38                     var result = editor.getFirstField(tag);
39
40                     if ( result != null || extra.dontCreate ) return result;
41                 }
42
43                 if ( positionGrouped ) {
44                     return editor.createFieldGrouped(tag);
45                 } else {
46                     return editor.createFieldOrdered(tag);
47                 }
48             }
49         } ],
50
51         // This regex is a little complicated, but allows for the following possibilities:
52         //   * 245a - Finds the first 245 field, then tries to find an a subfield within it. If none
53         //            exists, it is created. Will still fail if there is no 245 field.
54         //   * new 245a - always creates a new a subfield.
55         //   * new 245a at end - does the same as the above.
56         //   * $a or new $a - does the same as the above, but for the last-used tag.
57         //   * new 245a after b - creates a new subfield, placing it after the first subfield $b.
58         [ /^(new )?(\w{3}|\$)(\w)( (at end)| after (\w))?$/, function( forceCreate, tag, code, position, positionAtEnd, positionAfterSubfield ) {
59             if ( tag != '$' && tag < '010' ) return null;
60             if ( !forceCreate && position ) return null;
61
62             return function( editor, state, extra ) {
63                 extra = extra || {};
64
65                 var field;
66
67                 if ( tag == '$' ) {
68                     field = state.field;
69                 } else {
70                     field = editor.getFirstField(tag);
71                 }
72                 if ( field == null || field.isControlField ) return null;
73
74                 if ( !forceCreate ) {
75                     var subfield = field.getFirstSubfield(code)
76
77                     if ( subfield != null || extra.dontCreate ) return subfield;
78                 }
79
80                 if ( !position || position == ' at end' ) {
81                     return field.appendSubfield(code);
82                 } else if ( positionAfterSubfield ) {
83                     var afterSubfield = field.getFirstSubfield(positionAfterSubfield);
84
85                     if ( afterSubfield == null ) return null;
86
87                     return field.insertSubfield( code, afterSubfield.index + 1 );
88                 }
89             }
90         } ],
91
92         // Can set indicatators either for a particular field or the last-used tag.
93         [ /^((\w{3}) )?indicators$/, function( undef, tag ) {
94             if ( tag && tag < '010' ) return null;
95
96             return function( editor, state ) {
97                 var field;
98
99                 if ( tag == null ) {
100                     field = state.field;
101                 } else {
102                     field = editor.getFirstField(tag);
103                 }
104                 if ( field == null || field.isControlField ) return null;
105
106                 return {
107                     field: field,
108                     setText: function( text ) {
109                         field.setIndicator1( text.substr( 0, 1 ) );
110                         field.setIndicator2( text.substr( 1, 1 ) );
111                     }
112                 };
113             }
114         } ],
115     ];
116
117     // These patterns, on the other hand, appear inside interpolations on the right hand side.
118     var _rhsGenerators = [
119         [ /^(\w{3})$/, function( tag ) {
120             return function( editor, state, extra ) {
121                 return editor.getFirstField(tag);
122             }
123         } ],
124         [ /^(\w{3})(\w)$/, function( tag, code ) {
125             if ( tag < '010' ) return null;
126
127             return function( editor, state, extra ) {
128                 extra = extra || {};
129
130                 var field = editor.getFirstField(tag);
131                 if ( field == null ) return null;
132
133                 return field.getFirstSubfield(code);
134             }
135         } ],
136     ];
137
138     var _commandGenerators = [
139         [ /^delete (.+)$/, function( target ) {
140             var target_closure = _generate( _lhsGenerators, target );
141             if ( !target_closure ) return null;
142
143             return function( editor, state ) {
144                 var target = target_closure( editor, state, { dontCreate: true } );
145                 if ( target == null ) return;
146                 if ( !target.delete ) return false;
147
148                 state.field = null; // As other fields may have been invalidated
149                 target.delete();
150             }
151         } ],
152         [ /^([^=]+)=([^=]*)$/, function( lhs_desc, rhs_desc ) {
153             var lhs_closure = _generate( _lhsGenerators, lhs_desc );
154             if ( !lhs_closure ) return null;
155
156             var rhs_closure = _generateInterpolation( _rhsGenerators, rhs_desc );
157             if ( !rhs_closure ) return null;
158
159             return function( editor, state ) {
160                 var lhs = lhs_closure( editor, state );
161                 if ( lhs == null ) return;
162
163                 state.field = lhs.field || lhs;
164
165                 try {
166                     return lhs.setText( rhs_closure( editor, state ) );
167                 } catch (e) {
168                     if ( e instanceof MARCEditor.FieldError ) {
169                         return false;
170                     } else {
171                         throw e;
172                     }
173                 }
174             };
175         } ],
176     ];
177
178     function _generate( set, contents ) {
179         var closure;
180
181         if ( contents.match(/^\s*$/) ) return;
182
183         $.each( set, function( undef, gen ) {
184             var match;
185
186             if ( !( match = gen[0].exec( contents ) ) ) return;
187
188             closure = gen[1].apply(null, match.slice(1));
189             return false;
190         } );
191
192         return closure;
193     }
194
195     function _generateInterpolation( set, contents ) {
196         // While this regex will not match at all for an empty string, that just leaves an empty
197         // parts array which yields an empty string (which is what we want.)
198         var matcher = /\{([^}]+)\}|([^{]+)/g;
199         var match;
200
201         var parts = [];
202
203         while ( ( match = matcher.exec(contents) ) ) {
204             var closure;
205             if ( match[1] ) {
206                 // Found an interpolation
207                 var rhs_closure = _generate( set, match[1] );
208                 if ( rhs_closure == null ) return null;
209
210                 closure = ( function(rhs_closure) { return function( editor, state ) {
211                     var rhs = rhs_closure( editor, state );
212
213                     return rhs ? rhs.getText() : '';
214                 } } )( rhs_closure );
215             } else {
216                 // Plain text (needs artificial closure to keep match)
217                 closure = ( function(text) { return function() { return text }; } )( match[2] );
218             }
219
220             parts.push( closure );
221         }
222
223         return function( editor, state ) {
224             var result = '';
225             $.each( parts, function( i, part ) {
226                 result += part( editor, state );
227             } );
228
229             return result;
230         };
231     }
232
233     var RancorMacro = {
234         Compile: function( macro ) {
235             var result = { commands: [], errors: [] };
236
237             $.each( macro.split(/\r\n|\n/), function( line, contents ) {
238                 contents = contents.replace( /#.*$/, '' );
239                 if ( contents.match(/^\s*$/) ) return;
240
241                 var command = _generate( _commandGenerators, contents );
242
243                 if ( !command ) {
244                     result.errors.push( { line: line, error: 'unrecognized' } );
245                 }
246
247                 result.commands.push( { func: command, orig: contents, line: line } );
248             } );
249
250             return result;
251         },
252         Run: function( editor, macro ) {
253             var compiled = RancorMacro.Compile(macro);
254             if ( compiled.errors.length ) return { errors: compiled.errors };
255             var state = {
256                 field: null,
257             };
258
259             var run_result = { errors: [] };
260
261             editor.cm.operation( function() {
262                 $.each( compiled.commands, function( undef, command ) {
263                     var result = command.func( editor, state );
264
265                     if ( result === false ) {
266                         run_result.errors.push( { line: command.line, error: 'failed' } );
267                         return false;
268                     }
269                 } );
270             } );
271
272             return run_result;
273         },
274     };
275
276     return RancorMacro;
277 } );