[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&>D2YNFE-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><uCT!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 › Cataloging › [% 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> › <a href="/cgi-bin/koha/cataloguing/addbooks.pl">Cataloging</a> › [% 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 %]&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&info=mandatory"></script>
+
+[% INCLUDE 'intranet-bottom.inc' %]
--
1.7.6
More information about the Koha-patches
mailing list