[Koha-patches] [PATCH] Bug 6707 - Add textual MARC editor

Jesse Weaver pianohacker at gmail.com
Thu Aug 11 08:53:30 CEST 2011


Add an alternate MARC editor, which allows entering biblios in a textual
format. This allows a faster workflow for experienced catalogers.

Possible improvements:
  * Remove untranslateable strings from code
  * Replace parser with simpler version, if possible
  * Remove ugly redirect-hack

Known problems:
  * Does not support authorities. Adding this would be quite complicated.
  * Also not authorized values. Would be tough, but doable.
---
 C4/Biblio.pm                                       |  253 +++++
 C4/Koha.pm                                         |   18 +-
 cataloguing/addbiblio-text.pl                      |  635 +++++++++++
 cataloguing/addbiblio.pl                           |    6 +-
 cataloguing/framework-jsonp.pl                     |   60 +
 installer/data/mysql/en/mandatory/sysprefs.sql     |    1 +
 installer/data/mysql/updatedatabase.pl             |    7 +
 .../intranet-tmpl/prog/en/css/staff-global.css     |    5 +
 .../prog/en/includes/doc-head-close.inc            |    3 +
 koha-tmpl/intranet-tmpl/prog/en/js/marc.js         |  194 ++++
 .../prog/en/js/pages/addbiblio-text.js             |   82 ++
 .../intranet-tmpl/prog/en/lib/codemirror/LICENSE   |   23 +
 .../prog/en/lib/codemirror/css/csscolors.css       |   47 +
 .../prog/en/lib/codemirror/css/docs.css            |   42 +
 .../prog/en/lib/codemirror/css/jscolors.css        |   55 +
 .../prog/en/lib/codemirror/css/marccolors.css      |   24 +
 .../prog/en/lib/codemirror/css/people.jpg          |  Bin 0 -> 14122 bytes
 .../prog/en/lib/codemirror/css/sparqlcolors.css    |   39 +
 .../prog/en/lib/codemirror/css/xmlcolors.css       |   51 +
 .../prog/en/lib/codemirror/js/codemirror.js        |  219 ++++
 .../prog/en/lib/codemirror/js/editor.js            | 1176 ++++++++++++++++++++
 .../prog/en/lib/codemirror/js/mirrorframe.js       |   81 ++
 .../prog/en/lib/codemirror/js/parsecss.js          |  155 +++
 .../prog/en/lib/codemirror/js/parsehtmlmixed.js    |   73 ++
 .../prog/en/lib/codemirror/js/parsejavascript.js   |  322 ++++++
 .../prog/en/lib/codemirror/js/parsemarc.js         |  102 ++
 .../prog/en/lib/codemirror/js/parsesparql.js       |  162 +++
 .../prog/en/lib/codemirror/js/parsexml.js          |  286 +++++
 .../prog/en/lib/codemirror/js/select.js            |  584 ++++++++++
 .../prog/en/lib/codemirror/js/stringstream.js      |  131 +++
 .../prog/en/lib/codemirror/js/tokenize.js          |   57 +
 .../en/lib/codemirror/js/tokenizejavascript.js     |  176 +++
 .../prog/en/lib/codemirror/js/undo.js              |  388 +++++++
 .../prog/en/lib/codemirror/js/util.js              |  123 ++
 .../en/modules/admin/preferences/cataloguing.pref  |    7 +
 .../prog/en/modules/cataloguing/addbiblio-text.tt  |  155 +++
 36 files changed, 5740 insertions(+), 2 deletions(-)
 create mode 100755 cataloguing/addbiblio-text.pl
 create mode 100755 cataloguing/framework-jsonp.pl
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/js/marc.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/js/pages/addbiblio-text.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/LICENSE
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/csscolors.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/docs.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/jscolors.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/marccolors.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/people.jpg
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/sparqlcolors.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/xmlcolors.css
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/codemirror.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/editor.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/mirrorframe.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsecss.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsehtmlmixed.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsejavascript.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsemarc.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsesparql.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsexml.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/select.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/stringstream.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenize.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenizejavascript.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/undo.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/util.js
 create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio-text.tt

diff --git a/C4/Biblio.pm b/C4/Biblio.pm
index 96baaef..1734c43 100644
--- a/C4/Biblio.pm
+++ b/C4/Biblio.pm
@@ -126,6 +126,7 @@ BEGIN {
       &TransformHtmlToMarc2
       &TransformHtmlToMarc
       &TransformHtmlToXml
+      &TransformTextToMarc
       &PrepareItemrecordDisplay
       &GetNoZebraIndexes
     );
@@ -2097,6 +2098,258 @@ sub TransformHtmlToMarc {
     return $record;
 }
 
+=item TransformTextToMarc
+
+	$record = TransformTextToMarc($text[, existing_record => $existing_record, debug => $debug]);
+
+Parses a textual representation of MARC data into a MARC::Record. If an error
+occurs, will die(); this can be caught with eval { ... } if ($@) { ... }
+
+$text should be a series of lines with the following format:
+
+Control fields: 005 20080303040352.1
+Data fields: 245 10 $a The $1,000,000 problem / $c Robert Biggs.
+
+Indicators are optional. Subfields are delimited by | or $, and both of these
+characters are allowed in subfield contents as long as they are not followed by
+a number/digit and a space.
+
+If $existing_record is defined as a MARC::Record, TransformTextToMarc will place
+parsed fields into it and return it, rather than creating a new MARC::Record.
+
+If $debug is true, then the parser will output very verbose debugging
+information to stdout.
+
+=cut
+
+sub TransformTextToMarc {
+	# A non-deterministic-finite-state-machine based parser for a textual MARC
+	# format.
+	#
+	# Allowable contents of tag numbers, indicators and subfield codes are
+	# based on the MARCXML standard.
+	#
+	# While this is a mostly conventional FSM, it has two major peculiarities:
+	# * A buffer, separate from the current character, that is manually added
+	#   to by each state.
+	# * Two methods of transitioning between states; jumping, which preserves
+	#   the buffer, and switching, which does not.
+
+	our ($text, %options) = @_;
+
+	%options = ((
+		existing_record => MARC::Record->new(),
+		debug => 0,
+		strip_whitespace => 1,
+	), %options);
+
+	my $record = $options{'existing_record'};
+
+	$text =~ s/(\r\n)|\r/\n/g;
+
+	our $state = 'start';
+	our $last_state = '';
+	our $char = '';
+	our $line = 1;
+
+	our $field = undef;
+	our $buffer = '';
+	our $tag = '';
+	our $indicator = '';
+	our $subfield_code = '';
+
+	my %states = (
+		start => sub {
+			# Start of line. All buffers are empty.
+			if ($char =~ /[0-9]/) {
+				$buffer .= $char;
+				jump_state('tag_id');
+			} elsif ($char ne "\n") {
+				error("expected MARC tag number at start of line, got '$char'");
+			}
+		},
+		tag_id => sub {
+			# Jumped to from start, so buffer has first character of tag
+			# Allows letters in second and third digits of tag number
+			if (length($buffer) < 3) {
+				if ($char =~ /[0-9a-zA-Z]/) {
+					$buffer .= $char;
+				} else {
+					error("expected digit or letter, got '$char' in tag number");
+				}
+			} elsif ($char eq ' ') {
+				$tag = $buffer;
+				if ($tag =~ /^00/) {
+					set_state('control_field_content');
+				} else {
+					set_state('indicator');
+				}
+			} else {
+				error("expected whitespace after tag number, got '$char'");
+			}
+		},
+		indicator => sub {
+			# Parses optional indicator, composed of digits or lowercase letters
+			# Will consume leading $ or | of subfield if no indicator; otherwise
+			# expecting_subfield will do so
+			if (length($buffer) == 0) {
+				if ($char =~ /[\$\|]/) {
+					$indicator = '  ';
+					set_state('expecting_subfield_code');
+				} elsif ($char =~ /[0-9a-z_ ]/) {
+					$buffer .= $char;
+				} else {
+					error("expected either subfield or indicator after tag number, got '$char'");
+				}
+			} elsif (length($buffer) < 2) {
+				if ($char =~ /[0-9a-z_ ]/) {
+					$buffer .= $char;
+				} else {
+					error("expected digit, letter or blank in indicator, got '$char'");
+				}
+			} elsif ($char eq ' ') {
+				$indicator = $buffer;
+				$indicator =~ s/_/ /g;
+				set_state('expecting_subfield');
+			} else {
+				error("expected space after indicator, got '$char'");
+			}
+		},
+		expecting_subfield => sub {
+			if ($char =~ /[\$\|]/) {
+				set_state('expecting_subfield_code');
+			} else {
+				error("expected \$ or | after indicator or tag number, got '$char'");
+			}
+		},
+		expecting_subfield_code => sub {
+			if ($char =~ /[a-z0-9]/) {
+				$subfield_code = $char;
+				set_state('expecting_subfield_space');
+			} else {
+				error("expected number or letter in subfield code, got '$char'");
+			}
+		},
+		expecting_subfield_space => sub {
+			if ($char eq ' ') {
+				set_state('subfield_content');
+			} else {
+				error("expected space after subfield code, got '$char'");
+			}
+		},
+		control_field_content => sub {
+			if ($char eq "\n") {
+				if ($tag eq '000') {
+					$record->leader($buffer);
+				} else {
+					$record->append_fields(MARC::Field->new($tag, $buffer));
+				}
+				$tag = '';
+				set_state('start');
+			} else {
+				$buffer .= $char;
+			}
+		},
+		subfield_content => sub {
+			# Handles both additional subfields and inserting last subfield
+			if ($char =~ /[\$\|]/) {
+				$buffer .= $char;
+				jump_state('subfield_code');
+			} elsif ($char eq "\n") {
+				$buffer =~ s/(^\s+|\s+$)//g if ($options{'strip_whitespace'});
+				if ($field) {
+					$field->add_subfields($subfield_code, $buffer);
+				} else {
+					$field = MARC::Field->new($tag, substr($indicator, 0, 1), substr($indicator, 1), $subfield_code, $buffer);
+				}
+				$record->append_fields($field);
+
+				undef $field;
+				$tag = '';
+				$line++;
+
+				set_state('start');
+			} else {
+				$buffer .= $char;
+			}
+		},
+		# subfield_code and subfield_space both jump to subfield_content if
+		# they do not find the expected format, allowing strings like
+		# '245 $a The meaning of the $ sign' and '020 $a ... $c $10.00' to
+		# parse correctly
+		subfield_code => sub {
+			$buffer .= $char;
+
+			if ($char =~ /[a-z0-9]/) {
+				jump_state('subfield_space');
+			} elsif ($char eq "\n") {
+				error("Unexpected newline in subfield code");
+			} else {
+				jump_state('subfield_content');
+			}
+		},
+		subfield_space => sub {
+			# This has to do some manipulation of the buffer to ensure that the
+			# ending '$[a-z0-9] ' does not get inserted into the subfield
+			# contents
+			if ($char eq ' ') {
+				my $contents = substr($buffer, 0, -3);
+				$contents =~ s/(^\s+|\s+$)//g if ($options{'strip_whitespace'});
+				if ($field) {
+					$field->add_subfields($subfield_code, $contents);
+				} else {
+					$field = MARC::Field->new($tag, substr($indicator, 0, 1), substr($indicator, 1), $subfield_code, $contents);
+				}
+
+				$subfield_code = substr($buffer, -1);
+				set_state('subfield_content');
+			} else {
+				$buffer .= $char;
+				jump_state('subfield_content');
+			}
+		}
+	);
+
+	sub set_state {
+		my $new_state = shift;
+
+		print STDERR "$state -> $new_state (buffer was '$buffer'[" . length($buffer) . "])\n" if ($options{'debug'});
+
+		$buffer = '';
+		$last_state = $state;
+		$state = $new_state;
+	}
+
+	sub jump_state {
+		my $new_state = shift;
+
+		print STDERR "$state -- $new_state (buffer is '$buffer'[" . length($buffer) . "])\n" if ($options{'debug'});
+
+		$last_state = $state;
+		$state = $new_state;
+	}
+
+	sub error {
+		my $text = shift;
+		$text =~ s/\n/newline/gm;
+
+		die "Error on line $line: $text\n";
+	}
+
+	for $char (split '', $text) {
+		print STDERR "running $state with " . ($char eq "\n" ? "line-break" : "'$char'") . " and buffer '$buffer' (" . length($buffer) . " chars)\n" if ($options{'debug'} >= 2);
+		$states{$state}->();
+	}
+
+	if ($char ne "\n") {
+		print STDERR "running $state at end\n" if ($options{'debug'});
+		$char = "\n";
+		$states{$state}->();
+	}
+
+	return $record;
+}
+
 # cache inverted MARC field map
 our $inverted_field_map;
 
diff --git a/C4/Koha.pm b/C4/Koha.pm
index 06b2ec5..0b03773 100644
--- a/C4/Koha.pm
+++ b/C4/Koha.pm
@@ -38,7 +38,7 @@ BEGIN {
 		&slashifyDate
 		&subfield_is_koha_internal_p
 		&GetPrinters &GetPrinter
-		&GetItemTypes &getitemtypeinfo
+		&GetItemTypes &GetItemTypeList &getitemtypeinfo
 		&GetCcodes
 		&GetSupportName &GetSupportList
 		&get_itemtypeinfos_of
@@ -253,6 +253,22 @@ sub GetItemTypes {
     return ( \%itemtypes );
 }
 
+sub GetItemTypeList {
+	my ( $selected ) = @_;
+    my $itemtypes = GetItemTypes;
+    my @itemtypesloop;
+
+    foreach my $itemtype ( sort { $itemtypes->{$a}->{'description'} cmp $itemtypes->{$b}->{'description'} } keys( %$itemtypes ) ) {
+        push @itemtypesloop, {
+			value => $itemtype,
+            selected => ( $itemtype eq $selected ),
+            description => $itemtypes->{$itemtype}->{'description'},
+        };
+    }
+
+	return \@itemtypesloop;
+}
+
 sub get_itemtypeinfos_of {
     my @itemtypes = @_;
 
diff --git a/cataloguing/addbiblio-text.pl b/cataloguing/addbiblio-text.pl
new file mode 100755
index 0000000..a504ad0
--- /dev/null
+++ b/cataloguing/addbiblio-text.pl
@@ -0,0 +1,635 @@
+#!/usr/bin/perl
+
+# Copyright 2008 LibLime
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
+# Suite 330, Boston, MA  02111-1307 USA
+
+use strict;
+use CGI;
+use C4::Output qw(:html :ajax);
+use C4::Output::JSONStream;
+use JSON;
+use C4::Auth;
+use C4::Biblio;
+use C4::Search;
+use C4::AuthoritiesMarc;
+use C4::Context;
+use MARC::Record;
+use MARC::Field;
+use C4::Log;
+use C4::Koha;    # XXX subfield_is_koha_internal_p
+use C4::Branch;    # XXX subfield_is_koha_internal_p
+use C4::ClassSource;
+use C4::ImportBatch;
+use C4::Charset;
+
+use Date::Calc qw(Today);
+use MARC::File::USMARC;
+use MARC::File::XML;
+
+if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
+    MARC::File::XML->default_record_format('UNIMARC');
+}
+
+our($tagslib,$authorised_values_sth,$is_a_modif,$usedTagsLib,$mandatory_z3950);
+
+our ($sec, $min, $hour, $mday, $mon, $year, undef, undef, undef) = localtime(time);
+$year +=1900;
+$mon +=1;
+
+our %creators = (
+    '000@' => sub { '     nam a22     7a 4500' },
+    '005@' => sub { sprintf('%4d%02d%02d%02d%02d%02d.0', $year, $mon, $mday, $hour, $min, $sec) },
+    '008@' => sub { substr($year,2,2) . sprintf("%02d%02d", $mon, $mday) . 't        xxu||||| |||| 00| 0 eng d' },
+);
+
+=item MARCfindbreeding
+
+    $record = MARCfindbreeding($breedingid);
+
+Look up the import record repository for the record with
+record with id $breedingid.  If found, returns the decoded
+MARC::Record; otherwise, -1 is returned (FIXME).
+Returns as second parameter the character encoding.
+
+=cut
+
+sub MARCfindbreeding {
+    my ( $id ) = @_;
+    my ($marc, $encoding) = GetImportRecordMarc($id);
+    # remove the - in isbn, koha store isbn without any -
+    if ($marc) {
+        my $record = MARC::Record->new_from_usmarc($marc);
+        my ($isbnfield,$isbnsubfield) = GetMarcFromKohaField('biblioitems.isbn','');
+        if ( $record->field($isbnfield) ) {
+            foreach my $field ( $record->field($isbnfield) ) {
+                foreach my $subfield ( $field->subfield($isbnsubfield) ) {
+                    my $newisbn = $field->subfield($isbnsubfield);
+                    $newisbn =~ s/-//g;
+                    $field->update( $isbnsubfield => $newisbn );
+                }
+            }
+        }
+        # fix the unimarc 100 coded field (with unicode information)
+        if (C4::Context->preference('marcflavour') eq 'UNIMARC' && $record->subfield(100,'a')) {
+            my $f100a=$record->subfield(100,'a');
+            my $f100 = $record->field(100);
+            my $f100temp = $f100->as_string;
+            $record->delete_field($f100);
+            if ( length($f100temp) > 28 ) {
+                substr( $f100temp, 26, 2, "50" );
+                $f100->update( 'a' => $f100temp );
+                my $f100 = MARC::Field->new( '100', '', '', 'a' => $f100temp );
+                $record->insert_fields_ordered($f100);
+            }
+        }
+
+        if ( !defined(ref($record)) ) {
+            return -1;
+        }
+        else {
+            # normalize author : probably UNIMARC specific...
+            if (    C4::Context->preference("z3950NormalizeAuthor")
+                and C4::Context->preference("z3950AuthorAuthFields") )
+            {
+                my ( $tag, $subfield ) = GetMarcFromKohaField("biblio.author");
+
+ #                 my $summary = C4::Context->preference("z3950authortemplate");
+                my $auth_fields =
+                  C4::Context->preference("z3950AuthorAuthFields");
+                my @auth_fields = split /,/, $auth_fields;
+                my $field;
+
+                if ( $record->field($tag) ) {
+                    foreach my $tmpfield ( $record->field($tag)->subfields ) {
+
+       #                        foreach my $subfieldcode ($tmpfield->subfields){
+                        my $subfieldcode  = shift @$tmpfield;
+                        my $subfieldvalue = shift @$tmpfield;
+                        if ($field) {
+                            $field->add_subfields(
+                                "$subfieldcode" => $subfieldvalue )
+                              if ( $subfieldcode ne $subfield );
+                        }
+                        else {
+                            $field =
+                              MARC::Field->new( $tag, "", "",
+                                $subfieldcode => $subfieldvalue )
+                              if ( $subfieldcode ne $subfield );
+                        }
+                    }
+                }
+                $record->delete_field( $record->field($tag) );
+                foreach my $fieldtag (@auth_fields) {
+                    next unless ( $record->field($fieldtag) );
+                    my $lastname  = $record->field($fieldtag)->subfield('a');
+                    my $firstname = $record->field($fieldtag)->subfield('b');
+                    my $title     = $record->field($fieldtag)->subfield('c');
+                    my $number    = $record->field($fieldtag)->subfield('d');
+                    if ($title) {
+
+#                         $field->add_subfields("$subfield"=>"[ ".ucfirst($title).ucfirst($firstname)." ".$number." ]");
+                        $field->add_subfields(
+                                "$subfield" => ucfirst($title) . " "
+                              . ucfirst($firstname) . " "
+                              . $number );
+                    }
+                    else {
+
+#                       $field->add_subfields("$subfield"=>"[ ".ucfirst($firstname).", ".ucfirst($lastname)." ]");
+                        $field->add_subfields(
+                            "$subfield" => ucfirst($firstname) . ", "
+                              . ucfirst($lastname) );
+                    }
+                }
+                $record->insert_fields_ordered($field);
+            }
+            return $record, $encoding;
+        }
+    }
+    return -1;
+}
+
+# Borrowed from MARC::Record::JSON, due to its lack of availability on CPAN
+
+sub MARC::Record::as_json_record_structure {
+    my $self = shift;
+    my $data = { leader => $self->leader };
+    my @fields;
+    foreach my $field ($self->fields) {
+        my $json_field = { tag => $field->tag };
+
+        if ($field->is_control_field) {
+            $json_field->{contents} = $field->data;
+        } else {
+            $json_field->{indicator1} = $field->indicator(1);
+            $json_field->{indicator2} = $field->indicator(2);
+
+            $json_field->{subfields} = [ $field->subfields ];
+        }
+
+        push @fields, $json_field;
+    }
+
+    $data->{fields} = \@fields;
+
+    return $data;
+}
+
+=item GetMandatoryFieldZ3950
+
+    This function return an hashref which containts all mandatory field
+    to search with z3950 server.
+
+=cut
+
+sub GetMandatoryFieldZ3950($){
+    my $frameworkcode = shift;
+    my @isbn   = GetMarcFromKohaField('biblioitems.isbn',$frameworkcode);
+    my @title  = GetMarcFromKohaField('biblio.title',$frameworkcode);
+    my @author = GetMarcFromKohaField('biblio.author',$frameworkcode);
+    my @issn   = GetMarcFromKohaField('biblioitems.issn',$frameworkcode);
+    my @lccn   = GetMarcFromKohaField('biblioitems.lccn',$frameworkcode);
+
+    return {
+        $isbn[0].$isbn[1]     => 'isbn',
+        $title[0].$title[1]   => 'title',
+        $author[0].$author[1] => 'author',
+        $issn[0].$issn[1]     => 'issn',
+        $lccn[0].$lccn[1]     => 'lccn',
+    };
+}
+
+sub build_tabs ($$$$$) {
+    my($template, $record, $dbh,$encoding, $input) = @_;
+    # fill arrays
+    my @loop_data =();
+    my $tag;
+    my $i=0;
+    my $authorised_values_sth = $dbh->prepare("select authorised_value,lib
+        from authorised_values
+        where category=? order by lib");
+
+    # in this array, we will push all the 10 tabs
+    # to avoid having 10 tabs in the template : they will all be in the same BIG_LOOP
+    my @BIG_LOOP;
+    my @HIDDEN_LOOP;
+
+# loop through each tab 0 through 9
+    foreach my $tag (sort(keys (%{$tagslib}))) {
+        my $taglib = $tagslib->{$tag};
+        my $indicator;
+# if MARC::Record is not empty => use it as master loop, then add missing subfields that should be in the tab.
+# if MARC::Record is empty => use tab as master loop.
+        if ($record ne -1 && ($record->field($tag) || $tag eq '000')) {
+            my @fields;
+            if ($tag ne '000') {
+                @fields = $record->field($tag);
+            } else {
+                push @fields,$record->leader();
+            }
+            foreach my $field (@fields)  {
+                my $tag_writeout = "$tag ";
+                $tag_writeout .= ($field->indicator(1) eq ' ' ? '_' : $field->indicator(1)) . ($field->indicator(1) eq ' ' ? '_' : $field->indicator(1)) . ' ' if ($tag>=10);
+                my $tag_index = int(rand(1000000));
+                my @subfields_data;
+                if ($tag<10) {
+                    my ($value,$subfield);
+                    if ($tag ne '000') {
+                        $value=$field->data();
+                        $subfield="@";
+                    } else {
+                        $value = $field;
+                        $subfield='@';
+                    }
+                    my $subfieldlib = $taglib->{$subfield};
+                    next if ($subfieldlib->{kohafield} eq 'biblio.biblionumber');
+
+                    push(@subfields_data, "$value");
+                    $i++;
+                } else {
+                    my @subfields=$field->subfields();
+                    foreach my $subfieldcount (0..$#subfields) {
+                        my $subfield=$subfields[$subfieldcount][0];
+                        my $value=$subfields[$subfieldcount][1];
+                        my $subfieldlib = $taglib->{$subfield};
+                        next if (length $subfield !=1);
+                        next if ($subfieldlib->{tab} > 9 or $subfieldlib->{tab} == -1);
+                        push(@subfields_data, "\$$subfield $value");
+                        $i++;
+                    }
+                }
+# now, loop again to add parameter subfield that are not in the MARC::Record
+                foreach my $subfield (sort( keys %{$tagslib->{$tag}})) {
+                    my $subfieldlib = $taglib->{$subfield};
+                    next if (length $subfield !=1);
+                    next if ($tag<10);
+                    next if (!$subfieldlib->{mandatory});
+                    next if ($subfieldlib->{tab} > 9 or $subfieldlib->{tab} == -1);
+                    next if (defined($field->subfield($subfield)));
+                    push(@subfields_data, "\$$subfield");
+                    $i++;
+                }
+                if (@subfields_data) {
+                    $tag_writeout .= join(' ', @subfields_data);
+                    push (@BIG_LOOP, $tag_writeout);
+                }
+# If there is more than 1 field, add an empty hidden field as separator.
+            }
+# if breeding is empty
+        } else {
+            my $tag_writeout = "$tag ";
+            $tag_writeout .= '__ ' if ($tag>=10);
+            my @subfields_data;
+            foreach my $subfield (sort(keys %{$tagslib->{$tag}})) {
+                my $subfieldlib = $taglib->{$subfield};
+                next if (length $subfield !=1);
+                next if (!$subfieldlib->{mandatory});
+                next if ($subfieldlib->{tab} > 9);
+
+                if (ref($creators{$tag . $subfield}) eq 'CODE') {
+                    if (($subfieldlib->{hidden} <= -4) or ($subfieldlib->{hidden}>=5) or ($taglib->{tab} == -1)) {
+                        my %row = (
+                            tag => $tag,
+                            index => int(rand(1000000)),
+                            index_subfield => int(rand(1000000)),
+                            random => int(rand(1000000)),
+                            subfield => ($subfield eq '@' ? '00' : $subfield),
+                            subfield_value => $creators{$tag . $subfield}(),
+                        );
+                        push @HIDDEN_LOOP, \%row;
+                        next;
+                    } else {
+                        push @subfields_data, $creators{$tag . $subfield}();
+                        next;
+                    }
+                }
+
+                if ($tag >= 10) {
+                    push @subfields_data, "\$$subfield";
+                } else {
+                    push @subfields_data, "";
+                }
+                $i++;
+            }
+            next if (!@subfields_data);
+            push (@BIG_LOOP, $tag_writeout . join(' ', @subfields_data));
+        }
+    }
+#         $template->param($tabloop."XX" =>\@loop_data);
+    $template->param(
+        BIG_LOOP => join("\n", @BIG_LOOP),
+        HIDDEN_LOOP => \@HIDDEN_LOOP,
+        record_length => $#BIG_LOOP,
+    );
+}
+
+#
+# sub that tries to find authorities linked to the biblio
+# the sub :
+#   - search in the authority DB for the same authid (in $9 of the biblio)
+#   - search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
+#   - search in the authority DB for the same values (exactly) (in all subfields of the biblio)
+# if the authority is found, the biblio is modified accordingly to be connected to the authority.
+# if the authority is not found, it's added, and the biblio is then modified to be connected to the authority.
+#
+
+sub BiblioAddAuthorities{
+  my ( $record, $frameworkcode ) = @_;
+  my $dbh=C4::Context->dbh;
+  my $query=$dbh->prepare(qq|
+SELECT authtypecode,tagfield
+FROM marc_subfield_structure
+WHERE frameworkcode=?
+AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
+# SELECT authtypecode,tagfield
+# FROM marc_subfield_structure
+# WHERE frameworkcode=?
+# AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
+  $query->execute($frameworkcode);
+  my ($countcreated,$countlinked);
+  while (my $data=$query->fetchrow_hashref){
+    foreach my $field ($record->field($data->{tagfield})){
+      next if ($field->subfield('3')||$field->subfield('9'));
+      # No authorities id in the tag.
+      # Search if there is any authorities to link to.
+      my $query='at='.$data->{authtypecode}.' ';
+      map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)}  $field->subfields();
+      my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
+    # there is only 1 result
+      if ( $error ) {
+        warn "BIBLIOADDSAUTHORITIES: $error";
+        return (0,0) ;
+      }
+      if ($results && scalar(@$results)==1) {
+        my $marcrecord = MARC::File::USMARC::decode($results->[0]);
+        $field->add_subfields('9'=>$marcrecord->field('001')->data);
+        $countlinked++;
+      } elsif (scalar(@$results)>1) {
+   #More than One result
+   #This can comes out of a lack of a subfield.
+#         my $marcrecord = MARC::File::USMARC::decode($results->[0]);
+#         $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
+  $countlinked++;
+      } else {
+  #There are no results, build authority record, add it to Authorities, get authid and add it to 9
+  ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode
+  ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
+         my $authtypedata=GetAuthType($data->{authtypecode});
+         next unless $authtypedata;
+         my $marcrecordauth=MARC::Record->new();
+         my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
+         map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )}  $field->subfields();
+         $marcrecordauth->insert_fields_ordered($authfield);
+
+         # bug 2317: ensure new authority knows it's using UTF-8; currently
+         # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
+         # automatically for UNIMARC (by not transcoding)
+         # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
+         # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
+         # of change to a core API just before the 3.0 release.
+         if (C4::Context->preference('marcflavour') eq 'MARC21') {
+            SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
+         }
+
+#          warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
+
+         my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
+         $countcreated++;
+         $field->add_subfields('9'=>$authid);
+      }
+    }
+  }
+  return ($countlinked,$countcreated);
+}
+
+# ========================
+#          MAIN
+#=========================
+my $input = new CGI;
+my $error = $input->param('error');
+my $biblionumber  = $input->param('biblionumber'); # if biblionumber exists, it's a modif, not a new biblio.
+my $breedingid    = $input->param('breedingid');
+my $z3950         = $input->param('z3950');
+my $op            = $input->param('op');
+my $mode          = $input->param('mode');
+my $record_text   = $input->param('record');
+my $frameworkcode = $input->param('frameworkcode');
+my $dbh           = C4::Context->dbh;
+
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
+    {
+        template_name   => "cataloguing/addbiblio-text.tmpl",
+        query           => $input,
+        type            => "intranet",
+        authnotrequired => 0,
+        flagsrequired   => { editcatalogue => 1 },
+    }
+);
+
+if (is_ajax() && $op eq 'try_parse') {
+    my @params = $input->param();
+    my $record = TransformHtmlToMarc( \@params , $input );
+    my $response = new C4::Output::JSONStream;
+
+    eval {
+           $record = TransformTextToMarc( $record_text, existing_record => $record )
+    };
+    if ( $@ ) {
+        chomp $@;
+        $response->param( type => 'input', error => 'parse_failed', message => $@ );
+
+        output_with_http_headers $input, $cookie, $response->output, 'json';
+        exit;
+    }
+
+    $response->param( record => $record->as_json_record_structure );
+
+    output_with_http_headers $input, $cookie, $response->output, 'json';
+    exit;
+}
+
+$frameworkcode = &GetFrameworkCode($biblionumber)
+  if ( $biblionumber and not($frameworkcode) );
+
+$frameworkcode = '' if ( $frameworkcode eq 'Default' );
+
+# Getting the list of all frameworks
+# get framework list
+my $frameworks = getframeworks;
+my @frameworkcodeloop;
+foreach my $thisframeworkcode ( keys %$frameworks ) {
+    my %row = (
+        value         => $thisframeworkcode,
+        frameworktext => $frameworks->{$thisframeworkcode}->{'frameworktext'},
+    );
+    if ($frameworkcode eq $thisframeworkcode){
+        $row{'selected'}="selected=\"selected\"";
+        }
+    push @frameworkcodeloop, \%row;
+}
+$template->param( frameworkcodeloop => \@frameworkcodeloop,
+    breedingid => $breedingid );
+
+# ++ Global
+$tagslib         = &GetMarcStructure( 1, $frameworkcode );
+$usedTagsLib     = &GetUsedMarcStructure( $frameworkcode );
+$mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode);
+# -- Global
+
+my $record   = -1;
+my $encoding = "";
+my (
+    $biblionumbertagfield,
+    $biblionumbertagsubfield,
+    $biblioitemnumtagfield,
+    $biblioitemnumtagsubfield,
+    $bibitem,
+    $biblioitemnumber
+);
+
+if (($biblionumber) && !($breedingid)){
+    $record = GetMarcBiblio($biblionumber);
+}
+if ($breedingid) {
+    ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ;
+}
+
+$is_a_modif = 0;
+
+if ($biblionumber) {
+    $is_a_modif = 1;
+    $template->param( title => $record->title(), );
+
+    # if it's a modif, retrieve bibli and biblioitem numbers for the future modification of old-DB.
+    ( $biblionumbertagfield, $biblionumbertagsubfield ) =
+    &GetMarcFromKohaField( "biblio.biblionumber", $frameworkcode );
+    ( $biblioitemnumtagfield, $biblioitemnumtagsubfield ) =
+    &GetMarcFromKohaField( "biblioitems.biblioitemnumber", $frameworkcode );
+
+    # search biblioitems value
+    my $sth =  $dbh->prepare("select biblioitemnumber from biblioitems where biblionumber=?");
+    $sth->execute($biblionumber);
+    ($biblioitemnumber) = $sth->fetchrow;
+}
+
+#-------------------------------------------------------------------------------------
+if ( $op eq "addbiblio" ) {
+#-------------------------------------------------------------------------------------
+    # getting html input
+    my @params = $input->param();
+    $record = TransformHtmlToMarc( \@params , $input );
+    eval {
+           $record = TransformTextToMarc( $record_text, existing_record => $record )
+    };
+    # check for a duplicate
+    my ($duplicatebiblionumber,$duplicatetitle) = FindDuplicate($record) if (!$is_a_modif);
+    my $confirm_not_duplicate = $input->param('confirm_not_duplicate');
+    # it is not a duplicate (determined either by Koha itself or by user checking it's not a duplicate)
+    if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
+        my $oldbibnum;
+        my $oldbibitemnum;
+        if (C4::Context->preference("BiblioAddsAuthorities")){
+          my ($countlinked,$countcreated)=BiblioAddAuthorities($record,$frameworkcode);
+        }
+        if ( $is_a_modif ) {
+            ModBiblioframework( $biblionumber, $frameworkcode );
+            ModBiblio( $record, $biblionumber, $frameworkcode );
+        }
+        else {
+            ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
+        }
+
+        if ($mode ne "popup"){
+            print $input->redirect(
+                "/cgi-bin/koha/cataloguing/additem.pl?biblionumber=$biblionumber&frameworkcode=$frameworkcode"
+            );
+            exit;
+        } else {
+          $template->param(
+            biblionumber => $biblionumber,
+            done         =>1,
+            popup        =>1
+          );
+          $template->param( title => $record->subfield('200',"a") ) if ($record ne "-1" && C4::Context->preference('marcflavour') =~/unimarc/i);
+          $template->param( title => $record->title() ) if ($record ne "-1" && C4::Context->preference('marcflavour') eq "usmarc");
+          $template->param(
+            popup => $mode,
+            itemtype => $frameworkcode,
+          );
+          output_html_with_http_headers $input, $cookie, $template->output;
+          exit;
+        }
+    } else {
+    # it may be a duplicate, warn the user and do nothing
+        build_tabs ($template, $record, $dbh,$encoding,$input);
+        $template->param(
+            biblionumber             => $biblionumber,
+            biblioitemnumber         => $biblioitemnumber,
+            duplicatebiblionumber    => $duplicatebiblionumber,
+            duplicatebibid           => $duplicatebiblionumber,
+            duplicatetitle           => $duplicatetitle,
+        );
+    }
+}
+elsif ( $op eq "delete" ) {
+
+    my $error = &DelBiblio($biblionumber);
+    if ($error) {
+        warn "ERROR when DELETING BIBLIO $biblionumber : $error";
+        print "Content-Type: text/html\n\n<html><body><h1>ERROR when DELETING BIBLIO $biblionumber : $error</h1></body></html>";
+    exit;
+    }
+
+    print $input->redirect('/cgi-bin/koha/catalogue/search.pl');
+    exit;
+
+} else {
+   #----------------------------------------------------------------------------
+   # If we're in a duplication case, we have to set to "" the biblionumber
+   # as we'll save the biblio as a new one.
+    if ( $op eq "duplicate" ) {
+        $biblionumber = "";
+    }
+
+#FIXME: it's kind of silly to go from MARC::Record to MARC::File::XML and then back again just to fix the encoding
+    eval {
+        my $uxml = $record->as_xml;
+        MARC::Record::default_record_format("UNIMARC")
+          if ( C4::Context->preference("marcflavour") eq "UNIMARC" );
+        my $urecord = MARC::Record::new_from_xml( $uxml, 'UTF-8' );
+        $record = $urecord;
+    };
+    build_tabs( $template, $record, $dbh, $encoding,$input );
+    $template->param(
+        biblionumber             => $biblionumber,
+        biblionumbertagfield        => $biblionumbertagfield,
+        biblionumbertagsubfield     => $biblionumbertagsubfield,
+        biblioitemnumtagfield    => $biblioitemnumtagfield,
+        biblioitemnumtagsubfield => $biblioitemnumtagsubfield,
+        biblioitemnumber         => $biblioitemnumber,
+    );
+}
+
+$template->param( title => $record->title() ) if ( $record ne "-1" );
+$template->param(
+    popup => $mode,
+    frameworkcode => $frameworkcode,
+    itemtype => $frameworkcode,
+    itemtypes => GetItemTypeList(),
+);
+
+output_html_with_http_headers $input, $cookie, $template->output;
diff --git a/cataloguing/addbiblio.pl b/cataloguing/addbiblio.pl
index e912c66..06fefda 100755
--- a/cataloguing/addbiblio.pl
+++ b/cataloguing/addbiblio.pl
@@ -823,7 +823,7 @@ AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
 
 # ========================
 #          MAIN
-#=========================
+#========================
 my $input = new CGI;
 my $error = $input->param('error');
 my $biblionumber  = $input->param('biblionumber'); # if biblionumber exists, it's a modif, not a new biblio.
@@ -836,6 +836,10 @@ my $redirect      = $input->param('redirect');
 my $dbh           = C4::Context->dbh;
 
 my $userflags = ($frameworkcode eq 'FA') ? "fast_cataloging" : "edit_catalogue";
+if (C4::Context->preference('MARCEditor') eq 'text') {
+	print $input->redirect('/cgi-bin/koha/cataloguing/addbiblio-text.pl?' . $ENV{'QUERY_STRING'});
+	exit;
+}
 
 $frameworkcode = &GetFrameworkCode($biblionumber)
   if ( $biblionumber and not($frameworkcode) and $op ne 'addbiblio' );
diff --git a/cataloguing/framework-jsonp.pl b/cataloguing/framework-jsonp.pl
new file mode 100755
index 0000000..1d8c3fd
--- /dev/null
+++ b/cataloguing/framework-jsonp.pl
@@ -0,0 +1,60 @@
+#!/usr/bin/perl
+
+use CGI;
+use C4::Context;
+use C4::Biblio;
+
+my $input = new CGI;
+our $dbh = C4::Context->dbh;
+
+my $frameworkcode = $input->param('frameworkcode') || '';
+my $info = $input->param('info') || 'kohalinks';
+my $prepend = $input->param('prepend') || '';
+my $append = $input->param('append') || '';
+
+my $tagslib = GetMarcStructure(1, $frameworkcode);
+
+print $input->header('text/javascript');
+
+print $prepend . "{";
+
+if ($info eq 'kohalinks') {
+	foreach my $tag (sort(keys (%{$tagslib}))) {
+		my $taglib = $tagslib->{$tag};
+		foreach my $subfield (sort(keys %{$taglib})) {
+			my $subfieldlib = $taglib->{$subfield};
+			if ($subfieldlib->{kohafield}) {
+				print "'" . $subfieldlib->{kohafield} . "':['$tag','$subfield'],";
+			}
+		}
+	}
+} elsif ($info eq 'mandatory') {
+	my @mandatory_tags;
+	my @mandatory_subfields;
+
+	foreach my $tag (sort(keys (%{$tagslib}))) {
+		my $taglib = $tagslib->{$tag};
+		push @mandatory_tags, $tag if ($taglib->{mandatory});
+		foreach my $subfield (sort(keys %{$taglib})) {
+			my $subfieldlib = $taglib->{$subfield};
+			push @mandatory_subfields, "['$tag','$subfield']" if ($subfieldlib->{mandatory} && $subfieldlib->{tab} != -1 && $subfieldlib->{tab} != 10);
+		}
+	}
+
+	print "tags:[";
+	foreach my $tag (@mandatory_tags) { print "'$tag',"; }
+	print "],";
+
+	print "subfields:[";
+	foreach my $subfield (@mandatory_subfields) { print "$subfield,"; }
+	print "]";
+} elsif ($info eq 'itemtypes') {
+	my $sth=$dbh->prepare("select itemtype,description from itemtypes order by description");
+	$sth->execute;
+
+	while (my ($itemtype,$description) = $sth->fetchrow_array) {
+		print "'$itemtype':'$description',";
+	}
+}
+
+print "}" . $append;
diff --git a/installer/data/mysql/en/mandatory/sysprefs.sql b/installer/data/mysql/en/mandatory/sysprefs.sql
index 8407505..1f12879 100755
--- a/installer/data/mysql/en/mandatory/sysprefs.sql
+++ b/installer/data/mysql/en/mandatory/sysprefs.sql
@@ -317,3 +317,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES (
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('BasketConfirmations', '1', 'When closing or reopening a basket,', 'always ask for confirmation.|do not ask for confirmation.', 'Choice');
 INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('MARCAuthorityControlField008', '|| aca||aabn           | a|a     d', NULL, NULL, 'Textarea');
 INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpenLibraryCovers',0,'If ON Openlibrary book covers will be show',NULL,'YesNo');
+INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('MARCEditor','normal','Use the normal or textual MARC editor','normal|text','Choice');
diff --git a/installer/data/mysql/updatedatabase.pl b/installer/data/mysql/updatedatabase.pl
index 720bd8c..75f5a2b 100755
--- a/installer/data/mysql/updatedatabase.pl
+++ b/installer/data/mysql/updatedatabase.pl
@@ -4399,6 +4399,13 @@ if (C4::Context->preference("Version") < TransformToNum($DBversion)) {
     SetVersion($DBversion);
 }
 
+$DBversion = "3.05.00.XXX";
+if (C4::Context->preference("Version") < TransformToNum($DBversion)) {
+    $dbh->do("INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('MARCEditor','normal','Use the normal or textual MARC editor','normal|text','Choice');");
+    print "Upgrade to $DBversion done (Add syspref MARCEditor)\n";
+    SetVersion($DBversion);
+}
+
 =head1 FUNCTIONS
 
 =head2 DropAllForeignKeys($table)
diff --git a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
index 3c5d257..78822c2 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
+++ b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
@@ -2081,3 +2081,8 @@ fieldset.rows+h3 {clear:both;padding-top:.5em;}
     color : #cc0000;
     }
 
+#embedded_z3950 {
+	width: 100%;
+	height: 500px;
+	border: none;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
index 6a2dae0..c3ca8d9 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
@@ -72,6 +72,9 @@
 <script type="text/javascript" src="[% yuipath %]/container/container_core-min.js"></script> 
 <script type="text/javascript" src="[% yuipath %]/menu/menu-min.js"></script> 
 
+<script type="text/javascript">
+var koha = { themelang: '[% themelang %]' };
+</script>
 <!-- koha core js -->
 <script type="text/javascript" src="[% themelang %]/js/staff-global.js"></script>
 [% IF ( intranetuserjs ) %]
diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/marc.js b/koha-tmpl/intranet-tmpl/prog/en/js/marc.js
new file mode 100644
index 0000000..3e3a568
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/js/marc.js
@@ -0,0 +1,194 @@
+/* From MARC::Record::JSON: http://code.google.com/p/marc-json/downloads/list */
+/* Modified by Jesse Weaver */
+
+/*===========================================
+  MARC.Field(fdata)
+
+A MARC Field, as pulled from the json data.
+
+You can usually get what you want using MARCRecord.subfield(tag, sub)
+but may need this for more advanced usage
+
+  f = new MARC.Field(data);
+  isbn = f.subfield('a'); // if it's an 020, of course
+  isbn = f.as_string('a'); // same thing
+
+  alltitlestuff = f.as_string(); // if it's a 245
+  propertitle = f.as_string('anp'); // just the a, n, and p subfields
+
+  subfield('a', sep=' ') -- returns:
+    '' iff there is no subfield a
+    'value' iff there is exactly one subfield a
+    'value1|value2' iff there are more than on subfield a's
+
+  as_string(spec, sep, includesftags) -- where spec is either empty or a string of concat'd subfields.
+    spec is either null (all subfields) or a string listing the subfields (e.g., 'a' or 'abh')
+    sep is the string used to separate the values; a single space is the default
+    includesftags is a boolean that determines if the subfield tags will be included (e.g, $$a data $$h moredata)
+
+    It returns the found data joined by the string in 'sep', or an empty string if nothing is found.
+
+
+===============================================*/
+
+marc = {}
+
+marc.field = function ( tag, ind1, ind2, subfields ) {
+    this.tag = tag;
+
+    if (tag < 10) {
+        this.is_control_field = true;
+        this.data = ind1;
+        return;
+    }
+
+    this._subfields = subfields;
+
+    this._subfield_map = {};
+
+    if ( ind1 == '' ) ind1 = ' ';
+    if ( ind2 == '' ) ind2 = ' ';
+
+    this._indicators = [ ind1, ind2 ];
+
+    var field = this;
+
+    $.each( subfields, function( i, subfield ) {
+        var code = subfield[0];
+
+        if (!(code in field._subfield_map)) field._subfield_map[code] = [];
+
+        field._subfield_map[code].push(subfield[1]);
+    } );
+}
+
+$.extend( marc.field.prototype, {
+    indicator: function(ind) {
+        if (this.is_control_field) throw TypeError('indicator() called on control field');
+        if (ind != 1 && ind != 2) return null;
+
+        return this._indicators[ind - 1];
+    },
+
+    subfield: function(code) {
+        if (this.is_control_field) throw TypeError('subfield() called on control field');
+        if (!(code in this._subfield_map)) return null;
+
+        return this._subfield_map[code][0];
+    },
+
+    subfields: function(code) {
+        if (this.is_control_field) throw TypeError('subfields() called on control field');
+        if (code === undefined) {
+            return self._subfields;
+        } else {
+            if (!(code in this._subfield_map)) return null;
+
+            return this._subfield_map[code];
+        }
+    },
+
+    as_string: function() {
+        var buffer = [ this.tag, ' ' ];
+
+        if ( this.is_control_field ) {
+            buffer.push( this.data );
+        } else {
+            buffer.push( this._indicators[0], this._indicators[1], ' ' );
+
+            $.each( this.subfields, function( i, subfield ) {
+                buffer.push( '$', subfield[0], ' ', subfield[1] );
+            } );
+        }
+    },
+});
+
+
+/*===========================================
+MARCRecord -- a MARC::Record-like object
+
+  r.cfield('008') -- the contents of the 008 control field
+  r.cfield('LDR') -- ditto with the leader
+
+  array = r.controlFieldTags(); -- a list of the control field tags, for feeding into cfield
+
+  array = r.dfield('022') -- all the ISSN fields
+  r.dfield('022')[0].as_string -- the first 022 as a string
+  r.dfield('245')[0].as_string(); -- the title as a string
+  r.dfield('FAK') -- returns an empty array
+
+  r.dfields() -- return an array of all dfields
+
+  r.field('245')[0] -- 'field' is an alias for 'dfield'
+
+  r.subfield('245', 'a') -- the first 245/a
+  r.subfield('100', 'a') -- the author?
+
+  // Convenience functions
+
+  str = r.title();
+  str = r.author(); // Looks in 100, 110, and 111 in that order; returns '' on fail
+  edition = r.edition(); // from the 250/a
+
+
+===========================================*/
+
+marc.record = function(structure) {
+    this.leader = new Array(25).join(' '); // Poor man's ' ' x 24
+    this._fields = [];
+    this._field_map = {};
+
+    if (structure) {
+        this.leader = structure.leader;
+        var record = this;
+
+        $.each( structure.fields, function( i, field ) {
+            var tag = field.tag;
+
+            if ( !( tag in record._field_map ) ) record._field_map[tag] = [];
+
+            var f = field.contents ? new marc.field( tag, field.contents ) : new marc.field( tag, field.indicator1, field.indicator2, field.subfields );
+
+            record._fields.push( f );
+            record._field_map[tag].push( f );
+        } );
+    }
+}
+
+$.extend( marc.record.prototype, {
+    subfield: function(tag, subfield) {
+        if ( !( tag in this._field_map ) ) return false;
+
+        if ( subfield === undefined ) return true;
+
+        var found = null;
+
+        $.each( this._field_map[tag], function( i, field ) {
+            found = field.subfield( subfield );
+
+            if ( found ) return false;
+        } );
+
+        return found;
+    },
+
+    has: function( tag, subfield ) {
+        return Boolean( this.subfield( tag, subfield ) );
+    },
+
+    field: function(tag) {
+        if (!(tag in this._field_map)) return null;
+
+        return this._field_map[tag][0];
+    },
+
+    fields: function(tag) {
+        if (tag === undefined) {
+            return self._fields;
+        } else {
+            if (!(tag in this._field_map)) return null;
+
+            return this._field_map[tag];
+        }
+    },
+} );
diff --git a/koha-tmpl/intranet-tmpl/prog/en/js/pages/addbiblio-text.js b/koha-tmpl/intranet-tmpl/prog/en/js/pages/addbiblio-text.js
new file mode 100644
index 0000000..9e40499
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/js/pages/addbiblio-text.js
@@ -0,0 +1,82 @@
+addbiblio = {};
+
+$.extend( addbiblio, {
+	submit: function() {
+		$.ajax( {
+			url: '/cgi-bin/koha/cataloguing/addbiblio-text.pl',
+			type: 'POST',
+			dataType: 'json',
+			data: $( '#f input[name^="tag"]' ).serialize() + '&op=try_parse&record=' + escape(addbiblio.editor.getCode()),
+			success: addbiblio.submit.finished,
+		} );
+	},
+	insert_itemtype: function( event ) {
+		var iter = addbiblio.editor.cursorPosition();
+		addbiblio.editor.insertIntoLine( iter.line, iter.character, $( '#itemtypes' ).val() );
+
+		return false;
+	},
+	z3950_search: function() {
+		window.open( "/cgi-bin/koha/cataloguing/z3950_search.pl?biblionumber=" + addbiblio.biblionumber,"z3950search",'width=740,height=450,location=yes,toolbar=no,scrollbars=yes,resize=yes' );
+	},
+	not_duplicate: function() {
+		$( "#confirm_not_duplicate" ).attr( "value", "1" );
+		$( "#f" ).get( 0 ).submit();
+	},
+} );
+
+$.extend( addbiblio.submit, {
+	finished: function( data, status_ ) {
+		if ( data.error ) {
+			humanMsg.displayMsg( '<strong>Watch your language:</strong> ' + data.message );
+			return false;
+		}
+
+		var record = new marc.record(data.record);
+
+		var missing_tags = [], missing_subfields = [];
+
+		$.each( addbiblio.mandatory.tags, function( i, tag ) {
+			if ( tag == '000' ) {
+				if ( !record.leader) missing_tags.push( 'leader' );
+			} else if ( !record.has( tag ) ) {
+				missing_tags.push( tag );
+			}
+		} );
+
+		$.each( addbiblio.mandatory.subfields, function( i, sf ) {
+			if ( sf[0].substring( 0, 2 ) != '00' && !record.has( sf[0], sf[1] ) ) {
+				missing_subfields.push( sf.join( '$' ) );
+			}
+		} );
+
+		if ( missing_tags.length || missing_subfields.length ) {
+			message = [];
+
+			if ( missing_tags.length ) {
+				message.push( missing_tags.join( ', ' ) + ' tags' );
+			}
+
+			if ( missing_subfields.length ) {
+				message.push( missing_subfields.join( ', ' ) + ' subfields' );
+			}
+
+			humanMsg.displayMsg( '<strong>Record is missing pieces:</strong> ' + message.join( ' and ' ) + ' are mandatory' );
+			return;
+		}
+
+		$( '#f' ).get( 0 ).submit();
+	}
+} );
+
+$( function () {
+	$( '#insert-itemtype' ).click( addbiblio.insert_itemtype );
+
+	addbiblio.editor = CodeMirror.fromTextArea('record', {
+		height: "350px",
+		parserfile: "parsemarc.js",
+		stylesheet: koha.themelang + "/lib/codemirror/css/marccolors.css",
+		path: koha.themelang + "/lib/codemirror/js/",
+		autoMatchParens: true
+	});
+} );
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/LICENSE b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/LICENSE
new file mode 100644
index 0000000..cee9537
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/LICENSE
@@ -0,0 +1,23 @@
+ Copyright (c) 2007-2008 Marijn Haverbeke
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any
+ damages arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any
+ purpose, including commercial applications, and to alter it and
+ redistribute it freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must
+    not claim that you wrote the original software. If you use this
+    software in a product, an acknowledgment in the product
+    documentation would be appreciated but is not required.
+
+ 2. Altered source versions must be plainly marked as such, and must
+    not be misrepresented as being the original software.
+
+ 3. This notice may not be removed or altered from any source
+    distribution.
+
+ Marijn Haverbeke
+ marijnh at gmail
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/csscolors.css b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/csscolors.css
new file mode 100644
index 0000000..100c93f
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/csscolors.css
@@ -0,0 +1,47 @@
+.editbox {
+  margin: .4em;
+  padding: 0;
+  font-family: monospace;
+  font-size: 10pt;
+  color: black;
+}
+
+pre.code, .editbox {
+  color: #666666;
+}
+
+.editbox p {
+  margin: 0;
+}
+
+span.css-at {
+  color: #770088;
+}
+
+span.css-unit {
+  color: #228811;
+}
+
+span.css-value {
+  color: #770088;
+}
+
+span.css-identifier {
+  color: black;
+}
+
+span.css-important {
+  color: #0000FF;
+}
+
+span.css-colorcode {
+  color: #004499;
+}
+
+span.css-comment {
+  color: #AA7700;
+}
+
+span.css-string {
+  color: #AA2222;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/docs.css b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/docs.css
new file mode 100644
index 0000000..ff7e4dc
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/docs.css
@@ -0,0 +1,42 @@
+body {
+  margin: 0;
+  font-family: tahoma, arial, sans-serif;
+  padding: 3em 6em;
+  color: black;
+}
+
+h1 {
+  font-size: 22pt;
+}
+
+h2 {
+  font-size: 14pt;
+}
+
+p.rel {
+  padding-left: 2em;
+  text-indent: -2em;
+}
+
+div.border {
+  border: 1px solid black;
+  padding: 3px;
+}
+
+code {
+  font-family: courier, monospace;
+  font-size: 90%;
+  color: #155;
+}
+
+pre.code {
+  margin: 1.1em 12px;
+  border: 1px solid #CCCCCC;
+  color: black;
+  padding: .4em;
+  font-family: courier, monospace;
+}
+
+.warn {
+  color: #C00;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/jscolors.css b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/jscolors.css
new file mode 100644
index 0000000..3067628
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/jscolors.css
@@ -0,0 +1,55 @@
+.editbox {
+  margin: .4em;
+  padding: 0;
+  font-family: monospace;
+  font-size: 10pt;
+  color: black;
+}
+
+pre.code, .editbox {
+  color: #666666;
+}
+
+.editbox p {
+  margin: 0;
+}
+
+span.js-punctuation {
+  color: #666666;
+}
+
+span.js-operator {
+  color: #666666;
+}
+
+span.js-keyword {
+  color: #770088;
+}
+
+span.js-atom {
+  color: #228811;
+}
+
+span.js-variable {
+  color: black;
+}
+
+span.js-variabledef {
+  color: #0000FF;
+}
+
+span.js-localvariable {
+  color: #004499;
+}
+
+span.js-property {
+  color: black;
+}
+
+span.js-comment {
+  color: #AA7700;
+}
+
+span.js-string {
+  color: #AA2222;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/marccolors.css b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/marccolors.css
new file mode 100644
index 0000000..c1f292a
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/marccolors.css
@@ -0,0 +1,24 @@
+
+.editbox {
+  margin: .4em;
+  padding: 0;
+  font-family: monospace;
+  font-size: 10pt;
+  color: black;
+}
+
+.editbox p {
+  margin: 0;
+}
+
+span.marc-tag {
+  color: #880;
+}
+
+span.marc-indicator {
+  color: #088;
+}
+
+span.marc-subfield {
+  color: #808;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/people.jpg b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/people.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..734789542b3533f464c3eb18908f06e48f44e55d
GIT binary patch
literal 14122
zcmb8WRa9I}6D~aH0KqK*28X~PNg%jOfMIZlK>`7S%i!)3+#$FP_Tn-)1PDPBAZXCw
z&fqRNeE-#dE>G>%d-dw-T~BviRChns``@2`s{mrCyrMh+4Gj$dd42%@)&P`pUN)Zr
z02LJg2LJ%Tc`ncLZxJ91z{0 at 9#KgdQeqmu at VFU4TfX_no;sq`~2 at xqN2@wei871f?
z896lt3CSzQSJbq$^z`&(FPT`F=vY8>^mP9-g7*9=5F1E{gF{G1PC`!i|1AG{03<ko
z3qUOf8WRAW1Py}(?O#6t^gKr3^CbR%f{yt-MjVX)X%&b8Xc!n6|HH<>1!7_V0BGnK
zm;fviY*I!ZAQ_Xi205=8JeWczsj!}sxfkcX<|mirqK4lre6m`uA!{%B`%bk(Q=Sdc
zpaIaZ{x2Bp|5?HU0-lvZB+n at 5m>Ad?IOyol7|$qxXC(>NGYGb{2H7)0VJ|sT5(Oo%
zjAs4s6W|)N?0c=#e+vM7jOU&t7$g8mz#k$gN+3fO?M9=kI`)3)PD`M+l^=U{bEk2s
zAq~7lC?hpak!>`)&?@Bd*D`D;UW&d%y}F+n!60VxNI6Cpq7oG;!V93)Epst)t8|H~
zB^Y4e*Oo}jkZ+vvMKeD8C^=U``AhLX%Wtjwxc6FX>c`@N;hL{5SQfz-Pr8k*A{9m3
z(gzRRT$N2M<#8h{K6p&W&ME%@@9!fT4)n4sJ)_<;8Cf)JM&iK7&ifHNKl8e^x4U$F
z9exO8PnRGPI%^chlD<BC at cRX}SlwF)FhINe2S6g^_Oai0eOxYjeeX0zz6ZR2-6(P?
zfq+6&d=mqp=qUly`<6NWNG^+8VWq1Jdc<8I=U-RbZ~B){l!eBqze&ic3&r9W)95+Z
zWm`?Nrb+*~lh~PdjI0Prh at HwgY44-^d3tB7rQgXWe7{pGx?WnSoqe8GIROfwiM^w`
z5l_PgvdZs^u6#sFoaNA8QIGy at x{D*)If^nw69bBb at R$^9?#sP(?u26A)$$*=tZq*2
zb_xlUKf#|pA$~7sKPEbp at DE^6%%*OrgK97G7oh(6V&>&#8D2YNFE-rtH%a;Nt~B&5
z8MgP<q|G-krf;j@@1Y#k{{V?T)sUD?EKal^R`>i9#%oE5Nb at ZNQ(I$bTHtXb8eKf~
zZkJYhs2QYN?QcGtis;czpyGw%HE-oVfVqEQcm;`DY?1sHx&+0(BV4<>!hmhdz*eAg
zx`Mv5YwUddSOv8Is1VC^E)4P(v-|7Rdk2;6MVg<7SA5u3ld0{C&r~VD(AzT5fVF6Y
z*8c&VRsCtjG!|xH<qTE(;HUg?YPC+{GaJ?W4DG-}r`i*wXv2avdQ-VoDG;{1xC=v%
zkawN-(KSHsFRO0Na_9x*tsW<(W+(!aV2+c>4zx0CibPel8t-a6j%}R~7`J6)^y-;e
zjh)G=({H93mBOK2hkjOvDlRads!W8~kI&U0`-3i_<r({?<i38FGf%#ZfIand$WAEC
zPBZjycp+}^#UsII`6Xkdn%b9X&kd-w<#w(UvyHl_TE2E at QmDR>@^@wc&7UGVQmo%U
zdmMayEAS3~lHq3GzcY^FfH-95mJBRHbwbxPWdssciKOI-*h-i}&4cj{4z;rKrfQcS
z!}Cy3c^Ve<;`FgYM>R1k3H{PySC!s$2UQOK(xnXYYg9IDYQ=#8Y`~#$1|o~yA+Nct
zFi9$3Ga7W~RyTBD=fuN-_s6EK68obRR706ZLKZ=*Tfn4!G0__Dpy;sof8VP6cK)xf
zXILGvknR6C^ya5hvp3jAZl*+fy226!ND9HZ6u!|@aiad)Jv7seQ8kp}VAiAFl-RWx
zL7o_--2h)7mq-mhQa-OT&b$}ElbbOc#`Mg at 12U8i4p5|dpZUK73c9llQip$?cq}4w
zX!@CVm_eF8)5e_AQu~!^kZL>UPuWk9x92sHZv_>Byy=de<5Pj}vdB`?k8b|vHSyAI
zUT#zq$ZW5wb$tTqu*pET_i+Ju5dEg8-hg{H0tJuVdG&P?pQ$V*?>zT at U<|NPxO3Xa
zTuHCr+O$>rrxe8r-D0yV_DL=(aCw<DJPxGK#m)Lpxzaw{cXk&ma+;DXQqoOEMpI+s
zWR+;uMF?T<E)~1%b5?hJ5Xdb?{b#?|(Xq>0-}CZYdLEuH7~XGJ4wj9E8Yxxi1JPtV
zG<q+o01rKg28Z<V`R2DtU3JZwPebRbLOE6Tf7 at 8tz9((U)Sn_mJW!YMyD_n!UwL0m
z7R2AMhb6yLB~=h&aUGQ8IKDc)e|ebDu<H93>8s0&^1QPOROzPA7|cqCAdb#k9DtM8
zk9}E?lB0WhdzXY#yQeh>KYzUB;$4K%yeoZzgDjRWl=R>~I2e}-w}+m#Tiej>g&!_V
z^}xYsQyiWf?&K{bL%J%H6qKkcQ=fA&4myd;mG3B7SmRhdYA3ZEfYLeGw>MjWSun#f
zlM#s$XMx41zo<{ki<-FDTac78)vSX;vQZY*P&^2Pg)3|olvh^c{a<0qe7(rM-Q>K-
z-c1 at 0tz0%)EiysLVzF%9n4h%<6KU(4>aO at 9+wXYgSQ835rq%QUSeQPj+8l9bM>J0E
zB{6B51Pm;M?!usbS3=PEi*in8=7XG5jhF84Q9K?Niv?)z`YzJ6+cLpJ;M3D^OB26*
zWk6w<8FxnxO^nB=%g12)`Qj54X{@OQ#hDZb)l>bLS6_RU%g^Rbsl{xmM^xHHW4dK?
zPjHJs at crLmHkF!XGPRzzCFL>-XLx^8I%R6aWC*FY3*kn}R6;J))NpOyovy4=-ovWC
z#S*T1?95(uq_`XeSmg at Nxji2Phs-&Dxh%m~&ak&3fWqeEm_!dyq|Po2DLJv54h9m(
z!oS?wZ71Bt*EjH95WRP{R*;x at nRj5VNJoM1xIk2XHpth|U}I!hD at pL9J7mw&;;ijw
z{m$A=cz4L|Ni}724K-{vBHx8|Oj>56v#C6su_s=2i9A_)K$wb(YDmW_1`Rbs8%olq
zo=l$@R#8SHN|}2L#_GgeB_ at l1KNoFkQj}~4D{7%+{%9$@A`eINZLxxnX-kg;u_e5D
zCscEKX_Wr9q7!M~T%@VJen#9$(RR#!bEeA6lY^gwf3E6B)+$;-h+%T>qy(nNMA@|`
zuJRuXd`n9zT;$qND7xl;KfRlOSShQ<z$*LsFt*@mV`pkqjJ<8oI{iNGW;ls)NH?_!
zYRLFT#CV`L8Hd(eNL)nAX>suIV}rzK at L~7Nk^X4g- at V*Nft!GjbT&Tx)Tj*Ku*%Tp
zUB%2-jN40=TaAp0;01D0XtLBU;vYa^Bg-{geeR{5LqG<N_s9L5?P=tXzN@)!N^zZy
zM3P129@!s6{nL+$e;%_{TSP7?5`+nn1fk2TLUtsKg*W8?oL?ETT>kicpF0a`=1Tu#
zS%SkT4YkCRwWyv3vAq2e1k at U?rZr{MZ5|pEWOscmmN()sG7IoWdG5J7DKP|q$NvGS
z+>g(lChvUSz at k_+<i#oOs7Laxh0MAL`ccrd#H#bbe*m4Tx8S8-Izw%>$p)?OZSMY)
zp-~!o8PX}UC4(1S5*+^k*!(p|XsBx=pz=gXyUn9WoR*`LzF6y3CfGo}ztRptnopWC
zBBee!P$t;oOS_UjmXk}8%Gq(88l^D}xlms{QGgX=6S34^h4&U*!jL&yDr&+J0vU^C
zEjmUbHCtFeZe-+9{_NF7FthJz;O49k6UcX|RcYj9j*6K0zCG^Z1Zt~$sG-lJA0nNu
zvHBF3TF2BBr3XcIuE*e0ViTPTF$=WSQM!mmixg|`*&@E1k&kBjEJ9i+P6p8VY*iz6
zBi*EQAOl|UTk`EJL1alih83yEAvpk4i^j(1lO$f?9*X}mv+4EfivO;rq379<-j>EK
z_cu!O#8UYHuF}V=kM at P;yLNQ8ifgm-iP7MC4p=o~qG(TrPWrNyxdP4M)rOMuFO+YF
zkvE35v~G{O`1z))CK{FSQ0YIweAMGk<MnU<Z*9k2F)kg;9iBc8W5NTjC$#}nEl=P5
zW7_qaeLayo?P{K=!^I8{AJ-iB4LY~2dku`M(5vkBw5xpAmez&5olew!*J5Lid2UAl
zGTPT8gK}vpUrFB+qk1IhLDuMOF+0WX+K?RuG|bmt8O!u{=02NoYv%p&-P5g%vOb(;
zwZw$*o$I)&$i=8fZp5#VTkIFdp4<?Bf(oM@%ngjt;a#Bt$T)fTscSHs(tLIO4CjKW
zXV;pcB9vl<6W)u*7Q!=?*XdT;!Gh at N_@0HJ_%cbn2if;12L<Su<yOS?=eoB{FHVm9
z^sg-TW;1pu^KL0g&HG`FjQ_0?^|Zf6pyd6{yT76<`wM1VH)E3(l^Pp8wt)bZgVZNG
zB8Bv+N%60qcp6|&sy{A3Ns^?~vTXOOA4G-(Qu$4(X(IQd(w0=CnP?17yMpX#R(aS!
zm>TJ|V`%AoD22;X2Hk$<5qN+!mD75G)VV<cL!3y=&$iC($eC$J{&>#t#zhCVAJIwT
zTI_jn{_%UVG at I(ECGQpV%l%2R4u>dI!%&JCR~Q1sTs>kVAJ^=dEphx*+Bq|DRS4!p
zr%50ll{5B!amr$`CMva;C&U9qD(@Nny#-$1KXPonFL8BZc=`tjEWsmZ*GaRaP{7p_
zx~Q>X^ZMncKQz3{nbp`kNT=Q0?uAS~F#9nA9(#927>J^%x;WF${v^|W9Uqu83k`j`
z8+`omgskBz_`;4Ey7BQgj at 0?8?IA;QL!*jqaGvxe(EVKIA7I3eeZD~4tNT0hT|@Xt
zV8;ub<vJH={Z0^WwV69aOY9QIfIi&v*GsqXPqV~t8nxHWdG|YJ%22F|nu?M{#B8e^
ze)@dl!~r8^1l^E54rBUtfZ>d|NB2~g%jeuCn3KmBdxbBCA2?%ZcLHR(F3L-!*)~!i
z_9_iKGh38kFrs&NW~{fk($HezH3wG3^I;aE!ZOEscayX(<6?!F|JI~Z?A at +QoH3i1
z at E{VA!TaL2{JyOp+V&xntL^<?elAW-{<TE1zR*k!4I+F8P?Z|YwbtI^8~as<6G-UV
zEx|Opl$ea&;|xfRS4g9KuVnUP*w*m$Bh)#qX+2R}tQRcyP&p^;b;Mll;{AjDh%H#E
ze#o#=<%@B$ekP|92o2XwMX_R?6_Vf!3%-^;EY!aXA`hh2aqc$Z7qt51fD(Oe#Mwae
z3F>piYGY`6$qe8G-e~bKPRFw0N$s6n*1ovrgMDq_aMr2LgnmsIZQAM7eew)Wmh`G2
z`aX%YITgHevKV6_c36+Jd-uxM`ZL1RE^Ae#={IFK-W!Ufaf-yK)Cjr^51ruA1;^%1
zSsQ60u<@0l#^M at -fQz4Oslq_3(Z%5(-DXs|58n`{?#dJINOH=qG$5h8LLyt)-9y$}
zxy;xm-%)jo?++}JYXg at 9BK9CBh;$EnjF(7gWP*Iyvp(J&!3DxK6>u?Eo%1E%##uHO
zeY!SOr!_B4uM_<C at hDx%*6OW0lE6Ja%kuwgNR at OR6Q(Dd?5ZTy&8h33wrd>uOh&9Z
zOToXGOvcE`zT8?+`)YVVz|TiZ?!R8NTydXnC2-zXqI*&9!af>j6l4Xe3OPl8=WqLY
zu77*!qoHB<eIPWqHTI3QGtkh`!Kvk<JPV#r2|uaj(0(k!e8jsdig_gyz!kqWn}`G>
zLtBzslZj%zZ>c|4GU`_gh>~A%#e#27 at 8^iiR#1cCn5$0XI`HvvzT-GEXKU=uq*3R$
zb48wmb*=CCmXd`XvF<K3UMRB~Icls5oGaJJe7DxhTx>l->tRa?o*HZac;v#>Vs(_9
zKDlD$xFw*dI3g3li?-{}VZb|#w^+fK+XpbTH_A0z9ZBox5KV*tYs6%03rprW$*4n-
zqp`xOU at n14T7AV{b%N%1L_ecLHx}~+H%HX`dmC!6VqA9~G8K<o893&Rc~nQHw7ywl
z7hiqp9(vi>u~Ks`+I%q}A?~BK(d_%7gZ9^smSeM0Y~C4%`BLJkHv_4r{H1Hprn&H*
zB28BDyi;AOc*l|0PP^`}?Dxm!j-K9D{G9)RPVfWEF;>g?9ldDN^WOo#w=b|ikYrX7
z>5<*dwl-jvuAqxxRgOz~%|e?HZw^TCcFQJ+uEXu at 1U@h&=a~ISIHi8Swi at 8N8hSJH
zosaoBRP4)7Zw7w<D7 at EG6<vg^hH_x+{jjUddnbq=e-zl)0F9PKlt{&1lCLld4CVI9
zY=3;~P8(xsJO4s=Y2nMl;^!3>UL>)SomPePfSTIN=MBP}sWsjXL7Qm?bkOmvA_g<S
zO2Utr=cJsjc;>7+#fj4Ghs^-n;$x<zH>Rbuv`z0mo*ZXrK(5~;v72!Om{)t=R(9+N
zmmqX&S5joQIBx2<MTQGk#HsM2VsvH!HUHe+IwCfQ{X|~QMPrBIb5_$m at 6jaN!pG!H
z#h?>QLetnAX>sq>y!WcBn?XRf33ZDTYHxgn<oz3eEjyb`b*#|v-|9$)G_=rAsYGu`
z&meU5V(3>pwNZL95`>u+n_uE--&_j8)$>a8ieHr%YrQOwfozuX${QEX;E`J}R0^F@
z6^}S|IG2fGOid<<+|7&2&JYgN37f0 at x76xb&b?XLJsM5p_aPFSD9o+rM(c&0mLqFF
zdx;H0iPUO{*ue+|4U_rXRHyd1#0JqV8We(XzpYl)as7hRw}r9?H@$S`8-sHdmOYD4
z>ZFqU(;5L8HESD}ig5DiYQ-DS4jU$M07JBGp)&bzH2e0~c-BFq8EP9&m<JN~94gPd
z>a0&7^4d<+E4;q6{h}i6fYOro$t{wu_hse<Fh_Yl=cS0)poWD$V&J(Q-XPvCd`d#c
zWTN~pWKbAk^GPjkTCLbTXOMt5+0T)?-6yHSk at T%PDw!sJz0l#xLm;(j<*z3i3mqZR
zN5-6Z_A_^#U(1U3QX~2?d<a(m3Ei1h^TZ;*I*HKnEHe!>ws^nFx%6S^%!sben(7jp
z0~Gve@)wQaXi>(R8Rf0_bH&@^VK0HZysl8{sws8of0WqNbXk9K6WJz@(O7Bg(@z57
z`+RfGCDn2It`4ek<?|`N$rtiG5Y79av0n0j6U6;Fzd|^v*gU_F<(~Z3H}bqa10R)s
zK`qBU+?8A<JTA}$?i4 at tA^YFHfI<|nr+x0ShOYelYl+0g3+cBhYD}GnG1vxb)W)K3
zJgsgSBm^GdUi6j&+hVkVYi|!G8_-j8cbJ{TIX^B9;bO+FA)i9(pO!~-A^SEtA0&ZU
zDIqF9S4{R>j(Bj*)t%O)hxQogDtejHtT{(@4VOkk1S{A>&LTuCT!8CXEJs4&wk!y)
zN;_Q8t&7 at e`KX#uVx3xNZrPTh(C5^j;zNdX_5H6wHC0^%tk=~kj%3=1j4h!(SOPG(
z^{FZ3*e}Doljjq%<j>liddR7$B3{qVgzJt{kuf7o^lf8v#)9qoeN6 at 6rY^ZSxytPo
z26<WC>Fdkt_3yAx{J1k<Qs0Ku4lqj|p-YkQ at EO4GzeeZ!(}Z+scq@$b6ys?$J_{o?
z4lkO&>`m}(Nr%OC7;(vn$xrObl#!5I`Z!-w%_-cnb?DchMY+S6q;ymmfZJr(0it$0
zm8-VEDFFiB<={VHTMpgy5FMJwH0B!eMC{W%2p~a|K)ZUG&$vU2$1mH{b_e_;VnYz`
zo#mGvFX=xeUJzQ`!OUt#GCG1j+M6gd&k=VczMsgS^}enMU#PAjyptFKpE0<Z%0Z%1
zI+HyMII8sPs`UKdiyYYwDc=k(fSg%Sa71V)d9q~j(CbQ1ZSr)*bNhhO(0W^4Y5w_o
zW3>n#;Ou1H^ztu(YZLWCnT8}Lav~EE^;#m;%-Xkat at ZvJP@RxE`$e9EKbMiH!C(O*
z(RBQ^Ue95?;I|%06^_FqP}o|b#DZ}$FcZL3bijZJvK^e5oh|Y(d$YUWV?3e=K$yE)
z>rUf8^o`FV>wMr`*J6ps<g-~&GV at Pr&*9LqC}L!ItiKXNA|TDwruseM4;z!hnZjll
zvZX<96UyWwQp!3JP`RmhKoU1upq+}XB9D$`g0og*C>$Y%muqnhSj`C%NSVOMN<4^}
zupMcsvE|(`p15o`U<-7q8 at s~O+DeNLkbPC1<;Nhk!IG5<jymN-Iyi_4S__<l8#V6n
z`1kdjo1WbcZo(`sWHf(`RF*~- at v4296>1PVtD&4 at aoK~{oZlNz)H4cEP7VRXlT3PJ
zjBb at Y+5^c4b~j4x;>L`f=H$k<;&*qRU<dScAWa43tUoW6u at kOdCiFGn${P!>ZqBGO
z&aq{E>t#0Z>zXlM;EkHD9NwJ@<f7w{1gFsUBPuvqMO)8yN}#?<5gU4~XQyo~lc@<=
zYRu>5NB!3cA&4X0rB%NJD*|VCPMqCvp4s$uGNAk8qztm0g%pATAe$<CdOlq~DSBl4
zqM)6W6NlT|g0GI at vK=<E3`evs1tO<E)F+l at 9TC-xQlOki$;oPC+Hy<lTtzayp4i3o
zJ)ViBq4@=Y9Q=n~D&V87cbOe%mi|{mB^GmO)S2Mf!V|4-#GDVCf-&lwt%*Z|N><Y*
zpbW&=L at 1@|EFtktq at YGR=I9?l%dlv{IKIM_CWRp3^Fnh7_Huma!-_&}uC3uu?Alp{
zS0CoqGjUW|6intwk_q4M`VHo4+=Yx4R&2paJ|uy7<6oE3*n>}S29ME4((*Ib)WSXs
zLu)1Sc~d;BD`oh&zTm^A0JwM`(sMEGv0lNA;3EoqIvsnM{LA;njBIOp93KNZMeQ3p
z?4wEXH`lgOT8|V5fYYA}Z47W0glMfl(q+bfm6zn$15v4Pf|ZXaB+b_R_1YNq`E0j#
zez{j+TC9fSk#i20YiI|c#Nw#igrZVD%QY>0%JihNva?oWWd3D)0lBOwh??x=FO=5O
zpym6lNDyS?yWaNG(7fzhKKo&OpIVfyxI~`!1$NoO`pLM}g8V3gg}&*vy-^udCn!9P
zlq`WIxy1AzKsWOqLiseJRb~kT*{*p<AB-y!;a4vor2;8}JetY9=;?Fy(&cMBz0ckX
z&Z~ao)p#aT#x(P5)uh-*LWwMym0HB+-VDGa*OMbGZ>8(#M8I)nY8h$^FTK&sX0Ce)
zsu1X^uKC|fJi8^a1gx(hNq=}2lm1qv_p(YeV02W72U0sFu?`yyvpWE=7sq7eMfAvn
z(n1smOYPnh4|FIskeJPotS*8746#+X56yRWPG<9BWIzx*edkuek7lr^r5xUE_{07;
z*suUOnYU>(l2v>i7jRP8#RfdBcESYEVLW{f?Bh_S|6QwnGrgkUNz|a`0cT%q-2m&;
zhg)cRa;<LUA*(LYlapuHl8`_Bu4W<7Ap at cFJq<X~rq+qtW90W`nR`{Z#FV}1O)7}v
zHa7TygQs$G(RESQS_bHn6XZAGs%DIclvs?|9}jKsgMeb`0=mF4>aDf5(wHPoX9e$b
z_v!nPb<Y705Fr%z<4<hHmec(Eqwx}b>euACrW+owOAAv6%wBwu;cfHP#_Lm)l5vsZ
z$KX`Ntfaaut0ceGC6FWB!L^e;C6A2E$Y9=dQJAgie%%?e);#)}BZ}EBYs!9sqTE+G
zCeQ<)+iE_yPb%%zYpx+~uZ|uenrva3q=`*8h8c-2HLu+N99{S^J7VopNTw at P{^N8Y
z$2>wqGnQZ1#0KWWnlHHa1n+&@vlWzVmp!0Et+MHo^Pk-pTM0F*{4X9|b-KSDZ(UUl
zE_nPfq;C}(O-XMdEhVR>rO{u5oP?%?=3vBX4*HEysVroSyU-9<rF)h%FVDa89b~Q|
zE`!b at lau||g)!aNlf2+bzXYLSxlRiqw8(1P7(z*pHfzN{X$Y<ogI)RcJh4+{kw|r0
zZee$y8IN!h7j>DAenv$+#bGS54d9l(R~`w=Wag)xcD}VSj_9o1rc1%8Pa<F+pY3**
zogHNXZaL~5^46<AV+iynu#|Ja3EN1{d?v|u>Vc+#p|y}^a-Gy|7AQSSo<`_RFqRty
zK9PAZ7RPdk^v;&NSK*5VijLr$^{=R`kVV{})8xnqXX25*R28}7yB}JN|B1a`GUfaW
ztM8!4*NOHMu(@&wDKw7%t-+Vky;HDVcG}hK|Ep3Zk0;wLP<#saJ_tlnb<5&s3-{{S
z8ynzqd{@HnHZzsQ0jtAn+r*-8|053ATyI2W&$dUZhGlbYkgV^*2W4c^Q6j8R;gP_~
ziQDaVdAUXXePKP$)U+YkmjLs9(1s8igLhsOWH60FW9_KKA-iI~t(!Y;_{*>h*NW^d
z<n>VJ{)^vf+<Uw`%EBX2ni(k*ev*@jL^o7z{08)E|JPIEeWl<XDIib?kZ0>#;x?+w
zv~_m69Q8xUp}A|8ES@;$_95_GzD+gB)nv;OY;TD(uJ$nP74GMD$knyv%?XkW88v+O
zb+^Y|0+iwmm1cW2bXI_Ex6&%i#%GUtJ7IEAW2pU2&HSh}=pR6ZIpGKcw!Tso8BcVc
z-=iyoym?C;gem+NL4S-lClGHK_*DOhMdtie?R8>w5EqV!UM6Z#sE{WfzTpZC`v)ko
zsEhZxoo$+)*Ro%A;~c*xuqeoLsXFn|_PVXNyr1rTWzTwct^PE%QP2~^vg7Jxe at F5U
z5FNemL1n2iJ*ka8S%la5V9U>UL at iqLha7>ZV0Q-V-xp&BQrpbT{^+u89GAhdyviYD
zk0(at1;V;QhO at pBhJS*abAL&(Gs1e|(6Fxhdt5rp5U#tJH5ng;t0>9v{&r4~SW~m~
zEEN%lxP7p$ZP-5QPor?nhy9V2GEnC7X+LsmdNb$X+p6^63X;Jq6&_0j_3|K}Gl;>^
z81_Ce?l08%?4m<;J|(=DloVY<`qY!0N}ihcjYQWZy**c_s74nIB-1zd{qVS&Cru9u
zzL~Dq->v-Ryi8U-GFQ$jvX!mUc14>`Jqk)=)C2)YyM=|vcvWPP#hPq3yGpsF=2{N8
z at 94Z`I~|@`V|7;3P0knR`#@L&pU!W+Tw&5rGhNeV9nKp?DboS at al%X7KaCt3)+B-u
zP$%0*7ZLhW;F{JIx2t2NfG5u^qm_+XbMseO4b727pl|kc(YO9pqBLKsHd5LTkYQMk
zw=^2=_?MJD)n+^LnD_#Zk<u at Ti%w;$<Ux>!0SP2Sg0*V;WWK{F;fwy`C26OGrO>{#
z;9;SS1077MU6~2q^f+AgZEj5q6RiJ)WY2sjXK6Mq+=VabKDQdZ`d~AzV0ketYYQeU
zVPW;!m-lPbWvx{in##>LI(8*lQcE0k3n@%q(1j<2q~#C!*<>{4(!Nz+GE8DkdtqnA
zsoA(OxJ*BgqmR~LnwY4 at N*+UT%exT$-O{POAdJCXLmbQ6|0SYuukwWol-<S5S>ir2
zEF3NP0>Va5mUU$RySdW;)H9taOsuH{XRp4FEJW&2g=xBljP*CQXh)_(XMz9uQ<`qK
z$E3p7TyJh=>JVj3#E0LXQEDIZ&Ry<h6i~fNU^Y{6om0NJa4=B`L{T$%l}KbWDf*g)
zf9?7_NlJ5M&&cR#<nZF6UbCMmRv&9-7gdKWQK?{L1I=L^<8K1If3c?<9nX at kkI0K;
zz0vhZmS&#T*3){@S;5QJIeS06>8nW&tc|6f6KD!6JQ49Md#I>Nu!YKvFyN})e>d95
zqk!#GfeC{oZpN?W$WERLuTVzkae*Kc;5_TMM3 at 4tU|`}bM1nO at sCOG#Q^M}}iY3pJ
z=p<{@p~ppbXMXV*Z(0f>mY-2A*N?uHXY)YI3}a=vxK^o^xtcZlq82_)POqw<byh+D
zxmieLTxtBbR*cjKo3}kl{{SR!!ot9T#Ln^H=I!r^7QQ?^x)a3-ao(klL^k4qmL1J9
zW<P!BH?cr00^8F$57E-)CsZzNzbm}Dv4_dsF_K!6t1l_PW>~hXAdO&*H}UC9LMM67
zI5%`fSr&s1S_|>xF*w7+t8UeQ-^@Fvp89CxB(!QQr&S}$D}=`gePYjlSrIPy^0YlI
zjRZmrrdw;iljb at k0%Teo)FJ23nOBk5MBNAZZOtMaQgs=OOS2F>v)K^<$TSV(f&I?p
zu!9 at LcVWQ_BqY+NE6Tk<04?Ga at EMidv+c&J95%_#B_jekz62u~5T`l*wz}=Jj8fZs
z|Fz$X6Ue0agiHmil$v^D+{MU1__;X6w at INg32QonpIa1OmNW6LZQa<$D+(>CyCeq)
z3Ekl}rXzz0z4;5tf*tzC)4mB*@Ykx93fffvHGqcl7K^Tg2q}eH58WngsKwiS-rkpN
zW~=Sic4XocABZfc`$m0am4ZYb(=-z-OMsqIBUEeP1|&>PVI+MyBje+=k{my)YvR-A
z#}4f9`)uo(J`i-Z2+ng)`@m!Fn0bWPW88GE<9U<Ojr6ZQIVXGq+(+bAe%r?zgiTrq
z{aFY at IM_&<l%190&e=?<t3M2;zaNpQ%kR;orl}fS*|3)D7!mz0#KC^J|NU#ctkYH6
zvXdZ9$hQRxPSaO7kcC8XY%sdC at I(1VXrkJN$t-?Kx2$J&sslcW)KWbkpC{81NFyn%
zoRg~e7aj${bUBK=Lg}MSaZZ46k$Rzt3AAcgE5qxHg2^p#>zCaZJ5}1JiAO+HwhbfM
zT3jzpo`TTM3Pt77jlPE1n*R6%HF)ju)tP99#D)xh(ly3mHZnT(D-NxvP=RFYf1?@}
z->4Pgk$BiH0b<)oi8^T`0iMl0$yFUo$|VN3SLLlii3XNzSWT3p0mp8oel1%ae<22X
zLaOrU3PxI1)`C<de4<3}QrR1>J;)IuQlbg_SfVn2w$JOpQt<q at am>?+UEk!4iX%p2
zpp=N<N0wCLK#}3^B|OvoLkZL+_7`e##g>Q*t{G!D|E$%szPZ;g-f$cH$ze$5ih>c>
z*>%a%pp9gX`X^<<mnBzvgyd<=3l3x4;fS|zsNz!WM&W|(#H#x++gY)K^@imLRu$#v
z!!p(8tC}Nm2LVU|oyBXN2}iin#!eW~K)IK??qlg1D>Lqwu=WPAvvp$NwjJEsn45ag
zv9m3t#fj|i*MERPm-q<@MdnlfxXJHTHV%sQtORYmoD6=``}I%slckPZx~?iUdRY|3
z%=!jJ1(?_?E80Mfme(~_q-k$$U1K-0%K3cz{w{z_5kX2NvGiOm33C+Aj at +%}p`n|$
z7eFPhg<LxF%!oC{kST(W4pY at Fd`A{-G;0S|X*p at g*k(RI5l0#fQO>)^!2uI9U=v=Q
z=Bri(Aag1{RD2}0HjIK`<+@)PzkKk|955T*x;7?@E5 at KW^3D<L09lcfD$=qmquS-#
zYOU1^AxU^5QaG$q=RJH-Dxm>Yuylh=yU2ce?l(730t`o;1w9d09B#C$SEG6`t7I1B
z#N#Km4iz~QUxPLLXss(M&ZpmM(p7GWHTskYzrP6ls#`Zz`670WPSyMbJzB``A)!UR
z?V2h^gmK9~$sIPagjwN7uo)Pvlj6z3--uBQoALRBYAgdju|E=6>QOCtR(R|0i~9#4
z>4{@3v`%EJSn23?PnV&DT6>|Yvvc|?>E6j*aV1z#4Y3Uy$p<-JET7Ws<Fo0~6kN{v
z)C^o&LNv(6czG2J=iXqUUoBqkWZEfBEimZ;jL%=b=@};LZ=e2qtCeEsX_aTNu<}t6
z??9t_)V{t-gmMO*&!SG*Lg?gND;f>Ta<@61O>V#b*0k+~qr2D~t8=nPnyHnwSbosW
ztkDtrFQR_{C9@$5CguzRnX>8lV;eX(qxJB`d9_-PFjM38SvZnA#xZTGPFM3RH*p&;
zY8aNBa@%eT at kCQ#r))DgMcY(E8qGXx%u!TI5;tvJR{lB>-NJOk4Ax%3bPz71^YRM+
z0o4^dDa at a&`Xwi}h)N$+ious;Rc_Du2T&BFFQlj-8Nof#N4|6oI at NTL2rAkq!aR{c
zlCraVJ5-rjv#|RR!!_?Yw8{7DywrDwTs5+bpl=%bB5?h!@)>g`zL7N?nm_10r_<<^
z&VP;-_|RXqJ~K(78LBejcobWk>{`oNKd$1U-7dQ7hmIMW5b=bY<vEX7 at AwYXn(1QC
zSa|*CNPf$%<tI?Quh_K1472vo`BWWZkc(k_F6i8 at F>iJ~jQ?(<grUNaEVsa&<pSW7
z<=4DtpgEst`7^=6y>;J at _b$Yko!%F?Of|^Svz`4${a7As249J at VWe4%U4yp_0i58y
z$5u5fWG59H2oIw(YjGWK9?G`I97_0sFWSx_Y0L-q_(5B(xz`<=BQ|(8sT5&ppdg(o
zG1u7D*}Tbolp3Lz*X1qVPu*9jHZ039u!T`kSPAZ!kO at 3`8s>*tcl`?M=RZK01|4A!
z^<mNX4<tWoJgmRxevP?j5xK{$3wfWoJ0Kz7Nb#$T#&-a5etd_x^E*)Sh%UYtPrW26
zzL_<D!+q5Z0uUwq*5A|diZsfz{fo77=FKulXK+g$`!lffi1IfUC<C38)Q93_<ehd>
ze`tuvr=r;pjqh6w$-QZ`=TpC1vN*fAhTcRM1`Ay3?c&XC;JU0eZyQn0S~x&d0ew<E
zXY28V?55$B&kNpJwCX`NLyh!*-<w30S{$b at 8l<Exzc!fYv!&D_f)iAJfM(K5Sm?CD
z-P+g`UUG|-yV2!C=RKTwY^RVizC<_Wq_*r&_40*Zns#aY5J_KBax`wj*@JJV{sC;y
zu59jokJu^R$Ur_mX0CUb7{qqr@<h_wt+*X>0#5eC#cdi5Md>~VBgMOLHScpP#?^4K
zkxLSq(wlX`tVdJ%MaZU{tCIOl4ggR+lC<_K<|Kg)QAcwB^jSvAa at B?C)X6Q0@?&ai
z#)X?(Xh-|f96iPlC`ygD5`SPTgxY~|x!=lK?c*qac8r#v3K#=kZkQuzV$Jhf=ujmg
zD-sL;=$RK^N|4}egtIJ98&4%J<;(`=vhWR!7mXYFnDfnZ3$zkmzdX46n@&jK)W}km
z5Ul(D#-U)>*(X!}6L0+G^!`n02vHiMYnkRhSYie_&4;gRTfVUsJsB?tmw~mD?KGA%
zqoC2q`SC$nu%dKgZpW}LQRK>i at bC|&W>FW9Pv$n_?<rTxR7oHDGq%|8HpKiDDcIZf
zEv1`<U+P2UhOa5fYnwhQd;Ki;W?>@~3j8lm;x)41`z}?5 at Rz?uwABNrQPTL<v9z@}
z)d%-%apcqNcN{x04~k!|wnLKvxDJ?%vbeph9~o0L#KqavU>BX*^Ns_&!&)-_0}_)(
z6f>QU1e+*2#*PGzwE0pwk6yv{*vDys=845%_Sm2Ba4(HDOqOSEw*Fk)CPRD0k at C$*
zQ(u8ECT16$<Tr5sH}m{}kuawxob!ALBZO1NI-3R1D4)O|tfI#)Zr7oeiK-J>xMud}
z$NjEkvzQWRgu;bKns*UQ*T4{7+KnN+NYLipl(uD0Q{E{oc+Nx(t5U{yBVT4)2Dolh
z<Qmf|+U}Q0mY%G-v#b^5x{1puDaAe{tf`)-_zDWt+QoX&ZH>Y}6WDLp_VQN*iiS#W
z{+{aG+eN&)M2;?)|A?}S7**{|6}m{K_3zEGG?dZGAv=KS_H_Le$sq(S_CEb*_imt}
zi0l+$f_U|xVU(6eB6^-4GiERf|Lz=SsMu~4#i%g+Or<il)Mc8StgbmE>XA)J<63=E
z>_P=(v$Q|5=!3 at F>7-d64&GJRI55_c^SUVP!9q={-t;$*;9~Wo2coJrTq2t1QtOIa
zEkITkdpA0M9xuD9edP{I);L;=T>o6_x36SCw!vlkGMd5+bYB<z1_z3a6Z+*gZsnvn
zuU~y_`LYB&w_#<@|DvKqA8|&l6mMBqxVDrse6cZB*~O*&v{>!HS5m at sF#*97Ui+Ot
zS-A?A5Q&V8q2XLI7SsGa at X5pM>m63x_(u^rZyz^b-}ptdA`3)p^T-M+B%$;Wq`4zx
zP{cXX62Hq}-)Lob*V3$AAXLfqOVlZzTzB)OZ;!kJr!!g4&X5kbAZ&k+101NeywofH
z`R&{eZ1h&q2&<)mvwKh!IgHvUvtbs^7~drL^s6K-m74}~CZS`7AZe)Oj4`&bA$qAk
z4@=d><<yLaPJ0jY<$fsNNlM`G4VW#5!371U)i;q2W|;0$`+ at Y6nIc)fS at QeDT9*mh
zf2#?upL1)}GK%8Ag%xj1XVQv0l;PiFE(@jEdkm`>;(9Jc)#!5xM;Hy3kxZ-oujg1z
zBSFqsV9tw8w|@Xwot3ohmURjNJR5GwI55?O(Iyrf;Lg>y7B}8?NKU+&_j$mr4LMDv
zm?_Q~;J at uQVBD8&>E%$A(2hseyFqJXp25ipg;(%sVfZBbuIxdiRe^HmkJj-bCDlrr
zifp5$oQ*!S7CeV3Zqpg-2+Z)hGov94DzH$&h=M at 3LUbmVux)!cGf5wK!=D^&Fq4kv
zQ9FKmBT4lAEU&}+<Zv8m<slnIJZMyJ-&DTZgBEMUpXqd1;+^JV0wafZYB{SaJHjHb
z at MGG0+ZWwm+V&1Y)8~HU?QdA($|SdrE)mJbvcz|(NC;ZBRv$NUy}U_LCnAsx58~<b
zEV!Tr(#JXE at E=9h_sa&I$#4yRy~})^L)jX4U9U+iG89=>!xL=+(_>9zmBwUcz;YKC
z9#1N-Db%d)O1|>>ZI%QcKKKlQH>H8WoD7^|cdoM;`c897vTqigoDqDMZ+$r}ST8s`
zu%Oq at oxxkt2c=~Oj at DBGDtm9*Ej+76P@}P_?E)0e<hV=42hlC~-Zo*TmdjSNGtEX)
zKaCd%_!sV(nMUdM5SHMc<*u at Z1Z!uq#w66GX4 at G?>~y3W3}L|hL*VNdl6f;l4~u at x
z&h=(a;Ez5{Ri38&s8Yl9qBfHz#O&X1->AUH)I>7wUbuKY>a+^|pikM{Nt^F&KJ%hp
zWTB%e2kq*4_iS#SYgZN>np%yzPGePv>QIji%lj4VV?;6c3M0|+vt!hja$=m4jqye`
zzjv)SZs#Q9KFxe=C-R)Hbyw%57 at zpm_yODCrGTIrClRwKsjz6IIw7axmDk=qhZI<t
zlxUYj?9Cv5M_SEm8`=~#*|ky29qVIm5zEWA`zEZG=UeNoI=+a4`QvsOowYaMlgS($
zbq$QL1djU4LjzA*p+oDR*C?<xBhEpPcR6<Gq^G}dNp5{3p=_+g`m%-_F(M7XGdR*r
za8yL*{8J{>ER&1%O^{4Fp)^B0NBtr6h2}p%D&_}1BDd-5zrY%F!E&O0yl;VKtb>r1
z{u0*4v3^kg$X7R_&m5D~y*}xjo?x|d5e!E;bqJ?e9<Y%40J at EScr~N<1 at e17<t(mK
zXZI{bYPiPC at IQ&mk&|`ldnS3w-P%X?iuM)F*cW2Ye-VV{@2Ou|nh;UySe?~*-`4Xz
z<%N;opIKCFfaqQZk>)(=MUTCAAcW40&;NlTL at X*ws=O1OizFzjQ40ZbQwFlrXGiBV
zuW8H(-kGx(5Nmt-cDcA-P+&EA2*t?lEmoO%C!S3grwuY#<m82v#ib)>6dfI9N;+Wf
zP5ammJkz3I at k>$@7!*F%=Fb+|F?Z=DK3{se{|7LuXj?8Z0vR82$c&sYU9i16E2 at 9U
zFTUP29O)Dao_Q_6D-n`+- at 43G)o5R7(*I^JGQha&TFO at a1S?N4I4L2ja)J5FCs!hK
z7eTN882*|@#fVqfi~283b*&5hxOVp1=jcx;si}-xdSToXUkJ5EJf%J^ek%4;koa8A
zJvp at Q{^Iw%<qzH>-OzX(vCBP3npt~!>b;28_zIl)W+oFp1q)scW18nr4l}J1TyZSj
z+~w8-m&v>l6e at W3Dgh$tY(%l at ZJkXNYk$!dJb+;q=MOc2a=Z9CPSCRX>SZ|##}wL2
zS&HkIjNFLYziKOYbF{$u<={tg@%u2AdoAZh_p+~=S%y|N!Xs*uuq;|yFJC#8$;K4-
zRF=98RV737PDUu+&>+a1JVx2WKVT}t;M0+R#t}rxj?~<<73wb*IoV`@VVKcj8Og8R
z at RH0a+h5W#n&i`@Pvn)e at zS(f)Tthl>z55Fjq18q at 0t{O at Wkzu7-!e^KDYhYr2X=*
z0g-&0cTt)XsqKawU|o;1lpQ_e7UiWR$0P at yGA<DWS5=)>+&2kui~N<hsH$r8gR8Ui
zV8kaDp`L-hTd!O9k>;6Vc*B^5;IihI&m(*(_KNMx+&o*XvrDX1P2Z at -ywov4=q?0r
z8yZ=pP~0e)wgEGCo0eD3j9xgZIK#~1(jd1=DG&K|zN%~@e`l_4V*`CtOWO62c%C^-
zEyLU}!-W>c=H>PlMqC`Cdiobb_e<ul3(8FsvD at 0x_JI<aC|{N#?~jJ%WOGzJmxjut
z@#h+(Un<bHBV^0|p5otLNVr#siYrAmiKl|VXYzin=oO<1pIc&lE9M2Y6#)f#PaG5-
zPU#Sq010;w>#r&LO%53p%2n+}q^o`PTUqiw%y4mkvd#aJy%q+8OD<0}--RJI at h<K!
z*tMoPfB3G)3X4q?Dh<D7J%+vlR^yRJk?2UVp!F2Wq#kAony_2M{(e48p_B&Q-plf_
zYiUjVLX!z+H!+z at W!H=T at ZMyi&;%FC(ON5#A`|{D#ceW(&6(VX9K0vr8~AVj{{V3J
B7;*pr

literal 0
HcmV?d00001

diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/sparqlcolors.css b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/sparqlcolors.css
new file mode 100644
index 0000000..78d8ae0
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/sparqlcolors.css
@@ -0,0 +1,39 @@
+.editbox {
+  margin: .4em;
+  padding: 0;
+  font-family: monospace;
+  font-size: 10pt;
+  color: black;
+}
+
+.editbox p {
+  margin: 0;
+}
+
+span.sp-keyword {
+  color: #708;
+}
+
+span.sp-prefixed {
+  color: #5d1;
+}
+
+span.sp-var {
+  color: #00c;
+}
+
+span.sp-comment {
+  color: #a70;
+}
+
+span.sp-literal {
+  color: #a22;
+}
+
+span.sp-uri {
+  color: #292;
+}
+
+span.sp-operator {
+  color: #088;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/xmlcolors.css b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/xmlcolors.css
new file mode 100644
index 0000000..aa26579
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/xmlcolors.css
@@ -0,0 +1,51 @@
+.editbox {
+  margin: .4em;
+  padding: 0;
+  font-family: monospace;
+  font-size: 10pt;
+  color: black;
+}
+
+.editbox p {
+  margin: 0;
+}
+
+span.xml-tagname {
+  color: #A0B;
+}
+
+span.xml-attribute {
+  color: #281;
+}
+
+span.xml-punctuation {
+  color: black;
+}
+
+span.xml-attname {
+  color: #00F;
+}
+
+span.xml-comment {
+  color: #A70;
+}
+
+span.xml-cdata {
+  color: #48A;
+}
+
+span.xml-processing {
+  color: #999;
+}
+
+span.xml-entity {
+  color: #A22;
+}
+
+span.xml-error {
+  color: #F00;
+}
+
+span.xml-text {
+  color: black;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/codemirror.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/codemirror.js
new file mode 100644
index 0000000..18d8bf7
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/codemirror.js
@@ -0,0 +1,219 @@
+/* CodeMirror main module
+ *
+ * Implements the CodeMirror constructor and prototype, which take care
+ * of initializing the editor frame, and providing the outside interface.
+ */
+
+// The CodeMirrorConfig object is used to specify a default
+// configuration. If you specify such an object before loading this
+// file, the values you put into it will override the defaults given
+// below. You can also assign to it after loading.
+var CodeMirrorConfig = window.CodeMirrorConfig || {};
+
+var CodeMirror = (function(){
+  function setDefaults(object, defaults) {
+    for (var option in defaults) {
+      if (!object.hasOwnProperty(option))
+        object[option] = defaults[option];
+    }
+  }
+  function forEach(array, action) {
+    for (var i = 0; i < array.length; i++)
+      action(array[i]);
+  }
+
+  // These default options can be overridden by passing a set of
+  // options to a specific CodeMirror constructor. See manual.html for
+  // their meaning.
+  setDefaults(CodeMirrorConfig, {
+    stylesheet: "",
+    path: "",
+    parserfile: [],
+    basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
+    linesPerPass: 15,
+    passDelay: 200,
+    continuousScanning: false,
+    saveFunction: null,
+    onChange: null,
+    undoDepth: 20,
+    undoDelay: 800,
+    disableSpellcheck: true,
+    textWrapping: true,
+    readOnly: false,
+    width: "100%",
+    height: "300px",
+    autoMatchParens: false,
+    parserConfig: null,
+    dumbTabs: false,
+    activeTokens: null,
+    cursorActivity: null
+  });
+
+  function CodeMirror(place, options) {
+    // Use passed options, if any, to override defaults.
+    this.options = options = options || {};
+    setDefaults(options, CodeMirrorConfig);
+
+    var frame = this.frame = document.createElement("IFRAME");
+    frame.src = "javascript:false;";
+    frame.style.border = "0";
+    frame.style.width = options.width;
+    frame.style.height = options.height;
+    // display: block occasionally suppresses some Firefox bugs, so we
+    // always add it, redundant as it sounds.
+    frame.style.display = "block";
+
+    if (place.appendChild)
+      place.appendChild(frame);
+    else
+      place(frame);
+
+    // Link back to this object, so that the editor can fetch options
+    // and add a reference to itself.
+    frame.CodeMirror = this;
+    this.win = frame.contentWindow;
+
+    if (typeof options.parserfile == "string")
+      options.parserfile = [options.parserfile];
+    if (typeof options.stylesheet == "string")
+      options.stylesheet = [options.stylesheet];
+
+    var html = ["<html><head>"];
+    forEach(options.stylesheet, function(file) {
+      html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
+    });
+    forEach(options.basefiles.concat(options.parserfile), function(file) {
+      html.push("<script type=\"text/javascript\" src=\"" + options.path + file + "\"></script>");
+    });
+    html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
+              (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
+
+    var doc = this.win.document;
+    doc.open();
+    doc.write(html.join(""));
+    doc.close();
+  }
+
+  CodeMirror.prototype = {
+    getCode: function() {return this.editor.getCode();},
+    setCode: function(code) {this.editor.importCode(code);},
+    selection: function() {return this.editor.selectedText();},
+    reindent: function() {this.editor.reindent();},
+
+    focus: function() {
+      this.win.focus();
+      if (this.editor.selectionSnapshot) // IE hack
+        this.win.select.selectCoords(this.win, this.editor.selectionSnapshot);
+    },
+    replaceSelection: function(text) {
+      this.focus();
+      this.editor.replaceSelection(text);
+      return true;
+    },
+    replaceChars: function(text, start, end) {
+      this.editor.replaceChars(text, start, end);
+    },
+    getSearchCursor: function(string, fromCursor) {
+      return this.editor.getSearchCursor(string, fromCursor);
+    },
+
+    cursorPosition: function(start) {
+      if (this.win.select.ie_selection) this.focus();
+      return this.editor.cursorPosition(start);
+    },
+    firstLine: function() {return this.editor.firstLine();},
+    lastLine: function() {return this.editor.lastLine();},
+    nextLine: function(line) {return this.editor.nextLine(line);},
+    prevLine: function(line) {return this.editor.prevLine(line);},
+    lineContent: function(line) {return this.editor.lineContent(line);},
+    setLineContent: function(line, content) {this.editor.setLineContent(line, content);},
+    insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);},
+    selectLines: function(startLine, startOffset, endLine, endOffset) {
+      this.win.focus();
+      this.editor.selectLines(startLine, startOffset, endLine, endOffset);
+    },
+    nthLine: function(n) {
+      var line = this.firstLine();
+      for (; n > 1 && line !== false; n--)
+        line = this.nextLine(line);
+      return line;
+    },
+    lineNumber: function(line) {
+      var num = 0;
+      while (line !== false) {
+        num++;
+        line = this.prevLine(line);
+      }
+      return num;
+    },
+
+    // Old number-based line interface
+    jumpToLine: function(n) {
+      this.selectLines(this.nthLine(n), 0);
+      this.win.focus();
+    },
+    currentLine: function() {
+      return this.lineNumber(this.cursorPosition().line);
+    }
+  };
+
+  CodeMirror.InvalidLineHandle = {toString: function(){return "CodeMirror.InvalidLineHandle";}};
+
+  CodeMirror.replace = function(element) {
+    if (typeof element == "string")
+      element = document.getElementById(element);
+    return function(newElement) {
+      element.parentNode.replaceChild(newElement, element);
+    };
+  };
+
+  CodeMirror.fromTextArea = function(area, options) {
+    if (typeof area == "string")
+      area = document.getElementById(area);
+
+    options = options || {};
+    if (area.style.width) options.width = area.style.width;
+    if (area.style.height) options.height = area.style.height;
+    if (options.content == null) options.content = area.value;
+
+    if (area.form) {
+      function updateField() {
+        area.value = mirror.getCode();
+      }
+      if (typeof area.form.addEventListener == "function")
+        area.form.addEventListener("submit", updateField, false);
+      else
+        area.form.attachEvent("onsubmit", updateField);
+    }
+
+    function insert(frame) {
+      if (area.nextSibling)
+        area.parentNode.insertBefore(frame, area.nextSibling);
+      else
+        area.parentNode.appendChild(frame);
+    }
+
+    area.style.display = "none";
+    var mirror = new CodeMirror(insert, options);
+    return mirror;
+  };
+
+  CodeMirror.isProbablySupported = function() {
+    // This is rather awful, but can be useful.
+    var match;
+    if (window.opera)
+      return Number(window.opera.version()) >= 9.52;
+    else if (/Apple Computers, Inc/.test(navigator.vendor) && (match = navigator.userAgent.match(/Version\/(\d+(?:\.\d+)?)\./)))
+      return Number(match[1]) >= 3;
+    else if (document.selection && window.ActiveXObject && (match = navigator.userAgent.match(/MSIE (\d+(?:\.\d*)?)\b/)))
+      return Number(match[1]) >= 6;
+    else if (match = navigator.userAgent.match(/gecko\/(\d{8})/i))
+      return Number(match[1]) >= 20050901;
+    else if (/Chrome\//.test(navigator.userAgent))
+      return true;
+    else
+      return null;
+  };
+
+  return CodeMirror;
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/editor.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/editor.js
new file mode 100644
index 0000000..b2a96db
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/editor.js
@@ -0,0 +1,1176 @@
+/* The Editor object manages the content of the editable frame. It
+ * catches events, colours nodes, and indents lines. This file also
+ * holds some functions for transforming arbitrary DOM structures into
+ * plain sequences of <span> and <br> elements
+ */
+
+var safeWhiteSpace, splitSpaces;
+function setWhiteSpaceModel(collapsing) {
+  safeWhiteSpace = collapsing ?
+    // Make sure a string does not contain two consecutive 'collapseable'
+    // whitespace characters.
+    function(n) {
+      var buffer = [], nb = true;
+      for (; n > 0; n--) {
+        buffer.push((nb || n == 1) ? nbsp : " ");
+        nb = !nb;
+      }
+      return buffer.join("");
+    } :
+    function(n) {
+      var buffer = [];
+      for (; n > 0; n--) buffer.push(" ");
+      return buffer.join("");
+    };
+  splitSpaces = collapsing ?
+    // Create a set of white-space characters that will not be collapsed
+    // by the browser, but will not break text-wrapping either.
+    function(string) {
+      if (string.charAt(0) == " ") string = nbsp + string.slice(1);
+      return string.replace(/[\t \u00a0]{2,}/g, function(s) {return safeWhiteSpace(s.length);});
+    } :
+    function(string) {return string;};
+}
+
+function makePartSpan(value, doc) {
+  var text = value;
+  if (value.nodeType == 3) text = value.nodeValue;
+  else value = doc.createTextNode(text);
+
+  var span = doc.createElement("SPAN");
+  span.isPart = true;
+  span.appendChild(value);
+  span.currentText = text;
+  return span;
+}
+
+var Editor = (function(){
+  // The HTML elements whose content should be suffixed by a newline
+  // when converting them to flat text.
+  var newlineElements = {"P": true, "DIV": true, "LI": true};
+
+  function asEditorLines(string) {
+    return splitSpaces(string.replace(/\t/g, "  ").replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
+  }
+
+  var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+
+  // Helper function for traverseDOM. Flattens an arbitrary DOM node
+  // into an array of textnodes and <br> tags.
+  function simplifyDOM(root) {
+    var doc = root.ownerDocument;
+    var result = [];
+    var leaving = false;
+
+    function simplifyNode(node) {
+      if (node.nodeType == 3) {
+        var text = node.nodeValue = splitSpaces(node.nodeValue.replace(/[\n\r]/g, ""));
+        if (text.length) leaving = false;
+        result.push(node);
+      }
+      else if (node.nodeName == "BR" && node.childNodes.length == 0) {
+        leaving = true;
+        result.push(node);
+      }
+      else {
+        forEach(node.childNodes, simplifyNode);
+        if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
+          leaving = true;
+          result.push(doc.createElement("BR"));
+        }
+      }
+    }
+
+    simplifyNode(root);
+    return result;
+  }
+
+  // Creates a MochiKit-style iterator that goes over a series of DOM
+  // nodes. The values it yields are strings, the textual content of
+  // the nodes. It makes sure that all nodes up to and including the
+  // one whose text is being yielded have been 'normalized' to be just
+  // <span> and <br> elements.
+  // See the story.html file for some short remarks about the use of
+  // continuation-passing style in this iterator.
+  function traverseDOM(start){
+    function yield(value, c){cc = c; return value;}
+    function push(fun, arg, c){return function(){return fun(arg, c);};}
+    function stop(){cc = stop; throw StopIteration;};
+    var cc = push(scanNode, start, stop);
+    var owner = start.ownerDocument;
+    var nodeQueue = [];
+
+    // Create a function that can be used to insert nodes after the
+    // one given as argument.
+    function pointAt(node){
+      var parent = node.parentNode;
+      var next = node.nextSibling;
+      return function(newnode) {
+        parent.insertBefore(newnode, next);
+      };
+    }
+    var point = null;
+
+    // Insert a normalized node at the current point. If it is a text
+    // node, wrap it in a <span>, and give that span a currentText
+    // property -- this is used to cache the nodeValue, because
+    // directly accessing nodeValue is horribly slow on some browsers.
+    // The dirty property is used by the highlighter to determine
+    // which parts of the document have to be re-highlighted.
+    function insertPart(part){
+      var text = "\n";
+      if (part.nodeType == 3) {
+        select.snapshotChanged();
+        part = makePartSpan(part, owner);
+        text = part.currentText;
+      }
+      part.dirty = true;
+      nodeQueue.push(part);
+      point(part);
+      return text;
+    }
+
+    // Extract the text and newlines from a DOM node, insert them into
+    // the document, and yield the textual content. Used to replace
+    // non-normalized nodes.
+    function writeNode(node, c){
+      var toYield = [];
+      forEach(simplifyDOM(node), function(part) {
+        toYield.push(insertPart(part));
+      });
+      return yield(toYield.join(""), c);
+    }
+
+    // Check whether a node is a normalized <span> element.
+    function partNode(node){
+      if (node.nodeName == "SPAN" && node.childNodes.length == 1 && node.firstChild.nodeType == 3 && node.isPart) {
+        node.currentText = node.firstChild.nodeValue;
+        return !/[\n\t\r]/.test(node.currentText);
+      }
+      return false;
+    }
+
+    // Handle a node. Add its successor to the continuation if there
+    // is one, find out whether the node is normalized. If it is,
+    // yield its content, otherwise, normalize it (writeNode will take
+    // care of yielding).
+    function scanNode(node, c){
+      if (node.nextSibling)
+        c = push(scanNode, node.nextSibling, c);
+
+      if (partNode(node)){
+        nodeQueue.push(node);
+        return yield(node.currentText, c);
+      }
+      else if (node.nodeName == "BR") {
+        nodeQueue.push(node);
+        return yield("\n", c);
+      }
+      else {
+        point = pointAt(node);
+        removeElement(node);
+        return writeNode(node, c);
+      }
+    }
+
+    // MochiKit iterators are objects with a next function that
+    // returns the next value or throws StopIteration when there are
+    // no more values.
+    return {next: function(){return cc();}, nodes: nodeQueue};
+  }
+
+  // Determine the text size of a processed node.
+  function nodeSize(node) {
+    if (node.nodeName == "BR")
+      return 1;
+    else
+      return node.currentText.length;
+  }
+
+  // Search backwards through the top-level nodes until the next BR or
+  // the start of the frame.
+  function startOfLine(node) {
+    while (node && node.nodeName != "BR") node = node.previousSibling;
+    return node;
+  }
+  function endOfLine(node, container) {
+    if (!node) node = container.firstChild;
+    while (node && node.nodeName != "BR") node = node.nextSibling;
+    return node;
+  }
+
+  function cleanText(text) {
+    return text.replace(/\u00a0/g, " ");
+  }
+
+  // Client interface for searching the content of the editor. Create
+  // these by calling CodeMirror.getSearchCursor. To use, call
+  // findNext on the resulting object -- this returns a boolean
+  // indicating whether anything was found, and can be called again to
+  // skip to the next find. Use the select and replace methods to
+  // actually do something with the found locations.
+  function SearchCursor(editor, string, fromCursor) {
+    this.editor = editor;
+    this.history = editor.history;
+    this.history.commit();
+
+    // Are we currently at an occurrence of the search string?
+    this.atOccurrence = false;
+    // The object stores a set of nodes coming after its current
+    // position, so that when the current point is taken out of the
+    // DOM tree, we can still try to continue.
+    this.fallbackSize = 15;
+    var cursor;
+    // Start from the cursor when specified and a cursor can be found.
+    if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
+      this.line = cursor.node;
+      this.offset = cursor.offset;
+    }
+    else {
+      this.line = null;
+      this.offset = 0;
+    }
+    this.valid = !!string;
+
+    // Create a matcher function based on the kind of string we have.
+    var target = string.split("\n"), self = this;;
+    this.matches = (target.length == 1) ?
+      // For one-line strings, searching can be done simply by calling
+      // indexOf on the current line.
+      function() {
+        var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
+        if (match > -1)
+          return {from: {node: self.line, offset: self.offset + match},
+                  to: {node: self.line, offset: self.offset + match + string.length}};
+      } :
+      // Multi-line strings require internal iteration over lines, and
+      // some clunky checks to make sure the first match ends at the
+      // end of the line and the last match starts at the start.
+      function() {
+        var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
+        var match = firstLine.lastIndexOf(target[0]);
+        if (match == -1 || match != firstLine.length - target[0].length)
+          return false;
+        var startOffset = self.offset + match;
+
+        var line = self.history.nodeAfter(self.line);
+        for (var i = 1; i < target.length - 1; i++) {
+          if (cleanText(self.history.textAfter(line)) != target[i])
+            return false;
+          line = self.history.nodeAfter(line);
+        }
+
+        if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
+          return false;
+
+        return {from: {node: self.line, offset: startOffset},
+                to: {node: line, offset: target[target.length - 1].length}};
+      };
+  }
+
+  SearchCursor.prototype = {
+    findNext: function() {
+      if (!this.valid) return false;
+      this.atOccurrence = false;
+      var self = this;
+
+      // Go back to the start of the document if the current line is
+      // no longer in the DOM tree.
+      if (this.line && !this.line.parentNode) {
+        this.line = null;
+        this.offset = 0;
+      }
+
+      // Set the cursor's position one character after the given
+      // position.
+      function saveAfter(pos) {
+        if (self.history.textAfter(pos.node).length < pos.offset) {
+          self.line = pos.node;
+          self.offset = pos.offset + 1;
+        }
+        else {
+          self.line = self.history.nodeAfter(pos.node);
+          self.offset = 0;
+        }
+      }
+
+      while (true) {
+        var match = this.matches();
+        // Found the search string.
+        if (match) {
+          this.atOccurrence = match;
+          saveAfter(match.from);
+          return true;
+        }
+        this.line = this.history.nodeAfter(this.line);
+        this.offset = 0;
+        // End of document.
+        if (!this.line) {
+          this.valid = false;
+          return false;
+        }
+      }
+    },
+
+    select: function() {
+      if (this.atOccurrence) {
+        select.setCursorPos(this.editor.container, this.atOccurrence.from, this.atOccurrence.to);
+        select.scrollToCursor(this.editor.container);
+      }
+    },
+
+    replace: function(string) {
+      if (this.atOccurrence) {
+        var end = this.editor.replaceRange(this.atOccurrence.from, this.atOccurrence.to, string);
+        this.line = end.node;
+        this.offset = end.offset;
+        this.atOccurrence = false;
+      }
+    }
+  };
+
+  // The Editor object is the main inside-the-iframe interface.
+  function Editor(options) {
+    this.options = options;
+    this.parent = parent;
+    this.doc = document;
+    this.container = this.doc.body;
+    this.win = window;
+    this.history = new History(this.container, options.undoDepth, options.undoDelay,
+                               this, options.onChange);
+    var self = this;
+
+    if (!Editor.Parser)
+      throw "No parser loaded.";
+    if (options.parserConfig && Editor.Parser.configure)
+      Editor.Parser.configure(options.parserConfig);
+
+    if (!options.textWrapping)
+      this.container.style.whiteSpace = "pre";
+    setWhiteSpaceModel(options.textWrapping);
+
+    if (!options.readOnly)
+      select.setCursorPos(this.container, {node: null, offset: 0});
+
+    this.dirty = [];
+    if (options.content)
+      this.importCode(options.content);
+    else // FF acts weird when the editable document is completely empty
+      this.container.appendChild(this.doc.createElement("BR"));
+
+    if (!options.readOnly) {
+      if (options.continuousScanning !== false) {
+        this.scanner = this.documentScanner(options.linesPerPass);
+        this.delayScanning();
+      }
+
+      function setEditable() {
+        // In IE, designMode frames can not run any scripts, so we use
+        // contentEditable instead.
+        if (document.body.contentEditable != undefined && /MSIE/.test(navigator.userAgent))
+          document.body.contentEditable = "true";
+        else
+          document.designMode = "on";
+      }
+
+      // If setting the frame editable fails, try again when the user
+      // focus it (happens when the frame is not visible on
+      // initialisation, in Firefox).
+      try {
+        setEditable();
+      }
+      catch(e) {
+        var focusEvent = addEventHandler(document, "focus", function() {
+          focusEvent();
+          setEditable();
+        }, true);
+      }
+
+      addEventHandler(document, "keydown", method(this, "keyDown"));
+      addEventHandler(document, "keypress", method(this, "keyPress"));
+      addEventHandler(document, "keyup", method(this, "keyUp"));
+
+      function cursorActivity() {self.cursorActivity(false);}
+      addEventHandler(document.body, "paste", cursorActivity);
+      addEventHandler(document.body, "cut", cursorActivity);
+      addEventHandler(document.body, "mouseup", cursorActivity);
+
+      if (this.options.autoMatchParens)
+        addEventHandler(document.body, "click", method(this, "scheduleParenBlink"));
+    }
+  }
+
+  function isSafeKey(code) {
+    return (code >= 16 && code <= 18) || // shift, control, alt
+           (code >= 33 && code <= 40); // arrows, home, end
+  }
+
+  Editor.prototype = {
+    // Import a piece of code into the editor.
+    importCode: function(code) {
+      this.history.push(null, null, asEditorLines(code));
+      this.history.reset();
+    },
+
+    // Extract the code from the editor.
+    getCode: function() {
+      if (!this.container.firstChild)
+        return "";
+
+      var accum = [];
+      select.markSelection(this.win);
+      forEach(traverseDOM(this.container.firstChild), method(accum, "push"));
+      select.selectMarked();
+      return cleanText(accum.join(""));
+    },
+
+    checkLine: function(node) {
+      if (node === false || !(node == null || node.parentNode == this.container))
+        throw parent.CodeMirror.InvalidLineHandle;
+    },
+
+    cursorPosition: function(start) {
+      if (start == null) start = true;
+      var pos = select.cursorPos(this.container, start);
+      if (pos) return {line: pos.node, character: pos.offset};
+      else return {line: null, character: 0};
+    },
+
+    firstLine: function() {
+      return null;
+    },
+
+    lastLine: function() {
+      if (this.container.lastChild) return startOfLine(this.container.lastChild);
+      else return null;
+    },
+
+    nextLine: function(line) {
+      this.checkLine(line);
+      var end = endOfLine(line ? line.nextSibling : this.container.firstChild, this.container);
+      return end || false;
+    },
+
+    prevLine: function(line) {
+      this.checkLine(line);
+      if (line == null) return false;
+      return startOfLine(line.previousSibling);
+    },
+
+    selectLines: function(startLine, startOffset, endLine, endOffset) {
+      this.checkLine(startLine);
+      var start = {node: startLine, offset: startOffset}, end = null;
+      if (endOffset !== undefined) {
+        this.checkLine(endLine);
+        end = {node: endLine, offset: endOffset};
+      }
+      select.setCursorPos(this.container, start, end);
+    },
+
+    lineContent: function(line) {
+      this.checkLine(line);
+      var accum = [];
+      for (line = line ? line.nextSibling : this.container.firstChild;
+           line && line.nodeName != "BR"; line = line.nextSibling)
+        accum.push(line.innerText || line.textContent || line.nodeValue || "");
+      return cleanText(accum.join(""));
+    },
+
+    setLineContent: function(line, content) {
+      this.history.commit();
+      this.replaceRange({node: line, offset: 0},
+                        {node: line, offset: this.history.textAfter(line).length},
+                        content);
+      this.addDirtyNode(line);
+      this.scheduleHighlight();
+    },
+
+    insertIntoLine: function(line, position, content) {
+      var before = null;
+      if (position == "end") {
+        before = endOfLine(line ? line.nextSibling : this.container.firstChild, this.container);
+      }
+      else {
+        for (var cur = line ? line.nextSibling : this.container.firstChild; cur; cur = cur.nextSibling) {
+          if (position == 0) {
+            before = cur;
+            break;
+          }
+          var text = (cur.innerText || cur.textContent || cur.nodeValue || "");
+          if (text.length > position) {
+            before = cur.nextSibling;
+            content = text.slice(0, position) + content + text.slice(position);
+            removeElement(cur);
+            break;
+          }
+          position -= text.length;
+        }
+      }
+
+      var lines = asEditorLines(content), doc = this.container.ownerDocument;
+      for (var i = 0; i < lines.length; i++) {
+        if (i > 0) this.container.insertBefore(doc.createElement("BR"), before);
+        this.container.insertBefore(makePartSpan(lines[i], doc), before);
+      }
+      this.addDirtyNode(line);
+      this.scheduleHighlight();
+    },
+
+    // Retrieve the selected text.
+    selectedText: function() {
+      var h = this.history;
+      h.commit();
+
+      var start = select.cursorPos(this.container, true),
+          end = select.cursorPos(this.container, false);
+      if (!start || !end) return "";
+
+      if (start.node == end.node)
+        return h.textAfter(start.node).slice(start.offset, end.offset);
+
+      var text = [h.textAfter(start.node).slice(start.offset)];
+      for (pos = h.nodeAfter(start.node); pos != end.node; pos = h.nodeAfter(pos))
+        text.push(h.textAfter(pos));
+      text.push(h.textAfter(end.node).slice(0, end.offset));
+      return cleanText(text.join("\n"));
+    },
+
+    // Replace the selection with another piece of text.
+    replaceSelection: function(text) {
+      this.history.commit();
+      var start = select.cursorPos(this.container, true),
+          end = select.cursorPos(this.container, false);
+      if (!start || !end) return;
+
+      end = this.replaceRange(start, end, text);
+      select.setCursorPos(this.container, start, end);
+    },
+
+    replaceRange: function(from, to, text) {
+      var lines = asEditorLines(text);
+      lines[0] = this.history.textAfter(from.node).slice(0, from.offset) + lines[0];
+      var lastLine = lines[lines.length - 1];
+      lines[lines.length - 1] = lastLine + this.history.textAfter(to.node).slice(to.offset);
+      var end = this.history.nodeAfter(to.node);
+      this.history.push(from.node, end, lines);
+      return {node: this.history.nodeBefore(end),
+              offset: lastLine.length};
+    },
+
+    getSearchCursor: function(string, fromCursor) {
+      return new SearchCursor(this, string, fromCursor);
+    },
+
+    // Re-indent the whole buffer
+    reindent: function() {
+      if (this.container.firstChild)
+        this.indentRegion(null, this.container.lastChild);
+    },
+
+    // Intercept enter and tab, and assign their new functions.
+    keyDown: function(event) {
+      // Don't scan when the user is typing.
+      this.delayScanning();
+      // Schedule a paren-highlight event, if configured.
+      if (this.options.autoMatchParens)
+        this.scheduleParenBlink();
+
+      if (event.keyCode == 13) { // enter
+        if (event.ctrlKey) {
+          this.reparseBuffer();
+        }
+        else {
+          select.insertNewlineAtCursor(this.win);
+          this.indentAtCursor();
+          select.scrollToCursor(this.container);
+        }
+        event.stop();
+      }
+      else if (event.keyCode == 9) { // tab
+        this.handleTab(!event.ctrlKey && !event.shiftKey);
+        event.stop();
+      }
+      else if (event.ctrlKey || event.metaKey) {
+        if (event.keyCode == 90 || event.keyCode == 8) { // Z, backspace
+          this.history.undo();
+          event.stop();
+        }
+        else if (event.keyCode == 89) { // Y
+          this.history.redo();
+          event.stop();
+        }
+        else if (event.keyCode == 83 && this.options.saveFunction) { // S
+          this.options.saveFunction();
+          event.stop();
+        }
+      }
+    },
+
+    // Check for characters that should re-indent the current line,
+    // and prevent Opera from handling enter and tab anyway.
+    keyPress: function(event) {
+      var electric = Editor.Parser.electricChars;
+      // Hack for Opera, and Firefox on OS X, in which stopping a
+      // keydown event does not prevent the associated keypress event
+      // from happening, so we have to cancel enter and tab again
+      // here.
+      if (event.code == 13 || event.code == 9)
+        event.stop();
+      else if ((event.character == "[" || event.character == "]") && event.ctrlKey)
+        event.stop(), this.blinkParens();
+      else if (electric && electric.indexOf(event.character) != -1)
+        this.parent.setTimeout(method(this, "indentAtCursor"), 0);
+    },
+
+    // Mark the node at the cursor dirty when a non-safe key is
+    // released.
+    keyUp: function(event) {
+      this.cursorActivity(isSafeKey(event.keyCode));
+    },
+
+    // Indent the line following a given <br>, or null for the first
+    // line. If given a <br> element, this must have been highlighted
+    // so that it has an indentation method. Returns the whitespace
+    // element that has been modified or created (if any).
+    indentLineAfter: function(start, direction) {
+      // whiteSpace is the whitespace span at the start of the line,
+      // or null if there is no such node.
+      var whiteSpace = start ? start.nextSibling : this.container.firstChild;
+      if (whiteSpace && !hasClass(whiteSpace, "whitespace"))
+        whiteSpace = null;
+
+      // Sometimes the start of the line can influence the correct
+      // indentation, so we retrieve it.
+      var firstText = whiteSpace ? whiteSpace.nextSibling : (start ? start.nextSibling : this.container.firstChild);
+      var nextChars = (start && firstText && firstText.currentText) ? firstText.currentText : "";
+
+      // Ask the lexical context for the correct indentation, and
+      // compute how much this differs from the current indentation.
+      var newIndent = 0, curIndent = whiteSpace ? whiteSpace.currentText.length : 0;
+      if (start) newIndent = start.indentation(nextChars, curIndent, direction);
+      else if (Editor.Parser.firstIndentation) newIndent = Editor.Parser.firstIndentation(nextChars, curIndent, direction);
+      var indentDiff = newIndent - curIndent;
+
+      // If there is too much, this is just a matter of shrinking a span.
+      if (indentDiff < 0) {
+        if (newIndent == 0) {
+          if (firstText) select.snapshotMove(whiteSpace.firstChild, firstText.firstChild, 0);
+          removeElement(whiteSpace);
+          whiteSpace = null;
+        }
+        else {
+          select.snapshotMove(whiteSpace.firstChild, whiteSpace.firstChild, indentDiff, true);
+          whiteSpace.currentText = safeWhiteSpace(newIndent);
+          whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
+        }
+      }
+      // Not enough...
+      else if (indentDiff > 0) {
+        // If there is whitespace, we grow it.
+        if (whiteSpace) {
+          whiteSpace.currentText = safeWhiteSpace(newIndent);
+          whiteSpace.firstChild.nodeValue = whiteSpace.currentText;
+        }
+        // Otherwise, we have to add a new whitespace node.
+        else {
+          whiteSpace = makePartSpan(safeWhiteSpace(newIndent), this.doc);
+          whiteSpace.className = "whitespace";
+          if (start) insertAfter(whiteSpace, start);
+          else this.container.insertBefore(whiteSpace, this.container.firstChild);
+        }
+        if (firstText) select.snapshotMove(firstText.firstChild, whiteSpace.firstChild, curIndent, false, true);
+      }
+      if (indentDiff != 0) this.addDirtyNode(start);
+      return whiteSpace;
+    },
+
+    // Re-highlight the selected part of the document.
+    highlightAtCursor: function() {
+      var pos = select.selectionTopNode(this.container, true);
+      var to = select.selectionTopNode(this.container, false);
+      if (pos === false || !to) return;
+      // Skip one node ahead to make sure the cursor itself is
+      // *inside* a highlighted line.
+      if (to.nextSibling) to = to.nextSibling;
+
+      select.markSelection(this.win);
+      var toIsText = to.nodeType == 3;
+      if (!toIsText) to.dirty = true;
+
+      // Highlight lines as long as to is in the document and dirty.
+      while (to.parentNode == this.container && (toIsText || to.dirty)) {
+        var result = this.highlight(pos, 1, true);
+        if (result) pos = result.node;
+        if (!result || result.left) break;
+      }
+      select.selectMarked();
+    },
+
+    // When tab is pressed with text selected, the whole selection is
+    // re-indented, when nothing is selected, the line with the cursor
+    // is re-indented.
+    handleTab: function(direction) {
+      if (this.options.dumbTabs) {
+        select.insertTabAtCursor(this.win);
+      }
+      else if (!select.somethingSelected(this.win)) {
+        this.indentAtCursor(direction);
+      }
+      else {
+        var start = select.selectionTopNode(this.container, true),
+            end = select.selectionTopNode(this.container, false);
+        if (start === false || end === false) return;
+        this.indentRegion(start, end, direction);
+      }
+    },
+
+    // Delay (or initiate) the next paren blink event.
+    scheduleParenBlink: function() {
+      if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
+      this.parenEvent = this.parent.setTimeout(method(this, "blinkParens"), 300);
+    },
+
+    isNearParsedNode: function(node) {
+      var distance = 0;
+      while (node && (!node.parserFromHere || node.dirty)) {
+        distance += (node.textContent || node.innerText || "-").length;
+        if (distance > 800) return false;
+        node = node.previousSibling;
+      }
+      return true;
+    },
+
+    // Take the token before the cursor. If it contains a character in
+    // '()[]{}', search for the matching paren/brace/bracket, and
+    // highlight them in green for a moment, or red if no proper match
+    // was found.
+    blinkParens: function() {
+      // Clear the event property.
+      if (this.parenEvent) this.parent.clearTimeout(this.parenEvent);
+      this.parenEvent = null;
+
+      // Extract a 'paren' from a piece of text.
+      function paren(node) {
+        if (node.currentText) {
+          var match = node.currentText.match(/^[\s\u00a0]*([\(\)\[\]{}])[\s\u00a0]*$/);
+          return match && match[1];
+        }
+      }
+      // Determine the direction a paren is facing.
+      function forward(ch) {
+        return /[\(\[\{]/.test(ch);
+      }
+
+      var ch, self = this, cursor = select.selectionTopNode(this.container, true);
+      if (!cursor || !this.isNearParsedNode(cursor)) return;
+      this.highlightAtCursor();
+      cursor = select.selectionTopNode(this.container, true);
+      if (!cursor || !(ch = paren(cursor))) return;
+      // We only look for tokens with the same className.
+      var className = cursor.className, dir = forward(ch), match = matching[ch];
+
+      // Since parts of the document might not have been properly
+      // highlighted, and it is hard to know in advance which part we
+      // have to scan, we just try, and when we find dirty nodes we
+      // abort, parse them, and re-try.
+      function tryFindMatch() {
+        var stack = [], ch, ok = true;;
+        for (var runner = cursor; runner; runner = dir ? runner.nextSibling : runner.previousSibling) {
+          if (runner.className == className && runner.nodeName == "SPAN" && (ch = paren(runner))) {
+            if (forward(ch) == dir)
+              stack.push(ch);
+            else if (!stack.length)
+              ok = false;
+            else if (stack.pop() != matching[ch])
+              ok = false;
+            if (!stack.length) break;
+          }
+          else if (runner.dirty || runner.nodeName != "SPAN" && runner.nodeName != "BR") {
+            return {node: runner, status: "dirty"};
+          }
+        }
+        return {node: runner, status: runner && ok};
+      }
+      // Temporarily give the relevant nodes a colour.
+      function blink(node, ok) {
+        node.style.fontWeight = "bold";
+        node.style.color = ok ? "#8F8" : "#F88";
+        self.parent.setTimeout(function() {node.style.fontWeight = ""; node.style.color = "";}, 500);
+      }
+
+      while (true) {
+        var found = tryFindMatch();
+        if (found.status == "dirty") {
+          this.highlight(found.node, 1);
+          // Needed because in some corner cases a highlight does not
+          // reach a node.
+          found.node.dirty = false;
+          continue;
+        }
+        else {
+          blink(cursor, found.status);
+          if (found.node) blink(found.node, found.status);
+          break;
+        }
+      }
+    },
+
+    // Adjust the amount of whitespace at the start of the line that
+    // the cursor is on so that it is indented properly.
+    indentAtCursor: function(direction) {
+      if (!this.container.firstChild) return;
+      // The line has to have up-to-date lexical information, so we
+      // highlight it first.
+      this.highlightAtCursor();
+      var cursor = select.selectionTopNode(this.container, false);
+      // If we couldn't determine the place of the cursor,
+      // there's nothing to indent.
+      if (cursor === false)
+        return;
+      var lineStart = startOfLine(cursor);
+      var whiteSpace = this.indentLineAfter(lineStart, direction);
+      if (cursor == lineStart && whiteSpace)
+          cursor = whiteSpace;
+      // This means the indentation has probably messed up the cursor.
+      if (cursor == whiteSpace)
+        select.focusAfterNode(cursor, this.container);
+    },
+
+    // Indent all lines whose start falls inside of the current
+    // selection.
+    indentRegion: function(current, end, direction) {
+      select.markSelection(this.win);
+      current = startOfLine(current);
+      end = endOfLine(end, this.container);
+
+      do {
+        this.highlight(current);
+        var hl = this.highlight(current, 1);
+        this.indentLineAfter(current, direction);
+        current = hl ? hl.node : null;
+      } while (current != end);
+      select.selectMarked();
+    },
+
+    // Find the node that the cursor is in, mark it as dirty, and make
+    // sure a highlight pass is scheduled.
+    cursorActivity: function(safe) {
+      if (internetExplorer) {
+        this.container.createTextRange().execCommand("unlink");
+        this.selectionSnapshot = select.selectionCoords(this.win);
+      }
+
+      var activity = this.options.cursorActivity;
+      if (!safe || activity) {
+        var cursor = select.selectionTopNode(this.container, false);
+        if (cursor === false || !this.container.firstChild) return;
+        cursor = cursor || this.container.firstChild;
+        if (activity) activity(cursor);
+        if (!safe) {
+          this.scheduleHighlight();
+          this.addDirtyNode(cursor);
+        }
+      }
+    },
+
+    reparseBuffer: function() {
+      forEach(this.container.childNodes, function(node) {node.dirty = true;});
+      if (this.container.firstChild)
+        this.addDirtyNode(this.container.firstChild);
+    },
+
+    // Add a node to the set of dirty nodes, if it isn't already in
+    // there.
+    addDirtyNode: function(node) {
+      node = node || this.container.firstChild;
+      if (!node) return;
+
+      for (var i = 0; i < this.dirty.length; i++)
+        if (this.dirty[i] == node) return;
+
+      if (node.nodeType != 3)
+        node.dirty = true;
+      this.dirty.push(node);
+    },
+
+    // Cause a highlight pass to happen in options.passDelay
+    // milliseconds. Clear the existing timeout, if one exists. This
+    // way, the passes do not happen while the user is typing, and
+    // should as unobtrusive as possible.
+    scheduleHighlight: function() {
+      // Timeouts are routed through the parent window, because on
+      // some browsers designMode windows do not fire timeouts.
+      var self = this;
+      this.parent.clearTimeout(this.highlightTimeout);
+      this.highlightTimeout = this.parent.setTimeout(function(){self.highlightDirty();}, this.options.passDelay);
+    },
+
+    // Fetch one dirty node, and remove it from the dirty set.
+    getDirtyNode: function() {
+      while (this.dirty.length > 0) {
+        var found = this.dirty.pop();
+        // IE8 sometimes throws an unexplainable 'invalid argument'
+        // exception for found.parentNode
+        try {
+          // If the node has been coloured in the meantime, or is no
+          // longer in the document, it should not be returned.
+          while (found && found.parentNode != this.container)
+            found = found.parentNode
+          if (found && (found.dirty || found.nodeType == 3))
+            return found;
+        } catch (e) {}
+      }
+      return null;
+    },
+
+    // Pick dirty nodes, and highlight them, until
+    // options.linesPerPass lines have been highlighted. The highlight
+    // method will continue to next lines as long as it finds dirty
+    // nodes. It returns an object indicating the amount of lines
+    // left, and information about the place where it stopped. If
+    // there are dirty nodes left after this function has spent all
+    // its lines, it shedules another highlight to finish the job.
+    highlightDirty: function(force) {
+      var lines = force ? Infinity : this.options.linesPerPass;
+      if (!this.options.readOnly) select.markSelection(this.win);
+      var start;
+      while (lines > 0 && (start = this.getDirtyNode())){
+        var result = this.highlight(start, lines);
+        if (result) {
+          lines = result.left;
+          if (result.node && result.dirty)
+            this.addDirtyNode(result.node);
+        }
+      }
+      if (!this.options.readOnly) select.selectMarked();
+      if (start)
+        this.scheduleHighlight();
+      return this.dirty.length == 0;
+    },
+
+    // Creates a function that, when called through a timeout, will
+    // continuously re-parse the document.
+    documentScanner: function(linesPer) {
+      var self = this, pos = null;
+      return function() {
+        // If the current node is no longer in the document... oh
+        // well, we start over.
+        if (pos && pos.parentNode != self.container)
+          pos = null;
+        select.markSelection(self.win);
+        var result = self.highlight(pos, linesPer, true);
+        select.selectMarked();
+        var newPos = result ? (result.node && result.node.nextSibling) : null;
+        pos = (pos == newPos) ? null : newPos;
+        self.delayScanning();
+      };
+    },
+
+    // Starts the continuous scanning process for this document after
+    // a given interval.
+    delayScanning: function() {
+      if (this.scanner) {
+        this.parent.clearTimeout(this.documentScan);
+        this.documentScan = this.parent.setTimeout(this.scanner, this.options.continuousScanning);
+      }
+    },
+
+    // The function that does the actual highlighting/colouring (with
+    // help from the parser and the DOM normalizer). Its interface is
+    // rather overcomplicated, because it is used in different
+    // situations: ensuring that a certain line is highlighted, or
+    // highlighting up to X lines starting from a certain point. The
+    // 'from' argument gives the node at which it should start. If
+    // this is null, it will start at the beginning of the frame. When
+    // a number of lines is given with the 'lines' argument, it will
+    // colour no more than that amount. If at any time it comes across
+    // a 'clean' line (no dirty nodes), it will stop, except when
+    // 'cleanLines' is true.
+    highlight: function(from, lines, cleanLines){
+      var container = this.container, self = this, active = this.options.activeTokens, origFrom = from;
+
+      if (!container.firstChild)
+        return;
+      // lines given as null means 'make sure this BR node has up to date parser information'
+      if (lines == null) {
+        if (!from) return;
+        else from = from.previousSibling;
+      }
+      // Backtrack to the first node before from that has a partial
+      // parse stored.
+      while (from && (!from.parserFromHere || from.dirty))
+        from = from.previousSibling;
+      // If we are at the end of the document, do nothing.
+      if (from && !from.nextSibling)
+        return;
+
+      // Check whether a part (<span> node) and the corresponding token
+      // match.
+      function correctPart(token, part){
+        return !part.reduced && part.currentText == token.value && part.className == token.style;
+      }
+      // Shorten the text associated with a part by chopping off
+      // characters from the front. Note that only the currentText
+      // property gets changed. For efficiency reasons, we leave the
+      // nodeValue alone -- we set the reduced flag to indicate that
+      // this part must be replaced.
+      function shortenPart(part, minus){
+        part.currentText = part.currentText.substring(minus);
+        part.reduced = true;
+      }
+      // Create a part corresponding to a given token.
+      function tokenPart(token){
+        var part = makePartSpan(token.value, self.doc);
+        part.className = token.style;
+        return part;
+      }
+
+      // Get the token stream. If from is null, we start with a new
+      // parser from the start of the frame, otherwise a partial parse
+      // is resumed.
+      var traversal = traverseDOM(from ? from.nextSibling : container.firstChild),
+          stream = stringStream(traversal),
+          parsed = from ? from.parserFromHere(stream) : Editor.Parser.make(stream);
+
+      // parts is an interface to make it possible to 'delay' fetching
+      // the next DOM node until we are completely done with the one
+      // before it. This is necessary because often the next node is
+      // not yet available when we want to proceed past the current
+      // one.
+      var parts = {
+        current: null,
+        // Fetch current node.
+        get: function(){
+          if (!this.current)
+            this.current = traversal.nodes.shift();
+          return this.current;
+        },
+        // Advance to the next part (do not fetch it yet).
+        next: function(){
+          this.current = null;
+        },
+        // Remove the current part from the DOM tree, and move to the
+        // next.
+        remove: function(){
+          container.removeChild(this.get());
+          this.current = null;
+        },
+        // Advance to the next part that is not empty, discarding empty
+        // parts.
+        getNonEmpty: function(){
+          var part = this.get();
+          // Allow empty nodes when they are alone on a line, needed
+          // for the FF cursor bug workaround (see select.js,
+          // insertNewlineAtCursor).
+          while (part && part.nodeName == "SPAN" && part.currentText == "") {
+            var old = part;
+            this.remove();
+            part = this.get();
+            // Adjust selection information, if any. See select.js for details.
+            select.snapshotMove(old.firstChild, part.firstChild || part, 0);
+          }
+          return part;
+        }
+      };
+
+      var lineDirty = false, prevLineDirty = true, lineNodes = 0;
+
+      // This forEach loops over the tokens from the parsed stream, and
+      // at the same time uses the parts object to proceed through the
+      // corresponding DOM nodes.
+      forEach(parsed, function(token){
+        var part = parts.getNonEmpty();
+
+        if (token.value == "\n"){
+          // The idea of the two streams actually staying synchronized
+          // is such a long shot that we explicitly check.
+          if (part.nodeName != "BR")
+            throw "Parser out of sync. Expected BR.";
+
+          if (part.dirty || !part.indentation) lineDirty = true;
+          if (lineDirty) self.history.touch(from);
+          from = part;
+
+          // Every <br> gets a copy of the parser state and a lexical
+          // context assigned to it. The first is used to be able to
+          // later resume parsing from this point, the second is used
+          // for indentation.
+          part.parserFromHere = parsed.copy();
+          part.indentation = token.indentation;
+          part.dirty = false;
+
+          // No line argument passed means 'go at least until this node'.
+          if (lines == null && part == origFrom) throw StopIteration;
+
+          // A clean line with more than one node means we are done.
+          // Throwing a StopIteration is the way to break out of a
+          // MochiKit forEach loop.
+          if ((lines !== undefined && --lines <= 0) || (!lineDirty && !prevLineDirty && lineNodes > 1 && !cleanLines))
+            throw StopIteration;
+          prevLineDirty = lineDirty; lineDirty = false; lineNodes = 0;
+          parts.next();
+        }
+        else {
+          if (part.nodeName != "SPAN")
+            throw "Parser out of sync. Expected SPAN.";
+          if (part.dirty)
+            lineDirty = true;
+          lineNodes++;
+
+          // If the part matches the token, we can leave it alone.
+          if (correctPart(token, part)){
+            part.dirty = false;
+            parts.next();
+          }
+          // Otherwise, we have to fix it.
+          else {
+            lineDirty = true;
+            // Insert the correct part.
+            var newPart = tokenPart(token);
+            container.insertBefore(newPart, part);
+            if (active) active(newPart, token, self);
+            var tokensize = token.value.length;
+            var offset = 0;
+            // Eat up parts until the text for this token has been
+            // removed, adjusting the stored selection info (see
+            // select.js) in the process.
+            while (tokensize > 0) {
+              part = parts.get();
+              var partsize = part.currentText.length;
+              select.snapshotReplaceNode(part.firstChild, newPart.firstChild, tokensize, offset);
+              if (partsize > tokensize){
+                shortenPart(part, tokensize);
+                tokensize = 0;
+              }
+              else {
+                tokensize -= partsize;
+                offset += partsize;
+                parts.remove();
+              }
+            }
+          }
+        }
+      });
+      if (lineDirty) this.history.touch(from);
+
+      // The function returns some status information that is used by
+      // hightlightDirty to determine whether and where it has to
+      // continue.
+      return {left: lines,
+              node: parts.get(),
+              dirty: lineDirty};
+    }
+  };
+
+  return Editor;
+})();
+
+addEventHandler(window, "load", function() {
+  var CodeMirror = window.frameElement.CodeMirror;
+  CodeMirror.editor = new Editor(CodeMirror.options);
+  if (CodeMirror.options.initCallback) {
+    this.parent.setTimeout(function(){
+      CodeMirror.options.initCallback(CodeMirror);
+    }, 0);
+  }
+});
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/mirrorframe.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/mirrorframe.js
new file mode 100644
index 0000000..7f6ad1a
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/mirrorframe.js
@@ -0,0 +1,81 @@
+/* Demonstration of embedding CodeMirror in a bigger application. The
+ * interface defined here is a mess of prompts and confirms, and
+ * should probably not be used in a real project.
+ */
+
+function MirrorFrame(place, options) {
+  this.home = document.createElement("DIV");
+  if (place.appendChild)
+    place.appendChild(this.home);
+  else
+    place(this.home);
+
+  var self = this;
+  function makeButton(name, action) {
+    var button = document.createElement("INPUT");
+    button.type = "button";
+    button.value = name;
+    self.home.appendChild(button);
+    button.onclick = function(){self[action].call(self);};
+  }
+
+  makeButton("Search", "search");
+  makeButton("Replace", "replace");
+  makeButton("Current line", "line");
+  makeButton("Jump to line", "jump");
+  makeButton("Insert constructor", "macro");
+  makeButton("Indent all", "reindent");
+
+  this.mirror = new CodeMirror(this.home, options);
+}
+
+MirrorFrame.prototype = {
+  search: function() {
+    var text = prompt("Enter search term:", "");
+    if (!text) return;
+
+    var first = true;
+    do {
+      var cursor = this.mirror.getSearchCursor(text, first);
+      first = false;
+      while (cursor.findNext()) {
+        cursor.select();
+        if (!confirm("Search again?"))
+          return;
+      }
+    } while (confirm("End of document reached. Start over?"));
+  },
+
+  replace: function() {
+    // This is a replace-all, but it is possible to implement a
+    // prompting replace.
+    var from = prompt("Enter search string:", ""), to;
+    if (from) to = prompt("What should it be replaced with?", "");
+    if (to == null) return;
+
+    var cursor = this.mirror.getSearchCursor(from, false);
+    while (cursor.findNext())
+      cursor.replace(to);
+  },
+
+  jump: function() {
+    var line = prompt("Jump to line:", "");
+    if (line && !isNaN(Number(line)))
+      this.mirror.jumpToLine(Number(line));
+  },
+
+  line: function() {
+    alert("The cursor is currently at line " + this.mirror.currentLine());
+    this.mirror.focus();
+  },
+
+  macro: function() {
+    var name = prompt("Name your constructor:", "");
+    if (name)
+      this.mirror.replaceSelection("function " + name + "() {\n  \n}\n\n" + name + ".prototype = {\n  \n};\n");
+  },
+
+  reindent: function() {
+    this.mirror.reindent();
+  }
+};
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsecss.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsecss.js
new file mode 100644
index 0000000..c22f295
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsecss.js
@@ -0,0 +1,155 @@
+/* Simple parser for CSS */
+
+var CSSParser = Editor.Parser = (function() {
+  var tokenizeCSS = (function() {
+    function normal(source, setState) {
+      var ch = source.next();
+      if (ch == "@") {
+        source.nextWhile(matcher(/\w/));
+        return "css-at";
+      }
+      else if (ch == "/" && source.equals("*")) {
+        setState(inCComment);
+        return null;
+      }
+      else if (ch == "<" && source.equals("!")) {
+        setState(inSGMLComment);
+        return null;
+      }
+      else if (ch == "=") {
+        return "css-compare";
+      }
+      else if (source.equals("=") && (ch == "~" || ch == "|")) {
+        source.next();
+        return "css-compare";
+      }
+      else if (ch == "\"" || ch == "'") {
+        setState(inString(ch));
+        return null;
+      }
+      else if (ch == "#") {
+        source.nextWhile(matcher(/\w/));
+        return "css-hash";
+      }
+      else if (ch == "!") {
+        source.nextWhile(matcher(/[ \t]/));
+        source.nextWhile(matcher(/\w/));
+        return "css-important";
+      }
+      else if (/\d/.test(ch)) {
+        source.nextWhile(matcher(/[\w.%]/));
+        return "css-unit";
+      }
+      else if (/[,.+>*\/]/.test(ch)) {
+        return "css-select-op";
+      }
+      else if (/[;{}:\[\]]/.test(ch)) {
+        return "css-punctuation";
+      }
+      else {
+        source.nextWhile(matcher(/[\w\\\-_]/));
+        return "css-identifier";
+      }
+    }
+
+    function inCComment(source, setState) {
+      var maybeEnd = false;
+      while (!source.endOfLine()) {
+        var ch = source.next();
+        if (maybeEnd && ch == "/") {
+          setState(normal);
+          break;
+        }
+        maybeEnd = (ch == "*");
+      }
+      return "css-comment";
+    }
+
+    function inSGMLComment(source, setState) {
+      var dashes = 0;
+      while (!source.endOfLine()) {
+        var ch = source.next();
+        if (dashes >= 2 && ch == ">") {
+          setState(normal);
+          break;
+        }
+        dashes = (ch == "-") ? dashes + 1 : 0;
+      }
+      return "css-comment";
+    }
+
+    function inString(quote) {
+      return function(source, setState) {
+        var escaped = false;
+        while (!source.endOfLine()) {
+          var ch = source.next();
+          if (ch == quote && !escaped)
+            break;
+          escaped = !escaped && ch == "\\";
+        }
+        if (!escaped)
+          setState(normal);
+        return "css-string";
+      };
+    }
+
+    return function(source, startState) {
+      return tokenizer(source, startState || normal);
+    };
+  })();
+
+  function indentCSS(inBraces, inRule, base) {
+    return function(nextChars) {
+      if (!inBraces || /^\}/.test(nextChars)) return base;
+      else if (inRule) return base + 4;
+      else return base + 2;
+    };
+  }
+
+  // This is a very simplistic parser -- since CSS does not really
+  // nest, it works acceptably well, but some nicer colouroing could
+  // be provided with a more complicated parser.
+  function parseCSS(source, basecolumn) {
+    basecolumn = basecolumn || 0;
+    var tokens = tokenizeCSS(source);
+    var inBraces = false, inRule = false;
+
+    var iter = {
+      next: function() {
+        var token = tokens.next(), style = token.style, content = token.content;
+
+        if (style == "css-identifier" && inRule)
+          token.style = "css-value";
+        if (style == "css-hash")
+          token.style =  inRule ? "css-colorcode" : "css-identifier";
+
+        if (content == "\n")
+          token.indentation = indentCSS(inBraces, inRule, basecolumn);
+
+        if (content == "{")
+          inBraces = true;
+        else if (content == "}")
+          inBraces = inRule = false;
+        else if (inBraces && content == ";")
+          inRule = false;
+        else if (inBraces && style != "css-comment" && style != "whitespace")
+          inRule = true;
+
+        return token;
+      },
+
+      copy: function() {
+        var _inBraces = inBraces, _inRule = inRule, _tokenState = tokens.state;
+        return function(source) {
+          tokens = tokenizeCSS(source, _tokenState);
+          inBraces = _inBraces;
+          inRule = _inRule;
+          return iter;
+        };
+      }
+    };
+    return iter;
+  }
+
+  return {make: parseCSS, electricChars: "}"};
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsehtmlmixed.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsehtmlmixed.js
new file mode 100644
index 0000000..166967f
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsehtmlmixed.js
@@ -0,0 +1,73 @@
+var HTMLMixedParser = Editor.Parser = (function() {
+  if (!(CSSParser && JSParser && XMLParser))
+    throw new Error("CSS, JS, and XML parsers must be loaded for HTML mixed mode to work.");
+  XMLParser.configure({useHTMLKludges: true});
+
+  function parseMixed(stream) {
+    var htmlParser = XMLParser.make(stream), localParser = null, inTag = false;
+    var iter = {next: top, copy: copy};
+
+    function top() {
+      var token = htmlParser.next();
+      if (token.content == "<")
+        inTag = true;
+      else if (token.style == "xml-tagname" && inTag === true)
+        inTag = token.content.toLowerCase();
+      else if (token.content == ">") {
+        if (inTag == "script")
+          iter.next = local(JSParser, "</script");
+        else if (inTag == "style")
+          iter.next = local(CSSParser, "</style");
+        inTag = false;
+      }
+      return token;
+    }
+    function local(parser, tag) {
+      var baseIndent = htmlParser.indentation();
+      localParser = parser.make(stream, baseIndent + 2);
+      return function() {
+        if (stream.lookAhead(tag, false, false, true)) {
+          localParser = null;
+          iter.next = top;
+          return top();
+        }
+
+        var token = localParser.next();
+        var lt = token.value.lastIndexOf("<"), sz = Math.min(token.value.length - lt, tag.length);
+        if (lt != -1 && token.value.slice(lt, lt + sz).toLowerCase() == tag.slice(0, sz) &&
+            stream.lookAhead(tag.slice(sz), false, false, true)) {
+          stream.push(token.value.slice(lt));
+          token.value = token.value.slice(0, lt);
+        }
+
+        if (token.indentation) {
+          var oldIndent = token.indentation;
+          token.indentation = function(chars) {
+            if (chars == "</")
+              return baseIndent;
+            else
+              return oldIndent(chars);
+          }
+        }
+
+        return token;
+      };
+    }
+
+    function copy() {
+      var _html = htmlParser.copy(), _local = localParser && localParser.copy(),
+          _next = iter.next, _inTag = inTag;
+      return function(_stream) {
+        stream = _stream;
+        htmlParser = _html(_stream);
+        localParser = _local && _local(_stream);
+        iter.next = _next;
+        inTag = _inTag;
+        return iter;
+      };
+    }
+    return iter;
+  }
+
+  return {make: parseMixed, electricChars: "{}/"};
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsejavascript.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsejavascript.js
new file mode 100644
index 0000000..16ddf2b
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsejavascript.js
@@ -0,0 +1,322 @@
+/* Parse function for JavaScript. Makes use of the tokenizer from
+ * tokenizejavascript.js. Note that your parsers do not have to be
+ * this complicated -- if you don't want to recognize local variables,
+ * in many languages it is enough to just look for braces, semicolons,
+ * parentheses, etc, and know when you are inside a string or comment.
+ *
+ * See manual.html for more info about the parser interface.
+ */
+
+var JSParser = Editor.Parser = (function() {
+  // Token types that can be considered to be atoms.
+  var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true};
+  // Constructor for the lexical context objects.
+  function JSLexical(indented, column, type, align, prev) {
+    // indentation at start of this line
+    this.indented = indented;
+    // column at which this scope was opened
+    this.column = column;
+    // type of scope ('vardef', 'stat' (statement), 'form' (special form), '[', '{', or '(')
+    this.type = type;
+    // '[', '{', or '(' blocks that have any text after their opening
+    // character are said to be 'aligned' -- any lines below are
+    // indented all the way to the opening character.
+    if (align != null)
+      this.align = align;
+    // Parent scope, if any.
+    this.prev = prev;
+  }
+  // My favourite JavaScript indentation rules.
+  function indentJS(lexical) {
+    return function(firstChars) {
+      var firstChar = firstChars && firstChars.charAt(0);
+      var closing = firstChar == lexical.type;
+      if (lexical.type == "vardef")
+        return lexical.indented + 4;
+      else if (lexical.type == "form" && firstChar == "{")
+        return lexical.indented;
+      else if (lexical.type == "stat" || lexical.type == "form")
+        return lexical.indented + 2;
+      else if (lexical.align)
+        return lexical.column - (closing ? 1 : 0);
+      else
+        return lexical.indented + (closing ? 0 : 2);
+    };
+  }
+
+  // The parser-iterator-producing function itself.
+  function parseJS(input, basecolumn) {
+    // Wrap the input in a token stream
+    var tokens = tokenizeJavaScript(input);
+    // The parser state. cc is a stack of actions that have to be
+    // performed to finish the current statement. For example we might
+    // know that we still need to find a closing parenthesis and a
+    // semicolon. Actions at the end of the stack go first. It is
+    // initialized with an infinitely looping action that consumes
+    // whole statements.
+    var cc = [statements];
+    // Context contains information about the current local scope, the
+    // variables defined in that, and the scopes above it.
+    var context = null;
+    // The lexical scope, used mostly for indentation.
+    var lexical = new JSLexical((basecolumn || 0) - 2, 0, "block", false);
+    // Current column, and the indentation at the start of the current
+    // line. Used to create lexical scope objects.
+    var column = 0;
+    var indented = 0;
+    // Variables which are used by the mark, cont, and pass functions
+    // below to communicate with the driver loop in the 'next'
+    // function.
+    var consume, marked;
+
+    // The iterator object.
+    var parser = {next: next, copy: copy};
+
+    function next(){
+      // Start by performing any 'lexical' actions (adjusting the
+      // lexical variable), or the operations below will be working
+      // with the wrong lexical state.
+      while(cc[cc.length - 1].lex)
+        cc.pop()();
+
+      // Fetch a token.
+      var token = tokens.next();
+
+      // Adjust column and indented.
+      if (token.type == "whitespace" && column == 0)
+        indented = token.value.length;
+      column += token.value.length;
+      if (token.content == "\n"){
+        indented = column = 0;
+        // If the lexical scope's align property is still undefined at
+        // the end of the line, it is an un-aligned scope.
+        if (!("align" in lexical))
+          lexical.align = false;
+        // Newline tokens get an indentation function associated with
+        // them.
+        token.indentation = indentJS(lexical);
+      }
+      // No more processing for meaningless tokens.
+      if (token.type == "whitespace" || token.type == "comment")
+        return token;
+      // When a meaningful token is found and the lexical scope's
+      // align is undefined, it is an aligned scope.
+      if (!("align" in lexical))
+        lexical.align = true;
+
+      // Execute actions until one 'consumes' the token and we can
+      // return it.
+      while(true) {
+        consume = marked = false;
+        // Take and execute the topmost action.
+        cc.pop()(token.type, token.content);
+        if (consume){
+          // Marked is used to change the style of the current token.
+          if (marked)
+            token.style = marked;
+          // Here we differentiate between local and global variables.
+          else if (token.type == "variable" && inScope(token.content))
+            token.style = "js-localvariable";
+          return token;
+        }
+      }
+    }
+
+    // This makes a copy of the parser state. It stores all the
+    // stateful variables in a closure, and returns a function that
+    // will restore them when called with a new input stream. Note
+    // that the cc array has to be copied, because it is contantly
+    // being modified. Lexical objects are not mutated, and context
+    // objects are not mutated in a harmful way, so they can be shared
+    // between runs of the parser.
+    function copy(){
+      var _context = context, _lexical = lexical, _cc = cc.concat([]), _tokenState = tokens.state;
+
+      return function copyParser(input){
+        context = _context;
+        lexical = _lexical;
+        cc = _cc.concat([]); // copies the array
+        column = indented = 0;
+        tokens = tokenizeJavaScript(input, _tokenState);
+        return parser;
+      };
+    }
+
+    // Helper function for pushing a number of actions onto the cc
+    // stack in reverse order.
+    function push(fs){
+      for (var i = fs.length - 1; i >= 0; i--)
+        cc.push(fs[i]);
+    }
+    // cont and pass are used by the action functions to add other
+    // actions to the stack. cont will cause the current token to be
+    // consumed, pass will leave it for the next action.
+    function cont(){
+      push(arguments);
+      consume = true;
+    }
+    function pass(){
+      push(arguments);
+      consume = false;
+    }
+    // Used to change the style of the current token.
+    function mark(style){
+      marked = style;
+    }
+
+    // Push a new scope. Will automatically link the current scope.
+    function pushcontext(){
+      context = {prev: context, vars: {"this": true, "arguments": true}};
+    }
+    // Pop off the current scope.
+    function popcontext(){
+      context = context.prev;
+    }
+    // Register a variable in the current scope.
+    function register(varname){
+      if (context){
+        mark("js-variabledef");
+        context.vars[varname] = true;
+      }
+    }
+    // Check whether a variable is defined in the current scope.
+    function inScope(varname){
+      var cursor = context;
+      while (cursor) {
+        if (cursor.vars[varname])
+          return true;
+        cursor = cursor.prev;
+      }
+      return false;
+    }
+
+    // Push a new lexical context of the given type.
+    function pushlex(type){
+      var result = function(){
+        lexical = new JSLexical(indented, column, type, null, lexical)
+      };
+      result.lex = true;
+      return result;
+    }
+    // Pop off the current lexical context.
+    function poplex(){
+      lexical = lexical.prev;
+    }
+    poplex.lex = true;
+    // The 'lex' flag on these actions is used by the 'next' function
+    // to know they can (and have to) be ran before moving on to the
+    // next token.
+
+    // Creates an action that discards tokens until it finds one of
+    // the given type.
+    function expect(wanted){
+      return function expecting(type){
+        if (type == wanted) cont();
+        else cont(arguments.callee);
+      };
+    }
+
+    // Looks for a statement, and then calls itself.
+    function statements(type){
+      return pass(statement, statements);
+    }
+    // Dispatches various types of statements based on the type of the
+    // current token.
+    function statement(type){
+      if (type == "var") cont(pushlex("vardef"), vardef1, expect(";"), poplex);
+      else if (type == "keyword a") cont(pushlex("form"), expression, statement, poplex);
+      else if (type == "keyword b") cont(pushlex("form"), statement, poplex);
+      else if (type == "{") cont(pushlex("}"), block, poplex);
+      else if (type == "function") cont(functiondef);
+      else if (type == "for") cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"), poplex, statement, poplex);
+      else if (type == "variable") cont(pushlex("stat"), maybelabel);
+      else if (type == "case") cont(expression, expect(":"));
+      else if (type == "default") cont(expect(":"));
+      else if (type == "catch") cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), statement, poplex, popcontext);
+      else pass(pushlex("stat"), expression, expect(";"), poplex);
+    }
+    // Dispatch expression types.
+    function expression(type){
+      if (atomicTypes.hasOwnProperty(type)) cont(maybeoperator);
+      else if (type == "function") cont(functiondef);
+      else if (type == "keyword c") cont(expression);
+      else if (type == "(") cont(pushlex(")"), expression, expect(")"), poplex);
+      else if (type == "operator") cont(expression);
+      else if (type == "[") cont(pushlex("]"), commasep(expression), expect("]"), poplex);
+      else if (type == "{") cont(pushlex("}"), commasep(objprop), expect("}"), poplex);
+    }
+    // Called for places where operators, function calls, or
+    // subscripts are valid. Will skip on to the next action if none
+    // is found.
+    function maybeoperator(type){
+      if (type == "operator") cont(expression);
+      else if (type == "(") cont(pushlex(")"), expression, commasep(expression), expect(")"), poplex, maybeoperator);
+      else if (type == ".") cont(property, maybeoperator);
+      else if (type == "[") cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator);
+    }
+    // When a statement starts with a variable name, it might be a
+    // label. If no colon follows, it's a regular statement.
+    function maybelabel(type){
+      if (type == ":") cont(poplex, statement);
+      else pass(maybeoperator, expect(";"), poplex);
+    }
+    // Property names need to have their style adjusted -- the
+    // tokenizer thinks they are variables.
+    function property(type){
+      if (type == "variable") {mark("js-property"); cont();}
+    }
+    // This parses a property and its value in an object literal.
+    function objprop(type){
+      if (type == "variable") mark("js-property");
+      if (atomicTypes.hasOwnProperty(type)) cont(expect(":"), expression);
+    }
+    // Parses a comma-separated list of the things that are recognized
+    // by the 'what' argument.
+    function commasep(what){
+      function proceed(type) {
+        if (type == ",") cont(what, proceed);
+      };
+      return function commaSeparated() {
+        pass(what, proceed);
+      };
+    }
+    // Look for statements until a closing brace is found.
+    function block(type){
+      if (type == "}") cont();
+      else pass(statement, block);
+    }
+    // Variable definitions are split into two actions -- 1 looks for
+    // a name or the end of the definition, 2 looks for an '=' sign or
+    // a comma.
+    function vardef1(type, value){
+      if (type == "variable"){register(value); cont(vardef2);}
+      else cont();
+    }
+    function vardef2(type){
+      if (type == "operator") cont(expression, vardef2);
+      else if (type == ",") cont(vardef1);
+    }
+    // For loops.
+    function forspec1(type, value){
+      if (type == "var") cont(vardef1, forspec2);
+      else cont(expression, forspec2);
+    }
+    function forspec2(type){
+      if (type == ",") cont(forspec1);
+      if (type == ";") cont(expression, expect(";"), expression);
+    }
+    // A function definition creates a new context, and the variables
+    // in its argument list have to be added to this context.
+    function functiondef(type, value){
+      if (type == "variable"){register(value); cont(functiondef);}
+      else if (type == "(") cont(pushcontext, commasep(funarg), expect(")"), statement, popcontext);
+    }
+    function funarg(type, value){
+      if (type == "variable"){register(value); cont();}
+    }
+
+    return parser;
+  }
+
+  return {make: parseJS, electricChars: "{}"};
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsemarc.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsemarc.js
new file mode 100644
index 0000000..00ee23d
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsemarc.js
@@ -0,0 +1,102 @@
+Editor.Parser = (function() {
+	function isWhiteSpace(ch) {
+		// The messy regexp is because IE's regexp matcher is of the
+		// opinion that non-breaking spaces are no whitespace.
+		return ch != "\n" && /^[\s\u00a0]*$/.test(ch);
+	}
+
+	var tokenizeMARC = (function() {
+		function normal(source, setState) {
+			var ch = source.next();
+			if (ch == '$' || ch == '|') {
+				if (source.applies(matcher(/[a-z0-9]/)) && source.next() && source.applies(isWhiteSpace)) {
+					return 'marc-subfield';
+				} else {
+					return 'marc-word';
+				}
+			} else if (ch.match(/[0-9]/)) {
+				// This and the next block are muddled because tags are ^[0-9]{3} and indicators are [0-9_]{2}.
+				var length = 1;
+				while (source.applies(matcher(/[0-9]/))) {
+					source.next();
+					length++;
+				}
+
+				if (length == 1 && source.lookAhead('_')) {
+					source.next();
+					return 'marc-indicator';
+				}
+
+				if (source.applies(isWhiteSpace) && length == 2) {
+					return 'marc-indicator';
+				} else if (source.applies(isWhiteSpace) && length == 3) {
+					return 'marc-tag';
+				} else {
+					return 'marc-word';
+				}
+			} else if (ch == '_') {
+				if (source.applies(matcher(/[0-9_]/)) && source.next() && source.applies(isWhiteSpace)) {
+					return 'marc-indicator';
+				} else {
+					return 'marc-word';
+				}
+			} else {
+				source.nextWhile(matcher(/[^\$|\n]/));
+				return 'marc-word';
+			}
+		}
+
+		return function(source, startState) {
+			return tokenizer(source, startState || normal);
+		};
+	})();
+
+	function indentMARC(context) {
+		return function(nextChars) {
+			return 0;
+		};
+	}
+
+	function parseMARC(source) {
+		var tokens = tokenizeMARC(source);
+		var context = null, indent = 0, col = 0;
+
+		var iter = {
+			next: function() {
+				var token = tokens.next(), type = token.style, content = token.content, width = token.value.length;
+
+				if (content == "\n") {
+					token.indentation = indentMARC(context);
+					indent = col = 0;
+					if (context && context.align === null) { context.align = false }
+				} else if (type == "whitespace" && col === 0) {
+					indent = width;
+				} else if (type != "sp-comment" && context && context.align === null) {
+					context.align = true;
+				}
+
+				if ((type == 'marc-tag' && col != 0) || (type == 'marc-indicator' && col != 4)) {
+					token.style = 'marc-word';
+				}
+
+				if (content != "\n") { col += width }
+
+				return token;
+			},
+
+			copy: function() {
+				var _context = context, _indent = indent, _col = col, _tokenState = tokens.state;
+				return function(source) {
+					tokens = tokenizeMARC(source, _tokenState);
+					context = _context;
+					indent = _indent;
+					col = _col;
+					return iter;
+				};
+			}
+		};
+		return iter;
+	}
+
+	return {make: parseMARC, electricChars: "}]"};
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsesparql.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsesparql.js
new file mode 100644
index 0000000..58ced1c
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsesparql.js
@@ -0,0 +1,162 @@
+Editor.Parser = (function() {
+  function wordRegexp(words) {
+    return new RegExp("^(?:" + words.join("|") + ")$", "i");
+  }
+  var ops = wordRegexp(["str", "lang", "langmatches", "datatype", "bound", "sameterm", "isiri", "isuri",
+                        "isblank", "isliteral", "union", "a"]);
+  var keywords = wordRegexp(["base", "prefix", "select", "distinct", "reduced", "construct", "describe",
+                             "ask", "from", "named", "where", "order", "limit", "offset", "filter", "optional",
+                             "graph", "by", "asc", "desc", ]);
+  var operatorChars = /[*+\-<>=&|]/;
+
+  var tokenizeSparql = (function() {
+    function normal(source, setState) {
+      var ch = source.next();
+      if (ch == "$" || ch == "?") {
+        source.nextWhile(matcher(/[\w\d]/));
+        return "sp-var";
+      }
+      else if (ch == "<" && !source.applies(matcher(/[\s\u00a0=]/))) {
+        source.nextWhile(matcher(/[^\s\u00a0>]/));
+        if (source.equals(">")) source.next();
+        return "sp-uri";
+      }
+      else if (ch == "\"" || ch == "'") {
+        setState(inLiteral(ch));
+        return null;
+      }
+      else if (/[{}\(\),\.;\[\]]/.test(ch)) {
+        return "sp-punc";
+      }
+      else if (ch == "#") {
+        while (!source.endOfLine()) source.next();
+        return "sp-comment";
+      }
+      else if (operatorChars.test(ch)) {
+        source.nextWhile(matcher(operatorChars));
+        return "sp-operator";
+      }
+      else if (ch == ":") {
+        source.nextWhile(matcher(/[\w\d\._\-]/));
+        return "sp-prefixed";
+      }
+      else {
+        source.nextWhile(matcher(/[_\w\d]/));
+        if (source.equals(":")) {
+          source.next();
+          source.nextWhile(matcher(/[\w\d_\-]/));
+          return "sp-prefixed";
+        }
+        var word = source.get(), type;
+        if (ops.test(word))
+          type = "sp-operator";
+        else if (keywords.test(word))
+          type = "sp-keyword";
+        else
+          type = "sp-word";
+        return {style: type, content: word};
+      }
+    }
+
+    function inLiteral(quote) {
+      return function(source, setState) {
+        var escaped = false;
+        while (!source.endOfLine()) {
+          var ch = source.next();
+          if (ch == quote && !escaped) {
+            setState(normal);
+            break;
+          }
+          escaped = !escaped && ch == "\\";
+        }
+        return "sp-literal";
+      };
+    }
+
+    return function(source, startState) {
+      return tokenizer(source, startState || normal);
+    };
+  })();
+
+  function indentSparql(context) {
+    return function(nextChars) {
+      var firstChar = nextChars && nextChars.charAt(0);
+      if (/[\]\}]/.test(firstChar))
+        while (context && context.type == "pattern") context = context.prev;
+
+      var closing = context && firstChar == matching[context.type];
+      if (!context)
+        return 0;
+      else if (context.type == "pattern")
+        return context.col;
+      else if (context.align)
+        return context.col - (closing ? context.width : 0);
+      else
+        return context.indent + (closing ? 0 : 2);
+    }
+  }
+
+  function parseSparql(source) {
+    var tokens = tokenizeSparql(source);
+    var context = null, indent = 0, col = 0;
+    function pushContext(type, width) {
+      context = {prev: context, indent: indent, col: col, type: type, width: width};
+    }
+    function popContext() {
+      context = context.prev;
+    }
+
+    var iter = {
+      next: function() {
+        var token = tokens.next(), type = token.style, content = token.content, width = token.value.length;
+
+        if (content == "\n") {
+          token.indentation = indentSparql(context);
+          indent = col = 0;
+          if (context && context.align == null) context.align = false;
+        }
+        else if (type == "whitespace" && col == 0) {
+          indent = width;
+        }
+        else if (type != "sp-comment" && context && context.align == null) {
+          context.align = true;
+        }
+
+        if (content != "\n") col += width;
+
+        if (/[\[\{\(]/.test(content)) {
+          pushContext(content, width);
+        }
+        else if (/[\]\}\)]/.test(content)) {
+          while (context && context.type == "pattern")
+            popContext();
+          if (context && content == matching[context.type])
+            popContext();
+        }
+        else if (content == "." && context && context.type == "pattern") {
+          popContext();
+        }
+        else if ((type == "sp-word" || type == "sp-prefixed" || type == "sp-uri" || type == "sp-var" || type == "sp-literal") &&
+                 context && /[\{\[]/.test(context.type)) {
+          pushContext("pattern", width);
+        }
+
+        return token;
+      },
+
+      copy: function() {
+        var _context = context, _indent = indent, _col = col, _tokenState = tokens.state;
+        return function(source) {
+          tokens = tokenizeSparql(source, _tokenState);
+          context = _context;
+          indent = _indent;
+          col = _col;
+          return iter;
+        };
+      }
+    };
+    return iter;
+  }
+
+  return {make: parseSparql, electricChars: "}]"};
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsexml.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsexml.js
new file mode 100644
index 0000000..eee54de
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsexml.js
@@ -0,0 +1,286 @@
+/* This file defines an XML parser, with a few kludges to make it
+ * useable for HTML. autoSelfClosers defines a set of tag names that
+ * are expected to not have a closing tag, and doNotIndent specifies
+ * the tags inside of which no indentation should happen (see Config
+ * object). These can be disabled by passing the editor an object like
+ * {useHTMLKludges: false} as parserConfig option.
+ */
+
+var XMLParser = Editor.Parser = (function() {
+  var Kludges = {
+    autoSelfClosers: {"br": true, "img": true, "hr": true, "link": true, "input": true,
+                      "meta": true, "col": true, "frame": true, "base": true, "area": true},
+    doNotIndent: {"pre": true}
+  };
+  var NoKludges = {autoSelfClosers: {}, doNotIndent: {}};
+  var UseKludges = Kludges;
+
+  // Simple stateful tokenizer for XML documents. Returns a
+  // MochiKit-style iterator, with a state property that contains a
+  // function encapsulating the current state. See tokenize.js.
+  var tokenizeXML = (function() {
+    function inText(source, setState) {
+      var ch = source.next();
+      if (ch == "<") {
+        if (source.equals("!")) {
+          source.next();
+          if (source.equals("[")) {
+            if (source.lookAhead("[CDATA[", true)) {
+              setState(inBlock("xml-cdata", "]]>"));
+              return null;
+            }
+            else {
+              return "xml-text";
+            }
+          }
+          else if (source.lookAhead("--", true)) {
+            setState(inBlock("xml-comment", "-->"));
+            return null;
+          }
+          else {
+            return "xml-text";
+          }
+        }
+        else if (source.equals("?")) {
+          source.next();
+          source.nextWhile(matcher(/[\w\._\-]/));
+          setState(inBlock("xml-processing", "?>"));
+          return "xml-processing";
+        }
+        else {
+          if (source.equals("/")) source.next();
+          setState(inTag);
+          return "xml-punctuation";
+        }
+      }
+      else if (ch == "&") {
+        while (!source.endOfLine()) {
+          if (source.next() == ";")
+            break;
+        }
+        return "xml-entity";
+      }
+      else {
+        source.nextWhile(matcher(/[^&<\n]/));
+        return "xml-text";
+      }
+    }
+
+    function inTag(source, setState) {
+      var ch = source.next();
+      if (ch == ">") {
+        setState(inText);
+        return "xml-punctuation";
+      }
+      else if (/[?\/]/.test(ch) && source.equals(">")) {
+        source.next();
+        setState(inText);
+        return "xml-punctuation";
+      }
+      else if (ch == "=") {
+        return "xml-punctuation";
+      }
+      else if (/[\'\"]/.test(ch)) {
+        setState(inAttribute(ch));
+        return null;
+      }
+      else {
+        source.nextWhile(matcher(/[^\s\u00a0=<>\"\'\/?]/));
+        return "xml-name";
+      }
+    }
+
+    function inAttribute(quote) {
+      return function(source, setState) {
+        while (!source.endOfLine()) {
+          if (source.next() == quote) {
+            setState(inTag);
+            break;
+          }
+        }
+        return "xml-attribute";
+      };
+    }
+
+    function inBlock(style, terminator) {
+      return function(source, setState) {
+        while (!source.endOfLine()) {
+          if (source.lookAhead(terminator, true)) {
+            setState(inText);
+            break;
+          }
+          source.next();
+        }
+        return style;
+      };
+    }
+
+    return function(source, startState) {
+      return tokenizer(source, startState || inText);
+    };
+  })();
+
+  // The parser. The structure of this function largely follows that of
+  // parseJavaScript in parsejavascript.js (there is actually a bit more
+  // shared code than I'd like), but it is quite a bit simpler.
+  function parseXML(source) {
+    var tokens = tokenizeXML(source);
+    var cc = [base];
+    var tokenNr = 0, indented = 0;
+    var currentTag = null, context = null;
+    var consume, marked;
+
+    function push(fs) {
+      for (var i = fs.length - 1; i >= 0; i--)
+        cc.push(fs[i]);
+    }
+    function cont() {
+      push(arguments);
+      consume = true;
+    }
+    function pass() {
+      push(arguments);
+      consume = false;
+    }
+
+    function mark(style) {
+      marked = style;
+    }
+    function expect(text) {
+      return function(style, content) {
+        if (content == text) cont();
+        else mark("xml-error") || cont(arguments.callee);
+      };
+    }
+
+    function pushContext(tagname, startOfLine) {
+      var noIndent = UseKludges.doNotIndent.hasOwnProperty(tagname) || (context && context.noIndent);
+      context = {prev: context, name: tagname, indent: indented, startOfLine: startOfLine, noIndent: noIndent};
+    }
+    function popContext() {
+      context = context.prev;
+    }
+    function computeIndentation(baseContext) {
+      return function(nextChars) {
+        var context = baseContext;
+        if (context && context.noIndent)
+          return 0;
+        if (context && /^<\//.test(nextChars))
+          context = context.prev;
+        while (context && !context.startOfLine)
+          context = context.prev;
+        if (context)
+          return context.indent + 2;
+        else
+          return 0;
+      };
+    }
+
+    function base() {
+      return pass(element, base);
+    }
+    var harmlessTokens = {"xml-text": true, "xml-entity": true, "xml-comment": true,
+                          "xml-cdata": true, "xml-processing": true};
+    function element(style, content) {
+      if (content == "<") cont(tagname, attributes, endtag(tokenNr == 1));
+      else if (content == "</") cont(closetagname, expect(">"));
+      else if (content == "<?") cont(tagname, attributes, expect("?>"));
+      else if (harmlessTokens.hasOwnProperty(style)) cont();
+      else mark("xml-error") || cont();
+    }
+    function tagname(style, content) {
+      if (style == "xml-name") {
+        currentTag = content.toLowerCase();
+        mark("xml-tagname");
+        cont();
+      }
+      else {
+        currentTag = null;
+        pass();
+      }
+    }
+    function closetagname(style, content) {
+      if (style == "xml-name" && context && content.toLowerCase() == context.name) {
+        popContext();
+        mark("xml-tagname");
+      }
+      else {
+        mark("xml-error");
+      }
+      cont();
+    }
+    function endtag(startOfLine) {
+      return function(style, content) {
+        if (content == "/>" || (content == ">" && UseKludges.autoSelfClosers.hasOwnProperty(currentTag))) cont();
+        else if (content == ">") pushContext(currentTag, startOfLine) || cont();
+        else mark("xml-error") || cont(arguments.callee);
+      };
+    }
+    function attributes(style) {
+      if (style == "xml-name") mark("xml-attname") || cont(attribute, attributes);
+      else pass();
+    }
+    function attribute(style, content) {
+      if (content == "=") cont(value);
+      else if (content == ">" || content == "/>") pass(endtag);
+      else pass();
+    }
+    function value(style) {
+      if (style == "xml-attribute") cont(value);
+      else pass();
+    }
+
+    return {
+      indentation: function() {return indented;},
+
+      next: function(){
+        var token = tokens.next();
+        if (token.style == "whitespace" && tokenNr == 0)
+          indented = token.value.length;
+        else
+          tokenNr++;
+        if (token.content == "\n") {
+          indented = tokenNr = 0;
+          token.indentation = computeIndentation(context);
+        }
+
+        if (token.style == "whitespace" || token.type == "xml-comment")
+          return token;
+
+        while(true){
+          consume = marked = false;
+          cc.pop()(token.style, token.content);
+          if (consume){
+            if (marked)
+              token.style = marked;
+            return token;
+          }
+        }
+      },
+
+      copy: function(){
+        var _cc = cc.concat([]), _tokenState = tokens.state, _context = context;
+        var parser = this;
+
+        return function(input){
+          cc = _cc.concat([]);
+          tokenNr = indented = 0;
+          context = _context;
+          tokens = tokenizeXML(input, _tokenState);
+          return parser;
+        };
+      }
+    };
+  }
+
+  return {
+    make: parseXML,
+    electricChars: "/",
+    configure: function(config) {
+      if (config.useHTMLKludges)
+        UseKludges = Kludges;
+      else
+        UseKludges = NoKludges;
+    }
+  };
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/select.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/select.js
new file mode 100644
index 0000000..e90c98e
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/select.js
@@ -0,0 +1,584 @@
+/* Functionality for finding, storing, and restoring selections
+ *
+ * This does not provide a generic API, just the minimal functionality
+ * required by the CodeMirror system.
+ */
+
+// Namespace object.
+var select = {};
+
+(function() {
+  select.ie_selection = document.selection && document.selection.createRangeCollection;
+
+  // Find the 'top-level' (defined as 'a direct child of the node
+  // passed as the top argument') node that the given node is
+  // contained in. Return null if the given node is not inside the top
+  // node.
+  function topLevelNodeAt(node, top) {
+    while (node && node.parentNode != top)
+      node = node.parentNode;
+    return node;
+  }
+
+  // Find the top-level node that contains the node before this one.
+  function topLevelNodeBefore(node, top) {
+    while (!node.previousSibling && node.parentNode != top)
+      node = node.parentNode;
+    return topLevelNodeAt(node.previousSibling, top);
+  }
+
+  // Used to prevent restoring a selection when we do not need to.
+  var currentSelection = null;
+
+  var fourSpaces = "\u00a0\u00a0\u00a0\u00a0";
+
+  select.snapshotChanged = function() {
+    if (currentSelection) currentSelection.changed = true;
+  };
+
+  // This is called by the code in editor.js whenever it is replacing
+  // a text node. The function sees whether the given oldNode is part
+  // of the current selection, and updates this selection if it is.
+  // Because nodes are often only partially replaced, the length of
+  // the part that gets replaced has to be taken into account -- the
+  // selection might stay in the oldNode if the newNode is smaller
+  // than the selection's offset. The offset argument is needed in
+  // case the selection does move to the new object, and the given
+  // length is not the whole length of the new node (part of it might
+  // have been used to replace another node).
+  select.snapshotReplaceNode = function(from, to, length, offset) {
+    if (!currentSelection) return;
+    currentSelection.changed = true;
+
+    function replace(point) {
+      if (from == point.node) {
+        if (length && point.offset > length) {
+          point.offset -= length;
+        }
+        else {
+          point.node = to;
+          point.offset += (offset || 0);
+        }
+      }
+    }
+    replace(currentSelection.start);
+    replace(currentSelection.end);
+  };
+
+  select.snapshotMove = function(from, to, distance, relative, ifAtStart) {
+    if (!currentSelection) return;
+    currentSelection.changed = true;
+
+    function move(point) {
+      if (from == point.node && (!ifAtStart || point.offset == 0)) {
+        point.node = to;
+        if (relative) point.offset = Math.max(0, point.offset + distance);
+        else point.offset = distance;
+      }
+    }
+    move(currentSelection.start);
+    move(currentSelection.end);
+  };
+
+  // Most functions are defined in two ways, one for the IE selection
+  // model, one for the W3C one.
+  if (select.ie_selection) {
+    function selectionNode(win, start) {
+      var range = win.document.selection.createRange();
+      range.collapse(start);
+
+      function nodeAfter(node) {
+        var found = null;
+        while (!found && node) {
+          found = node.nextSibling;
+          node = node.parentNode;
+        }
+        return nodeAtStartOf(found);
+      }
+
+      function nodeAtStartOf(node) {
+        while (node && node.firstChild) node = node.firstChild;
+        return {node: node, offset: 0};
+      }
+
+      var containing = range.parentElement();
+      if (!isAncestor(win.document.body, containing)) return null;
+      if (!containing.firstChild) return nodeAtStartOf(containing);
+
+      var working = range.duplicate();
+      working.moveToElementText(containing);
+      working.collapse(true);
+      for (var cur = containing.firstChild; cur; cur = cur.nextSibling) {
+        if (cur.nodeType == 3) {
+          var size = cur.nodeValue.length;
+          working.move("character", size);
+        }
+        else {
+          working.moveToElementText(cur);
+          working.collapse(false);
+        }
+
+        var dir = range.compareEndPoints("StartToStart", working);
+        if (dir == 0) return nodeAfter(cur);
+        if (dir == 1) continue;
+        if (cur.nodeType != 3) return nodeAtStartOf(cur);
+
+        working.setEndPoint("StartToEnd", range);
+        return {node: cur, offset: size - working.text.length};
+      }
+      return nodeAfter(containing);
+    }
+
+    select.markSelection = function(win) {
+      currentSelection = null;
+      var sel = win.document.selection;
+      if (!sel) return;
+      var start = selectionNode(win, true),
+          end = sel.createRange().text == "" ? start : selectionNode(win, false);
+      if (!start || !end) return;
+      currentSelection = {start: start, end: end, window: win, changed: false};
+    };
+
+    select.selectMarked = function() {
+      if (!currentSelection || !currentSelection.changed) return;
+
+      function makeRange(point) {
+        var range = currentSelection.window.document.body.createTextRange();
+        var node = point.node;
+        if (!node) {
+          range.moveToElementText(win.document.body);
+          range.collapse(false);
+        }
+        else if (node.nodeType == 3) {
+          range.moveToElementText(node.parentNode);
+          var offset = point.offset;
+          while (node.previousSibling) {
+            node = node.previousSibling;
+            offset += (node.innerText || "").length;
+          }
+          range.move("character", offset);
+        }
+        else {
+          range.moveToElementText(node);
+          range.collapse(true);
+        }
+        return range;
+      }
+
+      var start = makeRange(currentSelection.start), end = makeRange(currentSelection.end);
+      start.setEndPoint("StartToEnd", end);
+      start.select();
+    };
+
+    // Get the top-level node that one end of the cursor is inside or
+    // after. Note that this returns false for 'no cursor', and null
+    // for 'start of document'.
+    select.selectionTopNode = function(container, start) {
+      var selection = container.ownerDocument.selection;
+      if (!selection) return false;
+
+      var range = selection.createRange();
+      range.collapse(start);
+      var around = range.parentElement();
+      if (around && isAncestor(container, around)) {
+        // Only use this node if the selection is not at its start.
+        var range2 = range.duplicate();
+        range2.moveToElementText(around);
+        if (range.compareEndPoints("StartToStart", range2) == -1)
+          return topLevelNodeAt(around, container);
+      }
+      // Fall-back hack
+      try {range.pasteHTML("<span id='xxx-temp-xxx'></span>");}
+      catch (e) {return false;}
+
+      var temp = container.ownerDocument.getElementById("xxx-temp-xxx");
+      if (temp) {
+        var result = topLevelNodeBefore(temp, container);
+        removeElement(temp);
+        return result;
+      }
+      return false;
+    };
+
+    // Place the cursor after this.start. This is only useful when
+    // manually moving the cursor instead of restoring it to its old
+    // position.
+    select.focusAfterNode = function(node, container) {
+      var range = container.ownerDocument.body.createTextRange();
+      range.moveToElementText(node || container);
+      range.collapse(!node);
+      range.select();
+    };
+
+    select.somethingSelected = function(win) {
+      var sel = win.document.selection;
+      return sel && (sel.createRange().text != "");
+    };
+
+    function insertAtCursor(window, html) {
+      var selection = window.document.selection;
+      if (selection) {
+        var range = selection.createRange();
+        range.pasteHTML(html);
+        range.collapse(false);
+        range.select();
+      }
+    }
+
+    // Used to normalize the effect of the enter key, since browsers
+    // do widely different things when pressing enter in designMode.
+    select.insertNewlineAtCursor = function(window) {
+      insertAtCursor(window, "<br/>");
+    };
+
+    select.insertTabAtCursor = function(window) {
+      insertAtCursor(window, fourSpaces);
+    };
+
+    // Get the BR node at the start of the line on which the cursor
+    // currently is, and the offset into the line. Returns null as
+    // node if cursor is on first line.
+    select.cursorPos = function(container, start) {
+      var selection = container.ownerDocument.selection;
+      if (!selection) return null;
+
+      var topNode = select.selectionTopNode(container, start);
+      while (topNode && topNode.nodeName != "BR")
+        topNode = topNode.previousSibling;
+
+      var range = selection.createRange(), range2 = range.duplicate();
+      range.collapse(start);
+      if (topNode) {
+        range2.moveToElementText(topNode);
+        range2.collapse(false);
+      }
+      else {
+        // When nothing is selected, we can get all kinds of funky errors here.
+        try { range2.moveToElementText(container); }
+        catch (e) { return null; }
+        range2.collapse(true);
+      }
+      range.setEndPoint("StartToStart", range2);
+
+      return {node: topNode, offset: range.text.length};
+    };
+
+    select.setCursorPos = function(container, from, to) {
+      function rangeAt(pos) {
+        var range = container.ownerDocument.body.createTextRange();
+        if (!pos.node) {
+          range.moveToElementText(container);
+          range.collapse(true);
+        }
+        else {
+          range.moveToElementText(pos.node);
+          range.collapse(false);
+        }
+        range.move("character", pos.offset);
+        return range;
+      }
+
+      var range = rangeAt(from);
+      if (to && to != from)
+        range.setEndPoint("EndToEnd", rangeAt(to));
+      range.select();
+    }
+
+    // Make sure the cursor is visible.
+    select.scrollToCursor = function(container) {
+      var selection = container.ownerDocument.selection;
+      if (!selection) return null;
+      selection.createRange().scrollIntoView();
+    };
+
+    // Some hacks for storing and re-storing the selection when the editor loses and regains focus.
+    select.selectionCoords = function (win) {
+      var selection = win.document.selection;
+      if (!selection) return null;
+      var start = selection.createRange(), end = start.duplicate();
+      start.collapse(true);
+      end.collapse(false);
+
+      var body = win.document.body;
+      return {start: {x: start.boundingLeft + body.scrollLeft - 1,
+                      y: start.boundingTop + body.scrollTop},
+              end: {x: end.boundingLeft + body.scrollLeft - 1,
+                    y: end.boundingTop + body.scrollTop}};
+    };
+
+    // Restore a stored selection.
+    select.selectCoords = function(win, coords) {
+      if (!coords) return;
+
+      var range1 = win.document.body.createTextRange(), range2 = range1.duplicate();
+      // This can fail for various hard-to-handle reasons.
+      try {
+        range1.moveToPoint(coords.start.x, coords.start.y);
+        range2.moveToPoint(coords.end.x, coords.end.y);
+        range1.setEndPoint("EndToStart", range2);
+        range1.select();
+      } catch(e) {alert(e.message);}
+    };
+  }
+  // W3C model
+  else {
+    // This is used to fix an issue with getting the scroll position
+    // in Opera.
+    var opera_scroll = window.scrollX == null;
+
+    // Store start and end nodes, and offsets within these, and refer
+    // back to the selection object from those nodes, so that this
+    // object can be updated when the nodes are replaced before the
+    // selection is restored.
+    select.markSelection = function (win) {
+      var selection = win.getSelection();
+      if (!selection || selection.rangeCount == 0)
+        return (currentSelection = null);
+      var range = selection.getRangeAt(0);
+
+      currentSelection = {
+        start: {node: range.startContainer, offset: range.startOffset},
+        end: {node: range.endContainer, offset: range.endOffset},
+        window: win,
+        scrollX: opera_scroll && win.document.body.scrollLeft,
+        scrollY: opera_scroll && win.document.body.scrollTop,
+        changed: false
+      };
+
+      // We want the nodes right at the cursor, not one of their
+      // ancestors with a suitable offset. This goes down the DOM tree
+      // until a 'leaf' is reached (or is it *up* the DOM tree?).
+      function normalize(point){
+        while (point.node.nodeType != 3 && point.node.nodeName != "BR") {
+          var newNode = point.node.childNodes[point.offset] || point.node.nextSibling;
+          point.offset = 0;
+          while (!newNode && point.node.parentNode) {
+            point.node = point.node.parentNode;
+            newNode = point.node.nextSibling;
+          }
+          point.node = newNode;
+          if (!newNode)
+            break;
+        }
+      }
+
+      normalize(currentSelection.start);
+      normalize(currentSelection.end);
+    };
+
+    select.selectMarked = function () {
+      if (!currentSelection || !currentSelection.changed) return;
+      var win = currentSelection.window, range = win.document.createRange();
+
+      function setPoint(point, which) {
+        if (point.node) {
+          // Some magic to generalize the setting of the start and end
+          // of a range.
+          if (point.offset == 0)
+            range["set" + which + "Before"](point.node);
+          else
+            range["set" + which](point.node, point.offset);
+        }
+        else {
+          range.setStartAfter(win.document.body.lastChild || win.document.body);
+        }
+      }
+
+      // Have to restore the scroll position of the frame in Opera.
+      if (opera_scroll) {
+        win.document.body.scrollLeft = currentSelection.scrollX;
+        win.document.body.scrollTop = currentSelection.scrollY;
+      }
+      setPoint(currentSelection.end, "End");
+      setPoint(currentSelection.start, "Start");
+      selectRange(range, win);
+    };
+
+    // Helper for selecting a range object.
+    function selectRange(range, window) {
+      var selection = window.getSelection();
+      selection.removeAllRanges();
+      selection.addRange(range);
+    };
+    function selectionRange(window) {
+      var selection = window.getSelection();
+      if (!selection || selection.rangeCount == 0)
+        return false;
+      else
+        return selection.getRangeAt(0);
+    }
+
+    // Finding the top-level node at the cursor in the W3C is, as you
+    // can see, quite an involved process.
+    select.selectionTopNode = function(container, start) {
+      var range = selectionRange(container.ownerDocument.defaultView);
+      if (!range) return false;
+
+      var node = start ? range.startContainer : range.endContainer;
+      var offset = start ? range.startOffset : range.endOffset;
+      // Work around (yet another) bug in Opera's selection model.
+      if (window.opera && !start && range.endContainer == container && range.endOffset == range.startOffset + 1 &&
+          container.childNodes[range.startOffset] && container.childNodes[range.startOffset].nodeName == "BR")
+        offset--;
+
+      // For text nodes, we look at the node itself if the cursor is
+      // inside, or at the node before it if the cursor is at the
+      // start.
+      if (node.nodeType == 3){
+        if (offset > 0)
+          return topLevelNodeAt(node, container);
+        else
+          return topLevelNodeBefore(node, container);
+      }
+      // Occasionally, browsers will return the HTML node as
+      // selection. If the offset is 0, we take the start of the frame
+      // ('after null'), otherwise, we take the last node.
+      else if (node.nodeName == "HTML") {
+        return (offset == 1 ? null : container.lastChild);
+      }
+      // If the given node is our 'container', we just look up the
+      // correct node by using the offset.
+      else if (node == container) {
+        return (offset == 0) ? null : node.childNodes[offset - 1];
+      }
+      // In any other case, we have a regular node. If the cursor is
+      // at the end of the node, we use the node itself, if it is at
+      // the start, we use the node before it, and in any other
+      // case, we look up the child before the cursor and use that.
+      else {
+        if (offset == node.childNodes.length)
+          return topLevelNodeAt(node, container);
+        else if (offset == 0)
+          return topLevelNodeBefore(node, container);
+        else
+          return topLevelNodeAt(node.childNodes[offset - 1], container);
+      }
+    };
+
+    select.focusAfterNode = function(node, container) {
+      var win = container.ownerDocument.defaultView,
+          range = win.document.createRange();
+      range.setStartBefore(container.firstChild || container);
+      // In Opera, setting the end of a range at the end of a line
+      // (before a BR) will cause the cursor to appear on the next
+      // line, so we set the end inside of the start node when
+      // possible.
+      if (node && !node.firstChild)
+        range.setEndAfter(node);
+      else if (node)
+        range.setEnd(node, node.childNodes.length);
+      else
+        range.setEndBefore(container.firstChild || container);
+      range.collapse(false);
+      selectRange(range, win);
+    };
+
+    select.somethingSelected = function(win) {
+      var range = selectionRange(win);
+      return range && !range.collapsed;
+    };
+
+    function insertNodeAtCursor(window, node) {
+      var range = selectionRange(window);
+      if (!range) return;
+
+      range.deleteContents();
+      range.insertNode(node);
+      range.setEndAfter(node);
+      range.collapse(false);
+      selectRange(range, window);
+      return node;
+    }
+
+    select.insertNewlineAtCursor = function(window) {
+      insertNodeAtCursor(window, window.document.createElement("BR"));
+    };
+
+    select.insertTabAtCursor = function(window) {
+      insertNodeAtCursor(window, window.document.createTextNode(fourSpaces));
+    };
+
+    select.cursorPos = function(container, start) {
+      var range = selectionRange(window);
+      if (!range) return;
+
+      var topNode = select.selectionTopNode(container, start);
+      while (topNode && topNode.nodeName != "BR")
+        topNode = topNode.previousSibling;
+
+      range = range.cloneRange();
+      range.collapse(start);
+      if (topNode)
+        range.setStartAfter(topNode);
+      else
+        range.setStartBefore(container);
+      return {node: topNode, offset: range.toString().length};
+    };
+
+    select.setCursorPos = function(container, from, to) {
+      var win = container.ownerDocument.defaultView,
+          range = win.document.createRange();
+
+      function setPoint(node, offset, side) {
+        if (!node)
+          node = container.firstChild;
+        else
+          node = node.nextSibling;
+
+        if (!node)
+          return;
+
+        if (offset == 0) {
+          range["set" + side + "Before"](node);
+          return true;
+        }
+
+        var backlog = []
+        function decompose(node) {
+          if (node.nodeType == 3)
+            backlog.push(node);
+          else
+            forEach(node.childNodes, decompose);
+        }
+        while (true) {
+          while (node && !backlog.length) {
+            decompose(node);
+            node = node.nextSibling;
+          }
+          var cur = backlog.shift();
+          if (!cur) return false;
+
+          var length = cur.nodeValue.length;
+          if (length >= offset) {
+            range["set" + side](cur, offset);
+            return true;
+          }
+          offset -= length;
+        }
+      }
+
+      to = to || from;
+      if (setPoint(to.node, to.offset, "End") && setPoint(from.node, from.offset, "Start"))
+        selectRange(range, win);
+    };
+
+    select.scrollToCursor = function(container) {
+      var body = container.ownerDocument.body, win = container.ownerDocument.defaultView;
+      var element = select.selectionTopNode(container, true) || container.firstChild;
+
+      // In Opera, BR elements *always* have a scrollTop property of zero. Go Opera.
+      while (element && !element.offsetTop)
+        element = element.previousSibling;
+
+      var y = 0, pos = element;
+      while (pos && pos.offsetParent) {
+        y += pos.offsetTop;
+        pos = pos.offsetParent;
+      }
+
+      var screen_y = y - body.scrollTop;
+      if (screen_y < 0 || screen_y > win.innerHeight - 10)
+        win.scrollTo(0, y);
+    };
+  }
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/stringstream.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/stringstream.js
new file mode 100644
index 0000000..e320f8b
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/stringstream.js
@@ -0,0 +1,131 @@
+/* String streams are the things fed to parsers (which can feed them
+ * to a tokenizer if they want). They provide peek and next methods
+ * for looking at the current character (next 'consumes' this
+ * character, peek does not), and a get method for retrieving all the
+ * text that was consumed since the last time get was called.
+ *
+ * An easy mistake to make is to let a StopIteration exception finish
+ * the token stream while there are still characters pending in the
+ * string stream (hitting the end of the buffer while parsing a
+ * token). To make it easier to detect such errors, the strings throw
+ * an exception when this happens.
+ */
+
+// Make a string stream out of an iterator that returns strings. This
+// is applied to the result of traverseDOM (see codemirror.js), and
+// the resulting stream is fed to the parser.
+window.stringStream = function(source){
+  source = iter(source);
+  // String that's currently being iterated over.
+  var current = "";
+  // Position in that string.
+  var pos = 0;
+  // Accumulator for strings that have been iterated over but not
+  // get()-ed yet.
+  var accum = "";
+  // Make sure there are more characters ready, or throw
+  // StopIteration.
+  function ensureChars() {
+    while (pos == current.length) {
+      accum += current;
+      current = ""; // In case source.next() throws
+      pos = 0;
+      try {current = source.next();}
+      catch (e) {
+        if (e != StopIteration) throw e;
+        else return false;
+      }
+    }
+    return true;
+  }
+
+  return {
+    // Return the next character in the stream.
+    peek: function() {
+      if (!ensureChars()) return null;
+      return current.charAt(pos);
+    },
+    // Get the next character, throw StopIteration if at end, check
+    // for unused content.
+    next: function() {
+      if (!ensureChars()) {
+        if (accum.length > 0)
+          throw "End of stringstream reached without emptying buffer ('" + accum + "').";
+        else
+          throw StopIteration;
+      }
+      return current.charAt(pos++);
+    },
+    // Return the characters iterated over since the last call to
+    // .get().
+    get: function() {
+      var temp = accum;
+      accum = "";
+      if (pos > 0){
+        temp += current.slice(0, pos);
+        current = current.slice(pos);
+        pos = 0;
+      }
+      return temp;
+    },
+    // Push a string back into the stream.
+    push: function(str) {
+      current = current.slice(0, pos) + str + current.slice(pos);
+    },
+    lookAhead: function(str, consume, skipSpaces, caseInsensitive) {
+      function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
+      str = cased(str);
+      var found = false;
+
+      var _accum = accum, _pos = pos;
+      if (skipSpaces) this.nextWhile(matcher(/[\s\u00a0]/));
+
+      while (true) {
+        var end = pos + str.length, left = current.length - pos;
+        if (end <= current.length) {
+          found = str == cased(current.slice(pos, end));
+          pos = end;
+          break;
+        }
+        else if (str.slice(0, left) == cased(current.slice(pos))) {
+          accum += current; current = "";
+          try {current = source.next();}
+          catch (e) {break;}
+          pos = 0;
+          str = str.slice(left);
+        }
+        else {
+          break;
+        }
+      }
+
+      if (!(found && consume)) {
+        current = accum.slice(_accum.length) + current;
+        pos = _pos;
+        accum = _accum;
+      }
+
+      return found;
+    },
+
+    // Utils built on top of the above
+    more: function() {
+      return this.peek() !== null;
+    },
+    applies: function(test) {
+      var next = this.peek();
+      return (next !== null && test(next));
+    },
+    nextWhile: function(test) {
+      while (this.applies(test))
+        this.next();
+    },
+    equals: function(ch) {
+      return ch === this.peek();
+    },
+    endOfLine: function() {
+      var next = this.peek();
+      return next == null || next == "\n";
+    }
+  };
+};
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenize.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenize.js
new file mode 100644
index 0000000..b0c9545
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenize.js
@@ -0,0 +1,57 @@
+// A framework for simple tokenizers. Takes care of newlines and
+// white-space, and of getting the text from the source stream into
+// the token object. A state is a function of two arguments -- a
+// string stream and a setState function. The second can be used to
+// change the tokenizer's state, and can be ignored for stateless
+// tokenizers. This function should advance the stream over a token
+// and return a string or object containing information about the next
+// token, or null to pass and have the (new) state be called to finish
+// the token. When a string is given, it is wrapped in a {style, type}
+// object. In the resulting object, the characters consumed are stored
+// under the content property. Any whitespace following them is also
+// automatically consumed, and added to the value property. (Thus,
+// content is the actual meaningful part of the token, while value
+// contains all the text it spans.)
+
+function tokenizer(source, state) {
+  // Newlines are always a separate token.
+  function isWhiteSpace(ch) {
+    // The messy regexp is because IE's regexp matcher is of the
+    // opinion that non-breaking spaces are no whitespace.
+    return ch != "\n" && /^[\s\u00a0]*$/.test(ch);
+  }
+
+  var tokenizer = {
+    state: state,
+
+    take: function(type) {
+      if (typeof(type) == "string")
+        type = {style: type, type: type};
+
+      type.content = (type.content || "") + source.get();
+      if (!/\n$/.test(type.content))
+        source.nextWhile(isWhiteSpace);
+      type.value = type.content + source.get();
+      return type;
+    },
+
+    next: function () {
+      if (!source.more()) throw StopIteration;
+
+      var type;
+      if (source.equals("\n")) {
+        source.next();
+        return this.take("whitespace");
+      }
+
+      if (source.applies(isWhiteSpace))
+        type = "whitespace";
+      else
+        while (!type)
+          type = this.state(source, function(s) {tokenizer.state = s;});
+
+      return this.take(type);
+    }
+  };
+  return tokenizer;
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenizejavascript.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenizejavascript.js
new file mode 100644
index 0000000..c60c6cb
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenizejavascript.js
@@ -0,0 +1,176 @@
+/* Tokenizer for JavaScript code */
+
+var tokenizeJavaScript = (function() {
+  // Advance the stream until the given character (not preceded by a
+  // backslash) is encountered, or the end of the line is reached.
+  function nextUntilUnescaped(source, end) {
+    var escaped = false;
+    var next;
+    while (!source.endOfLine()) {
+      var next = source.next();
+      if (next == end && !escaped)
+        return false;
+      escaped = !escaped && next == "\\";
+    }
+    return escaped;
+  }
+
+  // A map of JavaScript's keywords. The a/b/c keyword distinction is
+  // very rough, but it gives the parser enough information to parse
+  // correct code correctly (we don't care that much how we parse
+  // incorrect code). The style information included in these objects
+  // is used by the highlighter to pick the correct CSS style for a
+  // token.
+  var keywords = function(){
+    function result(type, style){
+      return {type: type, style: style};
+    }
+    // keywords that take a parenthised expression, and then a
+    // statement (if)
+    var keywordA = result("keyword a", "js-keyword");
+    // keywords that take just a statement (else)
+    var keywordB = result("keyword b", "js-keyword");
+    // keywords that optionally take an expression, and form a
+    // statement (return)
+    var keywordC = result("keyword c", "js-keyword");
+    var operator = result("operator", "js-keyword");
+    var atom = result("atom", "js-atom");
+    return {
+      "if": keywordA, "switch": keywordA, "while": keywordA, "with": keywordA,
+      "else": keywordB, "do": keywordB, "try": keywordB, "finally": keywordB,
+      "return": keywordC, "break": keywordC, "continue": keywordC, "new": keywordC, "delete": keywordC, "throw": keywordC,
+      "in": operator, "typeof": operator, "instanceof": operator,
+      "var": result("var", "js-keyword"), "function": result("function", "js-keyword"), "catch": result("catch", "js-keyword"),
+      "for": result("for", "js-keyword"),
+      "case": result("case", "js-keyword"), "default": result("default", "js-keyword"),
+      "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom
+    };
+  }();
+
+  // Some helper regexp matchers.
+  var isOperatorChar = matcher(/[+\-*&%\/=<>!?|]/);
+  var isDigit = matcher(/[0-9]/);
+  var isHexDigit = matcher(/[0-9A-Fa-f]/);
+  var isWordChar = matcher(/[\w\$_]/);
+
+  // Wrapper around jsToken that helps maintain parser state (whether
+  // we are inside of a multi-line comment and whether the next token
+  // could be a regular expression).
+  function jsTokenState(inside, regexp) {
+    return function(source, setState) {
+      var newInside = inside;
+      var type = jsToken(inside, regexp, source, function(c) {newInside = c;});
+      var newRegexp = type.type == "operator" || type.type == "keyword c" || type.type.match(/^[\[{}\(,;:]$/);
+      if (newRegexp != regexp || newInside != inside)
+        setState(jsTokenState(newInside, newRegexp));
+      return type;
+    };
+  }
+
+  // The token reader, inteded to be used by the tokenizer from
+  // tokenize.js (through jsTokenState). Advances the source stream
+  // over a token, and returns an object containing the type and style
+  // of that token.
+  function jsToken(inside, regexp, source, setInside) {
+    function readHexNumber(){
+      source.next(); // skip the 'x'
+      source.nextWhile(isHexDigit);
+      return {type: "number", style: "js-atom"};
+    }
+
+    function readNumber() {
+      source.nextWhile(isDigit);
+      if (source.equals(".")){
+        source.next();
+        source.nextWhile(isDigit);
+      }
+      if (source.equals("e") || source.equals("E")){
+        source.next();
+        if (source.equals("-"))
+          source.next();
+        source.nextWhile(isDigit);
+      }
+      return {type: "number", style: "js-atom"};
+    }
+    // Read a word, look it up in keywords. If not found, it is a
+    // variable, otherwise it is a keyword of the type found.
+    function readWord() {
+      source.nextWhile(isWordChar);
+      var word = source.get();
+      var known = keywords.hasOwnProperty(word) && keywords.propertyIsEnumerable(word) && keywords[word];
+      return known ? {type: known.type, style: known.style, content: word} :
+      {type: "variable", style: "js-variable", content: word};
+    }
+    function readRegexp() {
+      nextUntilUnescaped(source, "/");
+      source.nextWhile(matcher(/[gi]/));
+      return {type: "regexp", style: "js-string"};
+    }
+    // Mutli-line comments are tricky. We want to return the newlines
+    // embedded in them as regular newline tokens, and then continue
+    // returning a comment token for every line of the comment. So
+    // some state has to be saved (inside) to indicate whether we are
+    // inside a /* */ sequence.
+    function readMultilineComment(start){
+      var newInside = "/*";
+      var maybeEnd = (start == "*");
+      while (true) {
+        if (source.endOfLine())
+          break;
+        var next = source.next();
+        if (next == "/" && maybeEnd){
+          newInside = null;
+          break;
+        }
+        maybeEnd = (next == "*");
+      }
+      setInside(newInside);
+      return {type: "comment", style: "js-comment"};
+    }
+    function readOperator() {
+      source.nextWhile(isOperatorChar);
+      return {type: "operator", style: "js-operator"};
+    }
+    function readString(quote) {
+      var endBackSlash = nextUntilUnescaped(source, quote);
+      setInside(endBackSlash ? quote : null);
+      return {type: "string", style: "js-string"};
+    }
+
+    // Fetch the next token. Dispatches on first character in the
+    // stream, or first two characters when the first is a slash.
+    if (inside == "\"" || inside == "'")
+      return readString(inside);
+    var ch = source.next();
+    if (inside == "/*")
+      return readMultilineComment(ch);
+    else if (ch == "\"" || ch == "'")
+      return readString(ch);
+    // with punctuation, the type of the token is the symbol itself
+    else if (/[\[\]{}\(\),;\:\.]/.test(ch))
+      return {type: ch, style: "js-punctuation"};
+    else if (ch == "0" && (source.equals("x") || source.equals("X")))
+      return readHexNumber();
+    else if (isDigit(ch))
+      return readNumber();
+    else if (ch == "/"){
+      if (source.equals("*"))
+      { source.next(); return readMultilineComment(ch); }
+      else if (source.equals("/"))
+      { nextUntilUnescaped(source, null); return {type: "comment", style: "js-comment"};}
+      else if (regexp)
+        return readRegexp();
+      else
+        return readOperator();
+    }
+    else if (isOperatorChar(ch))
+      return readOperator();
+    else
+      return readWord();
+  }
+
+  // The external interface to the tokenizer.
+  return function(source, startState) {
+    return tokenizer(source, startState || jsTokenState(false, true));
+  };
+})();
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/undo.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/undo.js
new file mode 100644
index 0000000..f7990ee
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/undo.js
@@ -0,0 +1,388 @@
+/**
+ * Storage and control for undo information within a CodeMirror
+ * editor. 'Why on earth is such a complicated mess required for
+ * that?', I hear you ask. The goal, in implementing this, was to make
+ * the complexity of storing and reverting undo information depend
+ * only on the size of the edited or restored content, not on the size
+ * of the whole document. This makes it necessary to use a kind of
+ * 'diff' system, which, when applied to a DOM tree, causes some
+ * complexity and hackery.
+ *
+ * In short, the editor 'touches' BR elements as it parses them, and
+ * the History stores these. When nothing is touched in commitDelay
+ * milliseconds, the changes are committed: It goes over all touched
+ * nodes, throws out the ones that did not change since last commit or
+ * are no longer in the document, and assembles the rest into zero or
+ * more 'chains' -- arrays of adjacent lines. Links back to these
+ * chains are added to the BR nodes, while the chain that previously
+ * spanned these nodes is added to the undo history. Undoing a change
+ * means taking such a chain off the undo history, restoring its
+ * content (text is saved per line) and linking it back into the
+ * document.
+ */
+
+// A history object needs to know about the DOM container holding the
+// document, the maximum amount of undo levels it should store, the
+// delay (of no input) after which it commits a set of changes, and,
+// unfortunately, the 'parent' window -- a window that is not in
+// designMode, and on which setTimeout works in every browser.
+function History(container, maxDepth, commitDelay, editor, onChange) {
+  this.container = container;
+  this.maxDepth = maxDepth; this.commitDelay = commitDelay;
+  this.editor = editor; this.parent = editor.parent;
+  this.onChange = onChange;
+  // This line object represents the initial, empty editor.
+  var initial = {text: "", from: null, to: null};
+  // As the borders between lines are represented by BR elements, the
+  // start of the first line and the end of the last one are
+  // represented by null. Since you can not store any properties
+  // (links to line objects) in null, these properties are used in
+  // those cases.
+  this.first = initial; this.last = initial;
+  // Similarly, a 'historyTouched' property is added to the BR in
+  // front of lines that have already been touched, and 'firstTouched'
+  // is used for the first line.
+  this.firstTouched = false;
+  // History is the set of committed changes, touched is the set of
+  // nodes touched since the last commit.
+  this.history = []; this.redoHistory = []; this.touched = [];
+}
+
+History.prototype = {
+  // Schedule a commit (if no other touches come in for commitDelay
+  // milliseconds).
+  scheduleCommit: function() {
+    this.parent.clearTimeout(this.commitTimeout);
+    this.commitTimeout = this.parent.setTimeout(method(this, "tryCommit"), this.commitDelay);
+  },
+
+  // Mark a node as touched. Null is a valid argument.
+  touch: function(node) {
+    this.setTouched(node);
+    this.scheduleCommit();
+  },
+
+  // Undo the last change.
+  undo: function() {
+    // Make sure pending changes have been committed.
+    this.commit();
+
+    if (this.history.length) {
+      // Take the top diff from the history, apply it, and store its
+      // shadow in the redo history.
+      this.redoHistory.push(this.updateTo(this.history.pop(), "applyChain"));
+      if (this.onChange) this.onChange();
+    }
+  },
+
+  // Redo the last undone change.
+  redo: function() {
+    this.commit();
+    if (this.redoHistory.length) {
+      // The inverse of undo, basically.
+      this.addUndoLevel(this.updateTo(this.redoHistory.pop(), "applyChain"));
+      if (this.onChange) this.onChange();
+    }
+  },
+
+  // Push a changeset into the document.
+  push: function(from, to, lines) {
+    var chain = [];
+    for (var i = 0; i < lines.length; i++) {
+      var end = (i == lines.length - 1) ? to : this.container.ownerDocument.createElement("BR");
+      chain.push({from: from, to: end, text: lines[i]});
+      from = end;
+    }
+    this.pushChains([chain], from == null && to == null);
+  },
+
+  pushChains: function(chains, doNotHighlight) {
+    this.commit(doNotHighlight);
+    this.addUndoLevel(this.updateTo(chains, "applyChain"));
+    this.redoHistory = [];
+  },
+
+  // Clear the undo history, make the current document the start
+  // position.
+  reset: function() {
+    this.history = []; this.redoHistory = [];
+  },
+
+  textAfter: function(br) {
+    return this.after(br).text;
+  },
+
+  nodeAfter: function(br) {
+    return this.after(br).to;
+  },
+
+  nodeBefore: function(br) {
+    return this.before(br).from;
+  },
+
+  // Commit unless there are pending dirty nodes.
+  tryCommit: function() {
+    if (this.editor.highlightDirty()) this.commit();
+    else this.scheduleCommit();
+  },
+
+  // Check whether the touched nodes hold any changes, if so, commit
+  // them.
+  commit: function(doNotHighlight) {
+    this.parent.clearTimeout(this.commitTimeout);
+    // Make sure there are no pending dirty nodes.
+    if (!doNotHighlight) this.editor.highlightDirty(true);
+    // Build set of chains.
+    var chains = this.touchedChains(), self = this;
+
+    if (chains.length) {
+      this.addUndoLevel(this.updateTo(chains, "linkChain"));
+      this.redoHistory = [];
+      if (this.onChange) this.onChange();
+    }
+  },
+
+  // [ end of public interface ]
+
+  // Update the document with a given set of chains, return its
+  // shadow. updateFunc should be "applyChain" or "linkChain". In the
+  // second case, the chains are taken to correspond the the current
+  // document, and only the state of the line data is updated. In the
+  // first case, the content of the chains is also pushed iinto the
+  // document.
+  updateTo: function(chains, updateFunc) {
+    var shadows = [], dirty = [];
+    for (var i = 0; i < chains.length; i++) {
+      shadows.push(this.shadowChain(chains[i]));
+      dirty.push(this[updateFunc](chains[i]));
+    }
+    if (updateFunc == "applyChain")
+      this.notifyDirty(dirty);
+    return shadows;
+  },
+
+  // Notify the editor that some nodes have changed.
+  notifyDirty: function(nodes) {
+    forEach(nodes, method(this.editor, "addDirtyNode"))
+    this.editor.scheduleHighlight();
+  },
+
+  // Link a chain into the DOM nodes (or the first/last links for null
+  // nodes).
+  linkChain: function(chain) {
+    for (var i = 0; i < chain.length; i++) {
+      var line = chain[i];
+      if (line.from) line.from.historyAfter = line;
+      else this.first = line;
+      if (line.to) line.to.historyBefore = line;
+      else this.last = line;
+    }
+  },
+
+  // Get the line object after/before a given node.
+  after: function(node) {
+    return node ? node.historyAfter : this.first;
+  },
+  before: function(node) {
+    return node ? node.historyBefore : this.last;
+  },
+
+  // Mark a node as touched if it has not already been marked.
+  setTouched: function(node) {
+    if (node) {
+      if (!node.historyTouched) {
+        this.touched.push(node);
+        node.historyTouched = true;
+      }
+    }
+    else {
+      this.firstTouched = true;
+    }
+  },
+
+  // Store a new set of undo info, throw away info if there is more of
+  // it than allowed.
+  addUndoLevel: function(diffs) {
+    this.history.push(diffs);
+    if (this.history.length > this.maxDepth)
+      this.history.shift();
+  },
+
+  // Build chains from a set of touched nodes.
+  touchedChains: function() {
+    var self = this;
+    // Compare two strings, treating nbsps as spaces.
+    function compareText(a, b) {
+      return a.replace(/\u00a0/g, " ") == b.replace(/\u00a0/g, " ");
+    }
+
+    // The temp system is a crummy hack to speed up determining
+    // whether a (currently touched) node has a line object associated
+    // with it. nullTemp is used to store the object for the first
+    // line, other nodes get it stored in their historyTemp property.
+    var nullTemp = null;
+    function temp(node) {return node ? node.historyTemp : nullTemp;}
+    function setTemp(node, line) {
+      if (node) node.historyTemp = line;
+      else nullTemp = line;
+    }
+
+    function buildLine(node) {
+      var text = [];
+      for (var cur = node ? node.nextSibling : self.container.firstChild;
+           cur && cur.nodeName != "BR"; cur = cur.nextSibling)
+        if (cur.currentText) text.push(cur.currentText);
+      return {from: node, to: cur, text: text.join("")};
+    }
+
+    // Filter out unchanged lines and nodes that are no longer in the
+    // document. Build up line objects for remaining nodes.
+    var lines = [];
+    if (self.firstTouched) self.touched.push(null);
+    forEach(self.touched, function(node) {
+      if (node && node.parentNode != self.container) return;
+
+      if (node) node.historyTouched = false;
+      else self.firstTouched = false;
+
+      var line = buildLine(node), shadow = self.after(node);
+      if (!shadow || !compareText(shadow.text, line.text) || shadow.to != line.to) {
+        lines.push(line);
+        setTemp(node, line);
+      }
+    });
+
+    // Get the BR element after/before the given node.
+    function nextBR(node, dir) {
+      var link = dir + "Sibling", search = node[link];
+      while (search && search.nodeName != "BR")
+        search = search[link];
+      return search;
+    }
+
+    // Assemble line objects into chains by scanning the DOM tree
+    // around them.
+    var chains = []; self.touched = [];
+    forEach(lines, function(line) {
+      // Note that this makes the loop skip line objects that have
+      // been pulled into chains by lines before them.
+      if (!temp(line.from)) return;
+
+      var chain = [], curNode = line.from, safe = true;
+      // Put any line objects (referred to by temp info) before this
+      // one on the front of the array.
+      while (true) {
+        var curLine = temp(curNode);
+        if (!curLine) {
+          if (safe) break;
+          else curLine = buildLine(curNode);
+        }
+        chain.unshift(curLine);
+        setTemp(curNode, null);
+        if (!curNode) break;
+        safe = self.after(curNode);
+        curNode = nextBR(curNode, "previous");
+      }
+      curNode = line.to; safe = self.before(line.from);
+      // Add lines after this one at end of array.
+      while (true) {
+        if (!curNode) break;
+        var curLine = temp(curNode);
+        if (!curLine) {
+          if (safe) break;
+          else curLine = buildLine(curNode);
+        }
+        chain.push(curLine);
+        setTemp(curNode, null);
+        safe = self.before(curNode);
+        curNode = nextBR(curNode, "next");
+      }
+      chains.push(chain);
+    });
+
+    return chains;
+  },
+
+  // Find the 'shadow' of a given chain by following the links in the
+  // DOM nodes at its start and end.
+  shadowChain: function(chain) {
+    var shadows = [], next = this.after(chain[0].from), end = chain[chain.length - 1].to;
+    while (true) {
+      shadows.push(next);
+      var nextNode = next.to;
+      if (!nextNode || nextNode == end)
+        break;
+      else
+        next = nextNode.historyAfter || this.before(end);
+      // (The this.before(end) is a hack -- FF sometimes removes
+      // properties from BR nodes, in which case the best we can hope
+      // for is to not break.)
+    }
+    return shadows;
+  },
+
+  // Update the DOM tree to contain the lines specified in a given
+  // chain, link this chain into the DOM nodes.
+  applyChain: function(chain) {
+    // Some attempt is made to prevent the cursor from jumping
+    // randomly when an undo or redo happens. It still behaves a bit
+    // strange sometimes.
+    var cursor = select.cursorPos(this.container, false), self = this;
+
+    // Remove all nodes in the DOM tree between from and to (null for
+    // start/end of container).
+    function removeRange(from, to) {
+      var pos = from ? from.nextSibling : self.container.firstChild;
+      while (pos != to) {
+        var temp = pos.nextSibling;
+        removeElement(pos);
+        pos = temp;
+      }
+    }
+
+    var start = chain[0].from, end = chain[chain.length - 1].to;
+    // Clear the space where this change has to be made.
+    removeRange(start, end);
+
+    // Build a function that will insert nodes before the end node of
+    // this chain.
+    var insert = end ?
+      function(node) {self.container.insertBefore(node, end);}
+    : function(node) {self.container.appendChild(node);};
+
+    // Insert the content specified by the chain into the DOM tree.
+    for (var i = 0; i < chain.length; i++) {
+      var line = chain[i];
+      // The start and end of the space are already correct, but BR
+      // tags inside it have to be put back.
+      if (i > 0)
+        insert(line.from);
+      // Add the text.
+      var node = makePartSpan(splitSpaces(line.text), this.container.ownerDocument);
+      insert(node);
+      // See if the cursor was on this line. Put it back, adjusting
+      // for changed line length, if it was.
+      if (cursor && cursor.node == line.from) {
+        var cursordiff = 0;
+        var prev = this.after(line.from);
+        if (prev && i == chain.length - 1) {
+          // Only adjust if the cursor is after the unchanged part of
+          // the line.
+          for (var match = 0; match < cursor.offset &&
+               line.text.charAt(match) == prev.text.charAt(match); match++);
+          if (cursor.offset > match)
+            cursordiff = line.text.length - prev.text.length;
+        }
+        select.setCursorPos(this.container, {node: line.from, offset: Math.max(0, cursor.offset + cursordiff)});
+      }
+      // Cursor was in removed line, this is last new line.
+      else if (cursor && (i == chain.length - 1) && cursor.node && cursor.node.parentNode != this.container) {
+        select.setCursorPos(this.container, {node: line.from, offset: line.text.length});
+      }
+    }
+
+    // Anchor the chain in the DOM tree.
+    this.linkChain(chain);
+    return start;
+  }
+};
diff --git a/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/util.js b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/util.js
new file mode 100644
index 0000000..ba2e3d4
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/util.js
@@ -0,0 +1,123 @@
+/* A few useful utility functions. */
+
+// Capture a method on an object.
+function method(obj, name) {
+  return function() {obj[name].apply(obj, arguments);};
+}
+
+// The value used to signal the end of a sequence in iterators.
+var StopIteration = {toString: function() {return "StopIteration"}};
+
+// Checks whether the argument is an iterator or a regular sequence,
+// turns it into an iterator.
+function iter(seq) {
+  var i = 0;
+  if (seq.next) return seq;
+  else return {
+    next: function() {
+      if (i >= seq.length) throw StopIteration;
+      else return seq[i++];
+    }
+  };
+}
+
+// Apply a function to each element in a sequence.
+function forEach(iter, f) {
+  if (iter.next) {
+    try {while (true) f(iter.next());}
+    catch (e) {if (e != StopIteration) throw e;}
+  }
+  else {
+    for (var i = 0; i < iter.length; i++)
+      f(iter[i]);
+  }
+}
+
+// Map a function over a sequence, producing an array of results.
+function map(iter, f) {
+  var accum = [];
+  forEach(iter, function(val) {accum.push(f(val));});
+  return accum;
+}
+
+// Create a predicate function that tests a string againsts a given
+// regular expression.
+function matcher(regexp){
+  return function(value){return regexp.test(value);};
+}
+
+// Test whether a DOM node has a certain CSS class. Much faster than
+// the MochiKit equivalent, for some reason.
+function hasClass(element, className){
+  var classes = element.className;
+  return classes && new RegExp("(^| )" + className + "($| )").test(classes);
+}
+
+// Insert a DOM node after another node.
+function insertAfter(newNode, oldNode) {
+  var parent = oldNode.parentNode;
+  parent.insertBefore(newNode, oldNode.nextSibling);
+  return newNode;
+}
+
+function removeElement(node) {
+  if (node.parentNode)
+    node.parentNode.removeChild(node);
+}
+
+function clearElement(node) {
+  while (node.firstChild)
+    node.removeChild(node.firstChild);
+}
+
+// Check whether a node is contained in another one.
+function isAncestor(node, child) {
+  while (child = child.parentNode) {
+    if (node == child)
+      return true;
+  }
+  return false;
+}
+
+// The non-breaking space character.
+var nbsp = "\u00a0";
+var matching = {"{": "}", "[": "]", "(": ")",
+                "}": "{", "]": "[", ")": "("};
+
+// Standardize a few unportable event properties.
+function normalizeEvent(event) {
+  if (!event.stopPropagation) {
+    event.stopPropagation = function() {this.cancelBubble = true;};
+    event.preventDefault = function() {this.returnValue = false;};
+  }
+  if (!event.stop) {
+    event.stop = function() {
+      this.stopPropagation();
+      this.preventDefault();
+    };
+  }
+
+  if (event.type == "keypress") {
+    if (event.charCode === 0 || event.charCode == undefined)
+      event.code = event.keyCode;
+    else
+      event.code = event.charCode;
+    event.character = String.fromCharCode(event.code);
+  }
+  return event;
+}
+
+// Portably register event handlers.
+function addEventHandler(node, type, handler, removeFunc) {
+  function wrapHandler(event) {
+    handler(normalizeEvent(event || window.event));
+  }
+  if (typeof node.addEventListener == "function") {
+    node.addEventListener(type, wrapHandler, false);
+    if (removeFunc) return function() {node.removeEventListener(type, wrapHandler, false);};
+  }
+  else {
+    node.attachEvent("on" + type, wrapHandler);
+    if (removeFunc) return function() {node.detachEvent("on" + type, wrapHandler);};
+  }
+}
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref
index 4fc1305..470a364 100644
--- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/cataloguing.pref
@@ -12,6 +12,13 @@ Cataloging:
                   yes: "Don't display"
                   no: Display
             - descriptions of fields and subfields in the MARC editor.
+        -
+            - Use a
+            - pref: MARCEditor
+              choices:
+                  normal: "guided"
+                  text: "textual"
+            - editor for MARC records.
     Spine Labels:
         -
             - When using the quick spine label printer,
diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio-text.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio-text.tt
new file mode 100644
index 0000000..6acb6ba
--- /dev/null
+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio-text.tt
@@ -0,0 +1,155 @@
+[% INCLUDE 'doc-head-open.inc' %]
+<title>Koha &rsaquo; Cataloging &rsaquo; [% IF ( biblionumber ) %]Editing [% title |html %] (Record Number [% biblionumber %])[% ELSE %]Add MARC Record[% END %]</title>
+[% INCLUDE 'doc-head-close.inc' %]
+<script type="text/javascript" src="[% themelang %]/lib/yui/plugins/bubbling-min.js"></script>
+<link rel="stylesheet" type="text/css" href="[% themelang %]/css/humanmsg.css" />
+<script src="[% themelang %]/lib/jquery/plugins/humanmsg.js" type="text/javascript"></script>
+<style type="text/css">
+	.controls {clear: both}
+	#scratchpad {float: right; color: #888; display: none; width: 47%}
+	#record {float: left; width: 47%}
+	#close-scratchpad {display: none}
+</style>
+</head>
+<body>
+<div id="yui-cms-loading">
+      <div id="yui-cms-float">
+          Loading, please wait...
+      </div>
+  </div>
+<script type="text/javascript" src="[% themelang %]/lib/yui/plugins/loading-min.js"></script>
+<script type="text/javascript">
+//<![CDATA[
+(function() {
+	// configuring the loading mask
+	YAHOO.widget.Loading.config({
+		opacity: 0.8
+	});
+})();
+//]]>
+</script>
+[% INCLUDE 'header.inc' %]
+<div id="breadcrumbs"><a href="/cgi-bin/koha/mainpage.pl">Home</a> &rsaquo; <a href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a>  &rsaquo; [% IF ( biblionumber ) %]Editing <em>[% title |html %]</em> (Record Number [% biblionumber %])[% ELSE %]Add MARC Record[% END %]</div>
+
+<div id="doc" class="yui-t7">
+
+<div id="bd">
+        <div id="yui-main">
+        <div class="yui-g">
+
+
+
+<h1>[% IF ( biblionumber ) %]Editing <em>[% title |html %]</em> (Record Number [% biblionumber %])</h1>[% ELSE %]Add MARC Record</h1>[% END %]
+
+[% UNLESS ( number ) %]
+    <!-- show duplicate warning on tab 0 only -->
+        [% IF ( duplicatebiblionumber ) %]
+                    <div class="dialog alert">
+                        <h4>Duplicate Record suspected</h4>
+                        <p>Is this a duplicate of <a href="/cgi-bin/koha/catalogue/MARCdetail.pl?biblionumber=[% duplicatebiblionumber %]" onclick="openWindow('../MARCdetail.pl?biblionumber=[% duplicatebiblionumber %]&amp;popup=1', 'Duplicate biblio'; return false;)">[% duplicatetitle %]</a>?</p>
+                        <form action="/cgi-bin/koha/cataloguing/additem.pl" method="get">
+                            <input type="hidden" name="biblionumber" value="[% duplicatebiblionumber %]" />
+                            <input type="submit" class="edit" value="Yes: Edit existing items" />
+                        </form>
+                        <form action="/cgi-bin/koha/cataloguing/addbiblio-text.pl" method="get">
+                            <input type="submit" class="save" onclick="addbiblio.not_duplicate(); return false;" value="No: Save as New Record" />
+                        </form>
+                    </div>
+        [% END %]
+    [% END %]
+
+[% IF ( done ) %]
+    <script type="text/javascript">
+        opener.document.forms['f'].biblionumber.value=[% biblionumber %];
+        opener.document.forms['f'].title.value='[% title |html %]';
+        window.close();
+    </script>
+[% ELSE %]
+    <form method="post" name="f" id="f" action="/cgi-bin/koha/cataloguing/addbiblio-text.pl" onsubmit="addbiblio.submit(); return false">
+	<input type="hidden" value="0" id="confirm_not_duplicate" name="confirm_not_duplicate" />
+[% END %]
+
+<div id="toolbar">
+
+<script type="text/javascript">
+	//<![CDATA[
+
+	// prepare DOM for YUI Toolbar
+
+	 $(document).ready(function() {
+		$("#z3950searchc").empty();
+	    yuiToolbar();
+	 });
+
+	// YUI Toolbar Functions
+
+	function yuiToolbar() {
+	    new YAHOO.widget.Button("addbiblio");
+		new YAHOO.widget.Button({
+                                            id: "z3950search",
+                                            type: "button",
+                                            label: _("z39.50 Search"),
+                                            container: "z3950searchc",
+											onclick: {fn:function(){addbiblio.z3950_search()}}
+                                        });
+	}
+
+	//]]>
+	</script>
+
+		<ul class="toolbar">
+			<li><input id="addbiblio" type="submit" value="Save" /></li>
+			<li id="z3950searchc"><input type="button" id="z3950search" value="z39.50 Search" onclick="PopupZ3950(); return false;" /></li>
+			<li id="changeframework"><label for="Frameworks">Change framework: </label>
+			<select name="Frameworks" id="Frameworks" onchange="Changefwk(this);">
+			                <option value="">Default</option>
+							[% FOREACH frameworkcodeloo IN frameworkcodeloop %]
+                                <option value="[% frameworkcodeloo.value %]" [% frameworkcodeloo.selected %]>
+					             [% frameworkcodeloo.frameworktext %]
+                                 </option>
+					        [% END %]
+			</select>
+<input type="hidden" name="op" value="addbiblio" /></li>
+		</ul>
+</div>
+
+[% IF ( popup ) %]
+        <input type="hidden" name="mode" value="popup" />
+[% END %]
+        <input type="hidden" name="frameworkcode" value="[% frameworkcode %]" />
+        <input type="hidden" name="biblionumber" value="[% biblionumber %]" />
+        <input type="hidden" name="breedingid" value="[% breedingid %]" />
+		<p>
+		<label for="itemtypes">Insert Item Type Code for: </label>
+		<select id="itemtypes">
+			[% FOREACH itemtype IN itemtypes %]
+			<option value="[% itemtype.value %]">[% itemtype.description %]</option>
+			[% END %]
+		</select>
+		<button id="insert-itemtype">Insert</button>
+		</p>
+
+		<textarea name="record" id="record" rows="20">[% BIG_LOOP %]</textarea>
+		[% FOREACH HIDDEN_LOO IN HIDDEN_LOOP %]
+				<input type="hidden" name="tag_[% HIDDEN_LOO.tag %]_indicator1_[% HIDDEN_LOO.index %][% HIDDEN_LOO.random %]" value="" />
+				<input type="hidden" name="tag_[% HIDDEN_LOO.tag %]_indicator2_[% HIDDEN_LOO.index %][% HIDDEN_LOO.random %]" value="" />
+				<input type="hidden" name="tag_[% HIDDEN_LOO.tag %]_code_[% HIDDEN_LOO.subfield %]_[% HIDDEN_LOO.index %]_[% HIDDEN_LOO.index_subfield %]" value="[% HIDDEN_LOO.subfield %]" />
+				<input type="hidden" name="tag_[% HIDDEN_LOO.tag %]_subfield_[% HIDDEN_LOO.subfield %]_[% HIDDEN_LOO.index %]_[% HIDDEN_LOO.index_subfield %]" value="[% HIDDEN_LOO.subfield_value %]" />
+		[% END %]
+
+</form>
+
+</div>
+</div>
+
+<script type="text/javascript" src="[% themelang %]/lib/codemirror/js/codemirror.js"></script>
+<script type="text/javascript" src="[% themelang %]/js/marc.js"></script>
+<script type="text/javascript" src="[% themelang %]/js/pages/addbiblio-text.js"></script>
+<script type="text/javascript">
+	<!--
+	addbiblio.biblionumber = [% biblionumber or 0 %];
+	// -->
+</script>
+<script type="text/javascript" src="/cgi-bin/koha/cataloguing/framework-jsonp.pl?prepend=addbiblio.mandatory%3D&amp;info=mandatory"></script>
+
+[% INCLUDE 'intranet-bottom.inc' %]
-- 
1.7.6



More information about the Koha-patches mailing list