Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
Checking mergeability… Don't worry, you can still create the pull request.
  • 2 commits
  • 36 files changed
  • 0 commit comments
  • 1 contributor
Commits on Jun 14, 2011
@pianohacker 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.
2567a97
@pianohacker Add embedded Z39.50 search to cataloguing homepage
Also, make some minor changes to search interface to improve usability in
cramped space.
e97b314
Showing with 5,749 additions and 14 deletions.
  1. +253 −0 C4/Biblio.pm
  2. +17 −1 C4/Koha.pm
  3. +635 −0 cataloguing/addbiblio-text.pl
  4. +5 −1 cataloguing/addbiblio.pl
  5. +60 −0 cataloguing/framework-jsonp.pl
  6. +11 −1 cataloguing/z3950_search.pl
  7. +5 −1 koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
  8. +3 −0  koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
  9. +194 −0 koha-tmpl/intranet-tmpl/prog/en/js/marc.js
  10. +82 −0 koha-tmpl/intranet-tmpl/prog/en/js/pages/addbiblio-text.js
  11. +23 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/LICENSE
  12. +47 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/csscolors.css
  13. +42 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/docs.css
  14. +55 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/jscolors.css
  15. +24 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/marccolors.css
  16. BIN  koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/people.jpg
  17. +39 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/sparqlcolors.css
  18. +51 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/xmlcolors.css
  19. +219 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/codemirror.js
  20. +1,176 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/editor.js
  21. +81 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/mirrorframe.js
  22. +155 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsecss.js
  23. +73 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsehtmlmixed.js
  24. +322 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsejavascript.js
  25. +102 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsemarc.js
  26. +162 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsesparql.js
  27. +286 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/parsexml.js
  28. +584 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/select.js
  29. +131 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/stringstream.js
  30. +57 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenize.js
  31. +176 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/tokenizejavascript.js
  32. +388 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/undo.js
  33. +123 −0 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/util.js
  34. +153 −0 koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbiblio-text.tt
  35. +2 −0  koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/addbooks.tt
  36. +13 −10 koha-tmpl/intranet-tmpl/prog/en/modules/cataloguing/z3950_search.tt
View
253 C4/Biblio.pm
@@ -126,6 +126,7 @@ BEGIN {
&TransformHtmlToMarc2
&TransformHtmlToMarc
&TransformHtmlToXml
+ &TransformTextToMarc
&PrepareItemrecordDisplay
&GetNoZebraIndexes
);
@@ -2065,6 +2066,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;
View
18 C4/Koha.pm
@@ -38,7 +38,7 @@ BEGIN {
&slashifyDate
&subfield_is_koha_internal_p
&GetPrinters &GetPrinter
- &GetItemTypes &getitemtypeinfo
+ &GetItemTypes &GetItemTypeList &getitemtypeinfo
&GetCcodes
&GetSupportName &GetSupportList
&get_itemtypeinfos_of
@@ -253,6 +253,22 @@ sub GetItemTypes {
return ( \%itemtypes );
}
+sub GetItemTypeList {
+ my ( $selected ) = @_;
+ my $itemtypes = GetItemTypes;
+ my @itemtypesloop;
+
+ foreach my $itemtype ( sort { $itemtypes->{$a}->{'description'} cmp $itemtypes->{$b}->{'description'} } keys( %$itemtypes ) ) {
+ push @itemtypesloop, {
+ value => $itemtype,
+ selected => ( $itemtype eq $selected ),
+ description => $itemtypes->{$itemtype}->{'description'},
+ };
+ }
+
+ return \@itemtypesloop;
+}
+
sub get_itemtypeinfos_of {
my @itemtypes = @_;
View
635 cataloguing/addbiblio-text.pl
@@ -0,0 +1,635 @@
+#!/usr/bin/perl
+
+# Copyright 2008 LibLime
+#
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it under the
+# terms of the GNU General Public License as published by the Free Software
+# Foundation; either version 2 of the License, or (at your option) any later
+# version.
+#
+# Koha is distributed in the hope that it will be useful, but WITHOUT ANY
+# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Koha; if not, write to the Free Software Foundation, Inc., 59 Temple Place,
+# Suite 330, Boston, MA 02111-1307 USA
+
+use strict;
+use CGI;
+use C4::Output qw(:html :ajax);
+use C4::Output::JSONStream;
+use JSON;
+use C4::Auth;
+use C4::Biblio;
+use C4::Search;
+use C4::AuthoritiesMarc;
+use C4::Context;
+use MARC::Record;
+use MARC::Field;
+use C4::Log;
+use C4::Koha; # XXX subfield_is_koha_internal_p
+use C4::Branch; # XXX subfield_is_koha_internal_p
+use C4::ClassSource;
+use C4::ImportBatch;
+use C4::Charset;
+
+use Date::Calc qw(Today);
+use MARC::File::USMARC;
+use MARC::File::XML;
+
+if ( C4::Context->preference('marcflavour') eq 'UNIMARC' ) {
+ MARC::File::XML->default_record_format('UNIMARC');
+}
+
+our($tagslib,$authorised_values_sth,$is_a_modif,$usedTagsLib,$mandatory_z3950);
+
+our ($sec, $min, $hour, $mday, $mon, $year, undef, undef, undef) = localtime(time);
+$year +=1900;
+$mon +=1;
+
+our %creators = (
+ '000@' => sub { ' nam a22 7a 4500' },
+ '005@' => sub { sprintf('%4d%02d%02d%02d%02d%02d.0', $year, $mon, $mday, $hour, $min, $sec) },
+ '008@' => sub { substr($year,2,2) . sprintf("%02d%02d", $mon, $mday) . 't xxu||||| |||| 00| 0 eng d' },
+);
+
+=item MARCfindbreeding
+
+ $record = MARCfindbreeding($breedingid);
+
+Look up the import record repository for the record with
+record with id $breedingid. If found, returns the decoded
+MARC::Record; otherwise, -1 is returned (FIXME).
+Returns as second parameter the character encoding.
+
+=cut
+
+sub MARCfindbreeding {
+ my ( $id ) = @_;
+ my ($marc, $encoding) = GetImportRecordMarc($id);
+ # remove the - in isbn, koha store isbn without any -
+ if ($marc) {
+ my $record = MARC::Record->new_from_usmarc($marc);
+ my ($isbnfield,$isbnsubfield) = GetMarcFromKohaField('biblioitems.isbn','');
+ if ( $record->field($isbnfield) ) {
+ foreach my $field ( $record->field($isbnfield) ) {
+ foreach my $subfield ( $field->subfield($isbnsubfield) ) {
+ my $newisbn = $field->subfield($isbnsubfield);
+ $newisbn =~ s/-//g;
+ $field->update( $isbnsubfield => $newisbn );
+ }
+ }
+ }
+ # fix the unimarc 100 coded field (with unicode information)
+ if (C4::Context->preference('marcflavour') eq 'UNIMARC' && $record->subfield(100,'a')) {
+ my $f100a=$record->subfield(100,'a');
+ my $f100 = $record->field(100);
+ my $f100temp = $f100->as_string;
+ $record->delete_field($f100);
+ if ( length($f100temp) > 28 ) {
+ substr( $f100temp, 26, 2, "50" );
+ $f100->update( 'a' => $f100temp );
+ my $f100 = MARC::Field->new( '100', '', '', 'a' => $f100temp );
+ $record->insert_fields_ordered($f100);
+ }
+ }
+
+ if ( !defined(ref($record)) ) {
+ return -1;
+ }
+ else {
+ # normalize author : probably UNIMARC specific...
+ if ( C4::Context->preference("z3950NormalizeAuthor")
+ and C4::Context->preference("z3950AuthorAuthFields") )
+ {
+ my ( $tag, $subfield ) = GetMarcFromKohaField("biblio.author");
+
+ # my $summary = C4::Context->preference("z3950authortemplate");
+ my $auth_fields =
+ C4::Context->preference("z3950AuthorAuthFields");
+ my @auth_fields = split /,/, $auth_fields;
+ my $field;
+
+ if ( $record->field($tag) ) {
+ foreach my $tmpfield ( $record->field($tag)->subfields ) {
+
+ # foreach my $subfieldcode ($tmpfield->subfields){
+ my $subfieldcode = shift @$tmpfield;
+ my $subfieldvalue = shift @$tmpfield;
+ if ($field) {
+ $field->add_subfields(
+ "$subfieldcode" => $subfieldvalue )
+ if ( $subfieldcode ne $subfield );
+ }
+ else {
+ $field =
+ MARC::Field->new( $tag, "", "",
+ $subfieldcode => $subfieldvalue )
+ if ( $subfieldcode ne $subfield );
+ }
+ }
+ }
+ $record->delete_field( $record->field($tag) );
+ foreach my $fieldtag (@auth_fields) {
+ next unless ( $record->field($fieldtag) );
+ my $lastname = $record->field($fieldtag)->subfield('a');
+ my $firstname = $record->field($fieldtag)->subfield('b');
+ my $title = $record->field($fieldtag)->subfield('c');
+ my $number = $record->field($fieldtag)->subfield('d');
+ if ($title) {
+
+# $field->add_subfields("$subfield"=>"[ ".ucfirst($title).ucfirst($firstname)." ".$number." ]");
+ $field->add_subfields(
+ "$subfield" => ucfirst($title) . " "
+ . ucfirst($firstname) . " "
+ . $number );
+ }
+ else {
+
+# $field->add_subfields("$subfield"=>"[ ".ucfirst($firstname).", ".ucfirst($lastname)." ]");
+ $field->add_subfields(
+ "$subfield" => ucfirst($firstname) . ", "
+ . ucfirst($lastname) );
+ }
+ }
+ $record->insert_fields_ordered($field);
+ }
+ return $record, $encoding;
+ }
+ }
+ return -1;
+}
+
+# Borrowed from MARC::Record::JSON, due to its lack of availability on CPAN
+
+sub MARC::Record::as_json_record_structure {
+ my $self = shift;
+ my $data = { leader => $self->leader };
+ my @fields;
+ foreach my $field ($self->fields) {
+ my $json_field = { tag => $field->tag };
+
+ if ($field->is_control_field) {
+ $json_field->{contents} = $field->data;
+ } else {
+ $json_field->{indicator1} = $field->indicator(1);
+ $json_field->{indicator2} = $field->indicator(2);
+
+ $json_field->{subfields} = [ $field->subfields ];
+ }
+
+ push @fields, $json_field;
+ }
+
+ $data->{fields} = \@fields;
+
+ return $data;
+}
+
+=item GetMandatoryFieldZ3950
+
+ This function return an hashref which containts all mandatory field
+ to search with z3950 server.
+
+=cut
+
+sub GetMandatoryFieldZ3950($){
+ my $frameworkcode = shift;
+ my @isbn = GetMarcFromKohaField('biblioitems.isbn',$frameworkcode);
+ my @title = GetMarcFromKohaField('biblio.title',$frameworkcode);
+ my @author = GetMarcFromKohaField('biblio.author',$frameworkcode);
+ my @issn = GetMarcFromKohaField('biblioitems.issn',$frameworkcode);
+ my @lccn = GetMarcFromKohaField('biblioitems.lccn',$frameworkcode);
+
+ return {
+ $isbn[0].$isbn[1] => 'isbn',
+ $title[0].$title[1] => 'title',
+ $author[0].$author[1] => 'author',
+ $issn[0].$issn[1] => 'issn',
+ $lccn[0].$lccn[1] => 'lccn',
+ };
+}
+
+sub build_tabs ($$$$$) {
+ my($template, $record, $dbh,$encoding, $input) = @_;
+ # fill arrays
+ my @loop_data =();
+ my $tag;
+ my $i=0;
+ my $authorised_values_sth = $dbh->prepare("select authorised_value,lib
+ from authorised_values
+ where category=? order by lib");
+
+ # in this array, we will push all the 10 tabs
+ # to avoid having 10 tabs in the template : they will all be in the same BIG_LOOP
+ my @BIG_LOOP;
+ my @HIDDEN_LOOP;
+
+# loop through each tab 0 through 9
+ foreach my $tag (sort(keys (%{$tagslib}))) {
+ my $taglib = $tagslib->{$tag};
+ my $indicator;
+# if MARC::Record is not empty => use it as master loop, then add missing subfields that should be in the tab.
+# if MARC::Record is empty => use tab as master loop.
+ if ($record ne -1 && ($record->field($tag) || $tag eq '000')) {
+ my @fields;
+ if ($tag ne '000') {
+ @fields = $record->field($tag);
+ } else {
+ push @fields,$record->leader();
+ }
+ foreach my $field (@fields) {
+ my $tag_writeout = "$tag ";
+ $tag_writeout .= ($field->indicator(1) eq ' ' ? '_' : $field->indicator(1)) . ($field->indicator(1) eq ' ' ? '_' : $field->indicator(1)) . ' ' if ($tag>=10);
+ my $tag_index = int(rand(1000000));
+ my @subfields_data;
+ if ($tag<10) {
+ my ($value,$subfield);
+ if ($tag ne '000') {
+ $value=$field->data();
+ $subfield="@";
+ } else {
+ $value = $field;
+ $subfield='@';
+ }
+ my $subfieldlib = $taglib->{$subfield};
+ next if ($subfieldlib->{kohafield} eq 'biblio.biblionumber');
+
+ push(@subfields_data, "$value");
+ $i++;
+ } else {
+ my @subfields=$field->subfields();
+ foreach my $subfieldcount (0..$#subfields) {
+ my $subfield=$subfields[$subfieldcount][0];
+ my $value=$subfields[$subfieldcount][1];
+ my $subfieldlib = $taglib->{$subfield};
+ next if (length $subfield !=1);
+ next if ($subfieldlib->{tab} > 9 or $subfieldlib->{tab} == -1);
+ push(@subfields_data, "\$$subfield $value");
+ $i++;
+ }
+ }
+# now, loop again to add parameter subfield that are not in the MARC::Record
+ foreach my $subfield (sort( keys %{$tagslib->{$tag}})) {
+ my $subfieldlib = $taglib->{$subfield};
+ next if (length $subfield !=1);
+ next if ($tag<10);
+ next if (!$subfieldlib->{mandatory});
+ next if ($subfieldlib->{tab} > 9 or $subfieldlib->{tab} == -1);
+ next if (defined($field->subfield($subfield)));
+ push(@subfields_data, "\$$subfield");
+ $i++;
+ }
+ if (@subfields_data) {
+ $tag_writeout .= join(' ', @subfields_data);
+ push (@BIG_LOOP, $tag_writeout);
+ }
+# If there is more than 1 field, add an empty hidden field as separator.
+ }
+# if breeding is empty
+ } else {
+ my $tag_writeout = "$tag ";
+ $tag_writeout .= '__ ' if ($tag>=10);
+ my @subfields_data;
+ foreach my $subfield (sort(keys %{$tagslib->{$tag}})) {
+ my $subfieldlib = $taglib->{$subfield};
+ next if (length $subfield !=1);
+ next if (!$subfieldlib->{mandatory});
+ next if ($subfieldlib->{tab} > 9);
+
+ if (ref($creators{$tag . $subfield}) eq 'CODE') {
+ if (($subfieldlib->{hidden} <= -4) or ($subfieldlib->{hidden}>=5) or ($taglib->{tab} == -1)) {
+ my %row = (
+ tag => $tag,
+ index => int(rand(1000000)),
+ index_subfield => int(rand(1000000)),
+ random => int(rand(1000000)),
+ subfield => ($subfield eq '@' ? '00' : $subfield),
+ subfield_value => $creators{$tag . $subfield}(),
+ );
+ push @HIDDEN_LOOP, \%row;
+ next;
+ } else {
+ push @subfields_data, $creators{$tag . $subfield}();
+ next;
+ }
+ }
+
+ if ($tag >= 10) {
+ push @subfields_data, "\$$subfield";
+ } else {
+ push @subfields_data, "";
+ }
+ $i++;
+ }
+ next if (!@subfields_data);
+ push (@BIG_LOOP, $tag_writeout . join(' ', @subfields_data));
+ }
+ }
+# $template->param($tabloop."XX" =>\@loop_data);
+ $template->param(
+ BIG_LOOP => join("\n", @BIG_LOOP),
+ HIDDEN_LOOP => \@HIDDEN_LOOP,
+ record_length => $#BIG_LOOP,
+ );
+}
+
+#
+# sub that tries to find authorities linked to the biblio
+# the sub :
+# - search in the authority DB for the same authid (in $9 of the biblio)
+# - search in the authority DB for the same 001 (in $3 of the biblio in UNIMARC)
+# - search in the authority DB for the same values (exactly) (in all subfields of the biblio)
+# if the authority is found, the biblio is modified accordingly to be connected to the authority.
+# if the authority is not found, it's added, and the biblio is then modified to be connected to the authority.
+#
+
+sub BiblioAddAuthorities{
+ my ( $record, $frameworkcode ) = @_;
+ my $dbh=C4::Context->dbh;
+ my $query=$dbh->prepare(qq|
+SELECT authtypecode,tagfield
+FROM marc_subfield_structure
+WHERE frameworkcode=?
+AND (authtypecode IS NOT NULL AND authtypecode<>\"\")|);
+# SELECT authtypecode,tagfield
+# FROM marc_subfield_structure
+# WHERE frameworkcode=?
+# AND (authtypecode IS NOT NULL OR authtypecode<>\"\")|);
+ $query->execute($frameworkcode);
+ my ($countcreated,$countlinked);
+ while (my $data=$query->fetchrow_hashref){
+ foreach my $field ($record->field($data->{tagfield})){
+ next if ($field->subfield('3')||$field->subfield('9'));
+ # No authorities id in the tag.
+ # Search if there is any authorities to link to.
+ my $query='at='.$data->{authtypecode}.' ';
+ map {$query.= ' and he,ext="'.$_->[1].'"' if ($_->[0]=~/[A-z]/)} $field->subfields();
+ my ($error, $results, $total_hits)=SimpleSearch( $query, undef, undef, [ "authorityserver" ] );
+ # there is only 1 result
+ if ( $error ) {
+ warn "BIBLIOADDSAUTHORITIES: $error";
+ return (0,0) ;
+ }
+ if ($results && scalar(@$results)==1) {
+ my $marcrecord = MARC::File::USMARC::decode($results->[0]);
+ $field->add_subfields('9'=>$marcrecord->field('001')->data);
+ $countlinked++;
+ } elsif (scalar(@$results)>1) {
+ #More than One result
+ #This can comes out of a lack of a subfield.
+# my $marcrecord = MARC::File::USMARC::decode($results->[0]);
+# $record->field($data->{tagfield})->add_subfields('9'=>$marcrecord->field('001')->data);
+ $countlinked++;
+ } else {
+ #There are no results, build authority record, add it to Authorities, get authid and add it to 9
+ ###NOTICE : This is only valid if a subfield is linked to one and only one authtypecode
+ ###NOTICE : This can be a problem. We should also look into other types and rejected forms.
+ my $authtypedata=GetAuthType($data->{authtypecode});
+ next unless $authtypedata;
+ my $marcrecordauth=MARC::Record->new();
+ my $authfield=MARC::Field->new($authtypedata->{auth_tag_to_report},'','',"a"=>"".$field->subfield('a'));
+ map { $authfield->add_subfields($_->[0]=>$_->[1]) if ($_->[0]=~/[A-z]/ && $_->[0] ne "a" )} $field->subfields();
+ $marcrecordauth->insert_fields_ordered($authfield);
+
+ # bug 2317: ensure new authority knows it's using UTF-8; currently
+ # only need to do this for MARC21, as MARC::Record->as_xml_record() handles
+ # automatically for UNIMARC (by not transcoding)
+ # FIXME: AddAuthority() instead should simply explicitly require that the MARC::Record
+ # use UTF-8, but as of 2008-08-05, did not want to introduce that kind
+ # of change to a core API just before the 3.0 release.
+ if (C4::Context->preference('marcflavour') eq 'MARC21') {
+ SetMarcUnicodeFlag($marcrecordauth, 'MARC21');
+ }
+
+# warn "AUTH RECORD ADDED : ".$marcrecordauth->as_formatted;
+
+ my $authid=AddAuthority($marcrecordauth,'',$data->{authtypecode});
+ $countcreated++;
+ $field->add_subfields('9'=>$authid);
+ }
+ }
+ }
+ return ($countlinked,$countcreated);
+}
+
+# ========================
+# MAIN
+#=========================
+my $input = new CGI;
+my $error = $input->param('error');
+my $biblionumber = $input->param('biblionumber'); # if biblionumber exists, it's a modif, not a new biblio.
+my $breedingid = $input->param('breedingid');
+my $z3950 = $input->param('z3950');
+my $op = $input->param('op');
+my $mode = $input->param('mode');
+my $record_text = $input->param('record');
+my $frameworkcode = $input->param('frameworkcode');
+my $dbh = C4::Context->dbh;
+
+my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
+ {
+ template_name => "cataloguing/addbiblio-text.tmpl",
+ query => $input,
+ type => "intranet",
+ authnotrequired => 0,
+ flagsrequired => { editcatalogue => 1 },
+ }
+);
+
+if (is_ajax() && $op eq 'try_parse') {
+ my @params = $input->param();
+ my $record = TransformHtmlToMarc( \@params , $input );
+ my $response = new C4::Output::JSONStream;
+
+ eval {
+ $record = TransformTextToMarc( $record_text, existing_record => $record )
+ };
+ if ( $@ ) {
+ chomp $@;
+ $response->param( type => 'input', error => 'parse_failed', message => $@ );
+
+ output_with_http_headers $input, $cookie, $response->output, 'json';
+ exit;
+ }
+
+ $response->param( record => $record->as_json_record_structure );
+
+ output_with_http_headers $input, $cookie, $response->output, 'json';
+ exit;
+}
+
+$frameworkcode = &GetFrameworkCode($biblionumber)
+ if ( $biblionumber and not($frameworkcode) );
+
+$frameworkcode = '' if ( $frameworkcode eq 'Default' );
+
+# Getting the list of all frameworks
+# get framework list
+my $frameworks = getframeworks;
+my @frameworkcodeloop;
+foreach my $thisframeworkcode ( keys %$frameworks ) {
+ my %row = (
+ value => $thisframeworkcode,
+ frameworktext => $frameworks->{$thisframeworkcode}->{'frameworktext'},
+ );
+ if ($frameworkcode eq $thisframeworkcode){
+ $row{'selected'}="selected=\"selected\"";
+ }
+ push @frameworkcodeloop, \%row;
+}
+$template->param( frameworkcodeloop => \@frameworkcodeloop,
+ breedingid => $breedingid );
+
+# ++ Global
+$tagslib = &GetMarcStructure( 1, $frameworkcode );
+$usedTagsLib = &GetUsedMarcStructure( $frameworkcode );
+$mandatory_z3950 = GetMandatoryFieldZ3950($frameworkcode);
+# -- Global
+
+my $record = -1;
+my $encoding = "";
+my (
+ $biblionumbertagfield,
+ $biblionumbertagsubfield,
+ $biblioitemnumtagfield,
+ $biblioitemnumtagsubfield,
+ $bibitem,
+ $biblioitemnumber
+);
+
+if (($biblionumber) && !($breedingid)){
+ $record = GetMarcBiblio($biblionumber);
+}
+if ($breedingid) {
+ ( $record, $encoding ) = MARCfindbreeding( $breedingid ) ;
+}
+
+$is_a_modif = 0;
+
+if ($biblionumber) {
+ $is_a_modif = 1;
+ $template->param( title => $record->title(), );
+
+ # if it's a modif, retrieve bibli and biblioitem numbers for the future modification of old-DB.
+ ( $biblionumbertagfield, $biblionumbertagsubfield ) =
+ &GetMarcFromKohaField( "biblio.biblionumber", $frameworkcode );
+ ( $biblioitemnumtagfield, $biblioitemnumtagsubfield ) =
+ &GetMarcFromKohaField( "biblioitems.biblioitemnumber", $frameworkcode );
+
+ # search biblioitems value
+ my $sth = $dbh->prepare("select biblioitemnumber from biblioitems where biblionumber=?");
+ $sth->execute($biblionumber);
+ ($biblioitemnumber) = $sth->fetchrow;
+}
+
+#-------------------------------------------------------------------------------------
+if ( $op eq "addbiblio" ) {
+#-------------------------------------------------------------------------------------
+ # getting html input
+ my @params = $input->param();
+ $record = TransformHtmlToMarc( \@params , $input );
+ eval {
+ $record = TransformTextToMarc( $record_text, existing_record => $record )
+ };
+ # check for a duplicate
+ my ($duplicatebiblionumber,$duplicatetitle) = FindDuplicate($record) if (!$is_a_modif);
+ my $confirm_not_duplicate = $input->param('confirm_not_duplicate');
+ # it is not a duplicate (determined either by Koha itself or by user checking it's not a duplicate)
+ if ( !$duplicatebiblionumber or $confirm_not_duplicate ) {
+ my $oldbibnum;
+ my $oldbibitemnum;
+ if (C4::Context->preference("BiblioAddsAuthorities")){
+ my ($countlinked,$countcreated)=BiblioAddAuthorities($record,$frameworkcode);
+ }
+ if ( $is_a_modif ) {
+ ModBiblioframework( $biblionumber, $frameworkcode );
+ ModBiblio( $record, $biblionumber, $frameworkcode );
+ }
+ else {
+ ( $biblionumber, $oldbibitemnum ) = AddBiblio( $record, $frameworkcode );
+ }
+
+ if ($mode ne "popup"){
+ print $input->redirect(
+ "/cgi-bin/koha/cataloguing/additem.pl?biblionumber=$biblionumber&frameworkcode=$frameworkcode"
+ );
+ exit;
+ } else {
+ $template->param(
+ biblionumber => $biblionumber,
+ done =>1,
+ popup =>1
+ );
+ $template->param( title => $record->subfield('200',"a") ) if ($record ne "-1" && C4::Context->preference('marcflavour') =~/unimarc/i);
+ $template->param( title => $record->title() ) if ($record ne "-1" && C4::Context->preference('marcflavour') eq "usmarc");
+ $template->param(
+ popup => $mode,
+ itemtype => $frameworkcode,
+ );
+ output_html_with_http_headers $input, $cookie, $template->output;
+ exit;
+ }
+ } else {
+ # it may be a duplicate, warn the user and do nothing
+ build_tabs ($template, $record, $dbh,$encoding,$input);
+ $template->param(
+ biblionumber => $biblionumber,
+ biblioitemnumber => $biblioitemnumber,
+ duplicatebiblionumber => $duplicatebiblionumber,
+ duplicatebibid => $duplicatebiblionumber,
+ duplicatetitle => $duplicatetitle,
+ );
+ }
+}
+elsif ( $op eq "delete" ) {
+
+ my $error = &DelBiblio($biblionumber);
+ if ($error) {
+ warn "ERROR when DELETING BIBLIO $biblionumber : $error";
+ print "Content-Type: text/html\n\n<html><body><h1>ERROR when DELETING BIBLIO $biblionumber : $error</h1></body></html>";
+ exit;
+ }
+
+ print $input->redirect('/cgi-bin/koha/catalogue/search.pl');
+ exit;
+
+} else {
+ #----------------------------------------------------------------------------
+ # If we're in a duplication case, we have to set to "" the biblionumber
+ # as we'll save the biblio as a new one.
+ if ( $op eq "duplicate" ) {
+ $biblionumber = "";
+ }
+
+#FIXME: it's kind of silly to go from MARC::Record to MARC::File::XML and then back again just to fix the encoding
+ eval {
+ my $uxml = $record->as_xml;
+ MARC::Record::default_record_format("UNIMARC")
+ if ( C4::Context->preference("marcflavour") eq "UNIMARC" );
+ my $urecord = MARC::Record::new_from_xml( $uxml, 'UTF-8' );
+ $record = $urecord;
+ };
+ build_tabs( $template, $record, $dbh, $encoding,$input );
+ $template->param(
+ biblionumber => $biblionumber,
+ biblionumbertagfield => $biblionumbertagfield,
+ biblionumbertagsubfield => $biblionumbertagsubfield,
+ biblioitemnumtagfield => $biblioitemnumtagfield,
+ biblioitemnumtagsubfield => $biblioitemnumtagsubfield,
+ biblioitemnumber => $biblioitemnumber,
+ );
+}
+
+$template->param( title => $record->title() ) if ( $record ne "-1" );
+$template->param(
+ popup => $mode,
+ frameworkcode => $frameworkcode,
+ itemtype => $frameworkcode,
+ itemtypes => GetItemTypeList(),
+);
+
+output_html_with_http_headers $input, $cookie, $template->output;
View
6 cataloguing/addbiblio.pl
@@ -823,7 +823,7 @@ sub BiblioAddAuthorities{
# ========================
# MAIN
-#=========================
+#========================
my $input = new CGI;
my $error = $input->param('error');
my $biblionumber = $input->param('biblionumber'); # if biblionumber exists, it's a modif, not a new biblio.
@@ -836,6 +836,10 @@ sub BiblioAddAuthorities{
my $dbh = C4::Context->dbh;
my $userflags = ($frameworkcode eq 'FA') ? "fast_cataloging" : "edit_catalogue";
+if (C4::Context->preference('MARCEditor') eq 'text') {
+ print $input->redirect('/cgi-bin/koha/cataloguing/addbiblio-text.pl?' . $ENV{'QUERY_STRING'});
+ exit;
+}
$frameworkcode = &GetFrameworkCode($biblionumber)
if ( $biblionumber and not($frameworkcode) and $op ne 'addbiblio' );
View
60 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;
View
12 cataloguing/z3950_search.pl
@@ -215,6 +215,9 @@
last if $event == ZOOM::Event::ZEND;
}
+ my $edition_empty = 1;
+ my $lccn_empty = 1;
+
if ( $k != 0 ) {
$k--;
warn $serverhost[$k] if $DEBUG;
@@ -259,9 +262,13 @@
$imported, $breedingid
)
= ImportBreeding( $marcdata, 2, $serverhost[$k], $encoding[$k], $random, 'z3950' );
+
+ $lccn_empty = 0 if ( $oldbiblio->{lccn} );
+ $edition_empty = 0 if ( $oldbiblio->{editionstatement} );
+
my %row_data;
$row_data{server} = $servername[$k];
- $row_data{isbn} = $oldbiblio->{isbn};
+ $row_data{isbn} = [ map( +{ value => $_ }, split( /\|/, $oldbiblio->{isbn} ) ) ];
$row_data{lccn} = $oldbiblio->{lccn};
$row_data{title} = $oldbiblio->{title};
$row_data{author} = $oldbiblio->{author};
@@ -281,6 +288,9 @@
$numberpending = $nremaining - 1;
$template->param(
breeding_loop => \@breeding_loop,
+ lccn_empty => $lccn_empty,
+ edition_empty => $edition_empty,
+ column_backshift => ($lccn_empty ? 1 : 0) + ($edition_empty ? 1 : 0) + 0,
server => $servername[$k],
numberpending => $numberpending,
biblionumber => $biblionumber,
View
6 koha-tmpl/intranet-tmpl/prog/en/css/staff-global.css
@@ -2031,4 +2031,8 @@ fieldset.rows+h3 {clear:both;padding-top:.5em;}
border : 1px solid #EEE;
padding : 0.3em 0.4em;
}
-</style>
+#embedded_z3950 {
+ width: 100%;
+ height: 500px;
+ border: none;
+}
View
3  koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc
@@ -72,6 +72,9 @@
<script type="text/javascript" src="[% yuipath %]/container/container_core-min.js"></script>
<script type="text/javascript" src="[% yuipath %]/menu/menu-min.js"></script>
+<script type="text/javascript">
+var koha = { themelang: '<!-- TMPL_VAR NAME="themelang" -->' };
+</script>
<!-- koha core js -->
<script type="text/javascript" src="[% themelang %]/js/staff-global.js"></script>
[% IF ( intranetuserjs ) %]
View
194 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];
+ }
+ },
+} );
View
82 koha-tmpl/intranet-tmpl/prog/en/js/pages/addbiblio-text.js
@@ -0,0 +1,82 @@
+addbiblio = {};
+
+$.extend( addbiblio, {
+ submit: function() {
+ $.ajax( {
+ url: '/cgi-bin/koha/cataloguing/addbiblio-text.pl',
+ type: 'POST',
+ dataType: 'json',
+ data: $( '#f input[name^="tag"]' ).serialize() + '&op=try_parse&record=' + escape(addbiblio.editor.getCode()),
+ success: addbiblio.submit.finished,
+ } );
+ },
+ insert_itemtype: function( event ) {
+ var iter = addbiblio.editor.cursorPosition();
+ addbiblio.editor.insertIntoLine( iter.line, iter.character, $( '#itemtypes' ).val() );
+
+ return false;
+ },
+ z3950_search: function() {
+ window.open( "/cgi-bin/koha/cataloguing/z3950_search.pl?biblionumber=" + addbiblio.biblionumber,"z3950search",'width=740,height=450,location=yes,toolbar=no,scrollbars=yes,resize=yes' );
+ },
+ not_duplicate: function() {
+ $( "#confirm_not_duplicate" ).attr( "value", "1" );
+ $( "#f" ).get( 0 ).submit();
+ },
+} );
+
+$.extend( addbiblio.submit, {
+ finished: function( data, status_ ) {
+ if ( data.error ) {
+ humanMsg.displayMsg( '<strong>Watch your language:</strong> ' + data.message );
+ return false;
+ }
+
+ var record = new marc.record(data.record);
+
+ var missing_tags = [], missing_subfields = [];
+
+ $.each( addbiblio.mandatory.tags, function( i, tag ) {
+ if ( tag == '000' ) {
+ if ( !record.leader) missing_tags.push( 'leader' );
+ } else if ( !record.has( tag ) ) {
+ missing_tags.push( tag );
+ }
+ } );
+
+ $.each( addbiblio.mandatory.subfields, function( i, sf ) {
+ if ( sf[0].substring( 0, 2 ) != '00' && !record.has( sf[0], sf[1] ) ) {
+ missing_subfields.push( sf.join( '$' ) );
+ }
+ } );
+
+ if ( missing_tags.length || missing_subfields.length ) {
+ message = [];
+
+ if ( missing_tags.length ) {
+ message.push( missing_tags.join( ', ' ) + ' tags' );
+ }
+
+ if ( missing_subfields.length ) {
+ message.push( missing_subfields.join( ', ' ) + ' subfields' );
+ }
+
+ humanMsg.displayMsg( '<strong>Record is missing pieces:</strong> ' + message.join( ' and ' ) + ' are mandatory' );
+ return;
+ }
+
+ $( '#f' ).get( 0 ).submit();
+ }
+} );
+
+$( function () {
+ $( '#insert-itemtype' ).click( addbiblio.insert_itemtype );
+
+ addbiblio.editor = CodeMirror.fromTextArea('record', {
+ height: "350px",
+ parserfile: "parsemarc.js",
+ stylesheet: koha.themelang + "/lib/codemirror/css/marccolors.css",
+ path: koha.themelang + "/lib/codemirror/js/",
+ autoMatchParens: true
+ });
+} );
View
23 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
View
47 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;
+}
View
42 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;
+}
View
55 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;
+}
View
24 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;
+}
View
BIN  koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/people.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
39 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/sparqlcolors.css
@@ -0,0 +1,39 @@
+.editbox {
+ margin: .4em;
+ padding: 0;
+ font-family: monospace;
+ font-size: 10pt;
+ color: black;
+}
+
+.editbox p {
+ margin: 0;
+}
+
+span.sp-keyword {
+ color: #708;
+}
+
+span.sp-prefixed {
+ color: #5d1;
+}
+
+span.sp-var {
+ color: #00c;
+}
+
+span.sp-comment {
+ color: #a70;
+}
+
+span.sp-literal {
+ color: #a22;
+}
+
+span.sp-uri {
+ color: #292;
+}
+
+span.sp-operator {
+ color: #088;
+}
View
51 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/css/xmlcolors.css
@@ -0,0 +1,51 @@
+.editbox {
+ margin: .4em;
+ padding: 0;
+ font-family: monospace;
+ font-size: 10pt;
+ color: black;
+}
+
+.editbox p {
+ margin: 0;
+}
+
+span.xml-tagname {
+ color: #A0B;
+}
+
+span.xml-attribute {
+ color: #281;
+}
+
+span.xml-punctuation {
+ color: black;
+}
+
+span.xml-attname {
+ color: #00F;
+}
+
+span.xml-comment {
+ color: #A70;
+}
+
+span.xml-cdata {
+ color: #48A;
+}
+
+span.xml-processing {
+ color: #999;
+}
+
+span.xml-entity {
+ color: #A22;
+}
+
+span.xml-error {
+ color: #F00;
+}
+
+span.xml-text {
+ color: black;
+}
View
219 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/codemirror.js
@@ -0,0 +1,219 @@
+/* CodeMirror main module
+ *
+ * Implements the CodeMirror constructor and prototype, which take care
+ * of initializing the editor frame, and providing the outside interface.
+ */
+
+// The CodeMirrorConfig object is used to specify a default
+// configuration. If you specify such an object before loading this
+// file, the values you put into it will override the defaults given
+// below. You can also assign to it after loading.
+var CodeMirrorConfig = window.CodeMirrorConfig || {};
+
+var CodeMirror = (function(){
+ function setDefaults(object, defaults) {
+ for (var option in defaults) {
+ if (!object.hasOwnProperty(option))
+ object[option] = defaults[option];
+ }
+ }
+ function forEach(array, action) {
+ for (var i = 0; i < array.length; i++)
+ action(array[i]);
+ }
+
+ // These default options can be overridden by passing a set of
+ // options to a specific CodeMirror constructor. See manual.html for
+ // their meaning.
+ setDefaults(CodeMirrorConfig, {
+ stylesheet: "",
+ path: "",
+ parserfile: [],
+ basefiles: ["util.js", "stringstream.js", "select.js", "undo.js", "editor.js", "tokenize.js"],
+ linesPerPass: 15,
+ passDelay: 200,
+ continuousScanning: false,
+ saveFunction: null,
+ onChange: null,
+ undoDepth: 20,
+ undoDelay: 800,
+ disableSpellcheck: true,
+ textWrapping: true,
+ readOnly: false,
+ width: "100%",
+ height: "300px",
+ autoMatchParens: false,
+ parserConfig: null,
+ dumbTabs: false,
+ activeTokens: null,
+ cursorActivity: null
+ });
+
+ function CodeMirror(place, options) {
+ // Use passed options, if any, to override defaults.
+ this.options = options = options || {};
+ setDefaults(options, CodeMirrorConfig);
+
+ var frame = this.frame = document.createElement("IFRAME");
+ frame.src = "javascript:false;";
+ frame.style.border = "0";
+ frame.style.width = options.width;
+ frame.style.height = options.height;
+ // display: block occasionally suppresses some Firefox bugs, so we
+ // always add it, redundant as it sounds.
+ frame.style.display = "block";
+
+ if (place.appendChild)
+ place.appendChild(frame);
+ else
+ place(frame);
+
+ // Link back to this object, so that the editor can fetch options
+ // and add a reference to itself.
+ frame.CodeMirror = this;
+ this.win = frame.contentWindow;
+
+ if (typeof options.parserfile == "string")
+ options.parserfile = [options.parserfile];
+ if (typeof options.stylesheet == "string")
+ options.stylesheet = [options.stylesheet];
+
+ var html = ["<html><head>"];
+ forEach(options.stylesheet, function(file) {
+ html.push("<link rel=\"stylesheet\" type=\"text/css\" href=\"" + file + "\"/>");
+ });
+ forEach(options.basefiles.concat(options.parserfile), function(file) {
+ html.push("<script type=\"text/javascript\" src=\"" + options.path + file + "\"></script>");
+ });
+ html.push("</head><body style=\"border-width: 0;\" class=\"editbox\" spellcheck=\"" +
+ (options.disableSpellcheck ? "false" : "true") + "\"></body></html>");
+
+ var doc = this.win.document;
+ doc.open();
+ doc.write(html.join(""));
+ doc.close();
+ }
+
+ CodeMirror.prototype = {
+ getCode: function() {return this.editor.getCode();},
+ setCode: function(code) {this.editor.importCode(code);},
+ selection: function() {return this.editor.selectedText();},
+ reindent: function() {this.editor.reindent();},
+
+ focus: function() {
+ this.win.focus();
+ if (this.editor.selectionSnapshot) // IE hack
+ this.win.select.selectCoords(this.win, this.editor.selectionSnapshot);
+ },
+ replaceSelection: function(text) {
+ this.focus();
+ this.editor.replaceSelection(text);
+ return true;
+ },
+ replaceChars: function(text, start, end) {
+ this.editor.replaceChars(text, start, end);
+ },
+ getSearchCursor: function(string, fromCursor) {
+ return this.editor.getSearchCursor(string, fromCursor);
+ },
+
+ cursorPosition: function(start) {
+ if (this.win.select.ie_selection) this.focus();
+ return this.editor.cursorPosition(start);
+ },
+ firstLine: function() {return this.editor.firstLine();},
+ lastLine: function() {return this.editor.lastLine();},
+ nextLine: function(line) {return this.editor.nextLine(line);},
+ prevLine: function(line) {return this.editor.prevLine(line);},
+ lineContent: function(line) {return this.editor.lineContent(line);},
+ setLineContent: function(line, content) {this.editor.setLineContent(line, content);},
+ insertIntoLine: function(line, position, content) {this.editor.insertIntoLine(line, position, content);},
+ selectLines: function(startLine, startOffset, endLine, endOffset) {
+ this.win.focus();
+ this.editor.selectLines(startLine, startOffset, endLine, endOffset);
+ },
+ nthLine: function(n) {
+ var line = this.firstLine();
+ for (; n > 1 && line !== false; n--)
+ line = this.nextLine(line);
+ return line;
+ },
+ lineNumber: function(line) {
+ var num = 0;
+ while (line !== false) {
+ num++;
+ line = this.prevLine(line);
+ }
+ return num;
+ },
+
+ // Old number-based line interface
+ jumpToLine: function(n) {
+ this.selectLines(this.nthLine(n), 0);
+ this.win.focus();
+ },
+ currentLine: function() {
+ return this.lineNumber(this.cursorPosition().line);
+ }
+ };
+
+ CodeMirror.InvalidLineHandle = {toString: function(){return "CodeMirror.InvalidLineHandle";}};
+
+ CodeMirror.replace = function(element) {
+ if (typeof element == "string")
+ element = document.getElementById(element);
+ return function(newElement) {
+ element.parentNode.replaceChild(newElement, element);
+ };
+ };
+
+ CodeMirror.fromTextArea = function(area, options) {
+ if (typeof area == "string")
+ area = document.getElementById(area);
+
+ options = options || {};
+ if (area.style.width) options.width = area.style.width;
+ if (area.style.height) options.height = area.style.height;
+ if (options.content == null) options.content = area.value;
+
+ if (area.form) {
+ function updateField() {
+ area.value = mirror.getCode();
+ }
+ if (typeof area.form.addEventListener == "function")
+ area.form.addEventListener("submit", updateField, false);
+ else
+ area.form.attachEvent("onsubmit", updateField);
+ }
+
+ function insert(frame) {
+ if (area.nextSibling)
+ area.parentNode.insertBefore(frame, area.nextSibling);
+ else
+ area.parentNode.appendChild(frame);
+ }
+
+ area.style.display = "none";
+ var mirror = new CodeMirror(insert, options);
+ return mirror;
+ };
+
+ CodeMirror.isProbablySupported = function() {
+ // This is rather awful, but can be useful.
+ var match;
+ if (window.opera)
+ return Number(window.opera.version()) >= 9.52;
+ else if (/Apple Computers, Inc/.test(navigator.vendor) && (match = navigator.userAgent.match(/Version\/(\d+(?:\.\d+)?)\./)))
+ return Number(match[1]) >= 3;
+ else if (document.selection && window.ActiveXObject && (match = navigator.userAgent.match(/MSIE (\d+(?:\.\d*)?)\b/)))
+ return Number(match[1]) >= 6;
+ else if (match = navigator.userAgent.match(/gecko\/(\d{8})/i))
+ return Number(match[1]) >= 20050901;
+ else if (/Chrome\//.test(navigator.userAgent))
+ return true;
+ else
+ return null;
+ };
+
+ return CodeMirror;
+})();
View
1,176 koha-tmpl/intranet-tmpl/prog/en/lib/codemirror/js/editor.js
@@ -0,0 +1,1176 @@
+/* The Editor object manages the content of the editable frame. It
+ * catches events, colours nodes, and indents lines. This file also
+ * holds some functions for transforming arbitrary DOM structures into
+ * plain sequences of <span> and <br> elements
+ */
+
+var safeWhiteSpace, splitSpaces;
+function setWhiteSpaceModel(collapsing) {
+ safeWhiteSpace = collapsing ?
+ // Make sure a string does not contain two consecutive 'collapseable'
+ // whitespace characters.
+ function(n) {
+ var buffer = [], nb = true;
+ for (; n > 0; n--) {
+ buffer.push((nb || n == 1) ? nbsp : " ");
+ nb = !nb;
+ }
+ return buffer.join("");
+ } :
+ function(n) {
+ var buffer = [];
+ for (; n > 0; n--) buffer.push(" ");
+ return buffer.join("");
+ };
+ splitSpaces = collapsing ?
+ // Create a set of white-space characters that will not be collapsed
+ // by the browser, but will not break text-wrapping either.
+ function(string) {
+ if (string.charAt(0) == " ") string = nbsp + string.slice(1);
+ return string.replace(/[\t \u00a0]{2,}/g, function(s) {return safeWhiteSpace(s.length);});
+ } :
+ function(string) {return string;};
+}
+
+function makePartSpan(value, doc) {
+ var text = value;
+ if (value.nodeType == 3) text = value.nodeValue;
+ else value = doc.createTextNode(text);
+
+ var span = doc.createElement("SPAN");
+ span.isPart = true;
+ span.appendChild(value);
+ span.currentText = text;
+ return span;
+}
+
+var Editor = (function(){
+ // The HTML elements whose content should be suffixed by a newline
+ // when converting them to flat text.
+ var newlineElements = {"P": true, "DIV": true, "LI": true};
+
+ function asEditorLines(string) {
+ return splitSpaces(string.replace(/\t/g, " ").replace(/\u00a0/g, " ")).replace(/\r\n?/g, "\n").split("\n");
+ }
+
+ var internetExplorer = document.selection && window.ActiveXObject && /MSIE/.test(navigator.userAgent);
+
+ // Helper function for traverseDOM. Flattens an arbitrary DOM node
+ // into an array of textnodes and <br> tags.
+ function simplifyDOM(root) {
+ var doc = root.ownerDocument;
+ var result = [];
+ var leaving = false;
+
+ function simplifyNode(node) {
+ if (node.nodeType == 3) {
+ var text = node.nodeValue = splitSpaces(node.nodeValue.replace(/[\n\r]/g, ""));
+ if (text.length) leaving = false;
+ result.push(node);
+ }
+ else if (node.nodeName == "BR" && node.childNodes.length == 0) {
+ leaving = true;
+ result.push(node);
+ }
+ else {
+ forEach(node.childNodes, simplifyNode);
+ if (!leaving && newlineElements.hasOwnProperty(node.nodeName)) {
+ leaving = true;
+ result.push(doc.createElement("BR"));
+ }
+ }
+ }
+
+ simplifyNode(root);
+ return result;
+ }
+
+ // Creates a MochiKit-style iterator that goes over a series of DOM
+ // nodes. The values it yields are strings, the textual content of
+ // the nodes. It makes sure that all nodes up to and including the
+ // one whose text is being yielded have been 'normalized' to be just
+ // <span> and <br> elements.
+ // See the story.html file for some short remarks about the use of
+ // continuation-passing style in this iterator.
+ function traverseDOM(start){
+ function yield(value, c){cc = c; return value;}
+ function push(fun, arg, c){return function(){return fun(arg, c);};}
+ function stop(){cc = stop; throw StopIteration;};
+ var cc = push(scanNode, start, stop);
+ var owner = start.ownerDocument;
+ var nodeQueue = [];
+
+ // Create a function that can be used to insert nodes after the
+ // one given as argument.
+ function pointAt(node){
+ var parent = node.parentNode;
+ var next = node.nextSibling;
+ return function(newnode) {
+ parent.insertBefore(newnode, next);
+ };
+ }
+ var point = null;
+
+ // Insert a normalized node at the current point. If it is a text
+ // node, wrap it in a <span>, and give that span a currentText
+ // property -- this is used to cache the nodeValue, because
+ // directly accessing nodeValue is horribly slow on some browsers.
+ // The dirty property is used by the highlighter to determine
+ // which parts of the document have to be re-highlighted.
+ function insertPart(part){
+ var text = "\n";
+ if (part.nodeType == 3) {
+ select.snapshotChanged();
+ part = makePartSpan(part, owner);
+ text = part.currentText;
+ }
+ part.dirty = true;
+ nodeQueue.push(part);
+ point(part);
+ return text;
+ }
+
+ // Extract the text and newlines from a DOM node, insert them into
+ // the document, and yield the textual content. Used to replace
+ // non-normalized nodes.
+ function writeNode(node, c){
+ var toYield = [];
+ forEach(simplifyDOM(node), function(part) {
+ toYield.push(insertPart(part));
+ });
+ return yield(toYield.join(""), c);
+ }
+
+ // Check whether a node is a normalized <span> element.
+ function partNode(node){
+ if (node.nodeName == "SPAN" && node.childNodes.length == 1 && node.firstChild.nodeType == 3 && node.isPart) {
+ node.currentText = node.firstChild.nodeValue;
+ return !/[\n\t\r]/.test(node.currentText);
+ }
+ return false;
+ }
+
+ // Handle a node. Add its successor to the continuation if there
+ // is one, find out whether the node is normalized. If it is,
+ // yield its content, otherwise, normalize it (writeNode will take
+ // care of yielding).
+ function scanNode(node, c){
+ if (node.nextSibling)
+ c = push(scanNode, node.nextSibling, c);
+
+ if (partNode(node)){
+ nodeQueue.push(node);
+ return yield(node.currentText, c);
+ }
+ else if (node.nodeName == "BR") {
+ nodeQueue.push(node);
+ return yield("\n", c);
+ }
+ else {
+ point = pointAt(node);
+ removeElement(node);
+ return writeNode(node, c);
+ }
+ }
+
+ // MochiKit iterators are objects with a next function that
+ // returns the next value or throws StopIteration when there are
+ // no more values.
+ return {next: function(){return cc();}, nodes: nodeQueue};
+ }
+
+ // Determine the text size of a processed node.
+ function nodeSize(node) {
+ if (node.nodeName == "BR")
+ return 1;
+ else
+ return node.currentText.length;
+ }
+
+ // Search backwards through the top-level nodes until the next BR or
+ // the start of the frame.
+ function startOfLine(node) {
+ while (node && node.nodeName != "BR") node = node.previousSibling;
+ return node;
+ }
+ function endOfLine(node, container) {
+ if (!node) node = container.firstChild;
+ while (node && node.nodeName != "BR") node = node.nextSibling;
+ return node;
+ }
+
+ function cleanText(text) {
+ return text.replace(/\u00a0/g, " ");
+ }
+
+ // Client interface for searching the content of the editor. Create
+ // these by calling CodeMirror.getSearchCursor. To use, call
+ // findNext on the resulting object -- this returns a boolean
+ // indicating whether anything was found, and can be called again to
+ // skip to the next find. Use the select and replace methods to
+ // actually do something with the found locations.
+ function SearchCursor(editor, string, fromCursor) {
+ this.editor = editor;
+ this.history = editor.history;
+ this.history.commit();
+
+ // Are we currently at an occurrence of the search string?
+ this.atOccurrence = false;
+ // The object stores a set of nodes coming after its current
+ // position, so that when the current point is taken out of the
+ // DOM tree, we can still try to continue.
+ this.fallbackSize = 15;
+ var cursor;
+ // Start from the cursor when specified and a cursor can be found.
+ if (fromCursor && (cursor = select.cursorPos(this.editor.container))) {
+ this.line = cursor.node;
+ this.offset = cursor.offset;
+ }
+ else {
+ this.line = null;
+ this.offset = 0;
+ }
+ this.valid = !!string;
+
+ // Create a matcher function based on the kind of string we have.
+ var target = string.split("\n"), self = this;;
+ this.matches = (target.length == 1) ?
+ // For one-line strings, searching can be done simply by calling
+ // indexOf on the current line.
+ function() {
+ var match = cleanText(self.history.textAfter(self.line).slice(self.offset)).indexOf(string);
+ if (match > -1)
+ return {from: {node: self.line, offset: self.offset + match},
+ to: {node: self.line, offset: self.offset + match + string.length}};
+ } :
+ // Multi-line strings require internal iteration over lines, and
+ // some clunky checks to make sure the first match ends at the
+ // end of the line and the last match starts at the start.
+ function() {
+ var firstLine = cleanText(self.history.textAfter(self.line).slice(self.offset));
+ var match = firstLine.lastIndexOf(target[0]);
+ if (match == -1 || match != firstLine.length - target[0].length)
+ return false;
+ var startOffset = self.offset + match;
+
+ var line = self.history.nodeAfter(self.line);
+ for (var i = 1; i < target.length - 1; i++) {
+ if (cleanText(self.history.textAfter(line)) != target[i])
+ return false;
+ line = self.history.nodeAfter(line);
+ }
+
+ if (cleanText(self.history.textAfter(line)).indexOf(target[target.length - 1]) != 0)
+ return false;
+
+ return {from: {node: self.line, offset: startOffset},
+ to: {node: line, offset: target[target.length - 1].length}};
+ };
+ }
+
+ SearchCursor.prototype = {
+ findNext: function() {
+ if (!this.valid) return false;
+ this.atOccurrence = false;
+ var self = this;
+
+ // Go back to the start of the document if the current line is
+ // no longer in the DOM tree.
+ if (this.line && !this.line.parentNode) {
+ this.line = null;
+ this.offset = 0;
+ }
+
+ // Set the cursor's position one character after the given
+ // position.
+ function saveAfter(pos) {