From 99deb974d76df189276cbfddf153894d631f43d6 Mon Sep 17 00:00:00 2001 From: Jesse Weaver Date: Thu, 15 Jul 2010 15:44:50 -0600 Subject: [PATCH] Bug 6707 - Add textual MARC editor 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 | 254 +++++ C4/Koha.pm | 18 +- cataloguing/addbiblio-text.pl | 635 +++++++++++ cataloguing/addbiblio.pl | 8 +- cataloguing/framework-jsonp.pl | 60 + installer/data/mysql/sysprefs.sql | 2 + installer/data/mysql/updatedatabase.pl | 7 + .../intranet-tmpl/prog/en/css/staff-global.css | 6 + .../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, 5744 insertions(+), 3 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 2aa17d5..e0035ed 100644 --- a/C4/Biblio.pm +++ b/C4/Biblio.pm @@ -132,6 +132,8 @@ BEGIN { &TransformHtmlToMarc2 &TransformHtmlToMarc &TransformHtmlToXml + &TransformTextToMarc + &PrepareItemrecordDisplay &GetNoZebraIndexes prepare_host_field ); @@ -2407,6 +2409,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 ebb3eed..d842607 100644 --- a/C4/Koha.pm +++ b/C4/Koha.pm @@ -40,7 +40,7 @@ BEGIN { &slashifyDate &subfield_is_koha_internal_p &GetPrinters &GetPrinter - &GetItemTypes &getitemtypeinfo + &GetItemTypes &GetItemTypeList &getitemtypeinfo &GetCcodes &GetSupportName &GetSupportList &get_itemtypeinfos_of @@ -256,6 +256,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..af6a91d --- /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

ERROR when DELETING BIBLIO $biblionumber : $error

"; + 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 d5778a4..fb8e3d8 100755 --- a/cataloguing/addbiblio.pl +++ b/cataloguing/addbiblio.pl @@ -718,7 +718,7 @@ sub build_tabs { # ======================== # 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. @@ -733,12 +733,16 @@ my $dbh = C4::Context->dbh; my $hostbiblionumber = $input->param('hostbiblionumber'); my $hostitemnumber = $input->param('hostitemnumber'); - my $userflags = 'edit_catalogue'; if ($frameworkcode eq 'FA'){ $userflags = 'fast_cataloging'; } +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/sysprefs.sql b/installer/data/mysql/sysprefs.sql index 2ffe2df..718f2e6 100644 --- a/installer/data/mysql/sysprefs.sql +++ b/installer/data/mysql/sysprefs.sql @@ -328,6 +328,7 @@ 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'); +<<<<<<< HEAD:installer/data/mysql/sysprefs.sql INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('BorrowerUnwantedField','','Name the fields you don\'t need to store for a patron\'s account',NULL,'free'); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES ('OpacKohaUrl','1',"Show 'Powered by Koha' text on OPAC footer.",NULL,NULL); INSERT INTO `systempreferences` (variable,value,explanation,options,type) VALUES('EasyAnalyticalRecords','0','If on, display in the catalogue screens tools to easily setup analytical record relationships','','YesNo'); @@ -365,3 +366,4 @@ INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES ( INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('OpacBrowseResults','1','Disable/enable browsing and paging search results from the OPAC detail page.',NULL,'YesNo'); INSERT INTO systempreferences (variable,value,explanation,options,type) VALUES('SvcMaxReportRows','10','Maximum number of rows to return via the report web service.',NULL,'Integer'); INSERT INTO systempreferences (variable,value,options,explanation,type) VALUES ('ReservesControlBranch','PatronLibrary','ItemHomeLibrary|PatronLibrary','Branch checked for members reservations rights','Choice'); +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 dbbdee3..13a64a2 100755 --- a/installer/data/mysql/updatedatabase.pl +++ b/installer/data/mysql/updatedatabase.pl @@ -5308,6 +5308,13 @@ if (C4::Context->preference("Version") < TransformToNum($DBversion)) { SetVersion ($DBversion); } +$DBversion = "3.09.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 TableExists($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 e591a73..1094bb2 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css +++ b/koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css @@ -2274,3 +2274,9 @@ ul.ui-autocomplete { .ui-autocomplete-loading { background: #FFF url("../../img/loading-small.gif") right center no-repeat; } + +#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 4d83a5b..ce375c3 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 @@ -37,6 +37,9 @@ + [% 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( 'Watch your language: ' + 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( 'Record is missing pieces: ' + 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 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"]; + forEach(options.stylesheet, function(file) { + html.push(""); + }); + forEach(options.basefiles.concat(options.parserfile), function(file) { + html.push(""); + }); + html.push(""); + + 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 and
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
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 + // and
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 , 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 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
, or null for the first + // line. If given a
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 ( 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
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, "= 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 == "")); + else if (content == "")); + 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("");} + 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, "
"); + }; + + 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 10e1d13..ef5dd10 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 @@ -18,6 +18,13 @@ Cataloging: yes: Display no: "Don't Display" - easy ways to create analytical record relationships + - + - 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' %] +Koha › Cataloging › [% IF ( biblionumber ) %]Editing [% title |html %] (Record Number [% biblionumber %])[% ELSE %]Add MARC Record[% END %] +[% INCLUDE 'doc-head-close.inc' %] + + + + + + +
+
+ Loading, please wait... +
+
+ + +[% INCLUDE 'header.inc' %] + + +
+ +
+
+
+ + + +

[% IF ( biblionumber ) %]Editing [% title |html %] (Record Number [% biblionumber %])

[% ELSE %]Add MARC Record[% END %] + +[% UNLESS ( number ) %] + + [% IF ( duplicatebiblionumber ) %] +
+

Duplicate Record suspected

+

Is this a duplicate of [% duplicatetitle %]?

+
+ + +
+
+ +
+
+ [% END %] + [% END %] + +[% IF ( done ) %] + +[% ELSE %] +
+ +[% END %] + +
+ + + +
    +
  • +
  • +
  • + +
  • +
+
+ +[% IF ( popup ) %] + +[% END %] + + + +

+ + + +

+ + + [% FOREACH HIDDEN_LOO IN HIDDEN_LOOP %] + + + + + [% END %] + +
+ +
+
+ + + + + + + +[% INCLUDE 'intranet-bottom.inc' %] -- 1.7.2.5