X-Git-Url: http://git.vanrenterghem.biz/git.ikiwiki.info.git/blobdiff_plain/8f4878d8aee676157d9d24e926dbd80d440b581a..b43e5c56eda051a364c826b481465e1fe8ac553b:/IkiWiki.pm diff --git a/IkiWiki.pm b/IkiWiki.pm index a39e317f5..62b54373b 100644 --- a/IkiWiki.pm +++ b/IkiWiki.pm @@ -9,39 +9,42 @@ use open qw{:utf8 :std}; use vars qw{%config %links %oldlinks %oldpagemtime %pagectime %pagecase %renderedfiles %oldrenderedfiles %pagesources %depends %hooks - %forcerebuild}; + %forcerebuild $gettext_obj}; use Exporter q{import}; our @EXPORT = qw(hook debug error template htmlpage add_depends pagespec_match bestlink htmllink readfile writefile pagetype srcfile pagename - displaytime will_render + displaytime will_render gettext %config %links %renderedfiles %pagesources); -our $VERSION = 1.01; # plugin interface version +our $VERSION = 1.02; # plugin interface version, next is ikiwiki version +our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE +my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE # Optimisation. use Memoize; memoize("abs2rel"); memoize("pagespec_translate"); - -my $installdir=''; # INSTALLDIR_AUTOREPLACE done by Makefile, DNE -our $version='unknown'; # VERSION_AUTOREPLACE done by Makefile, DNE +memoize("file_pruned"); sub defaultconfig () { #{{{ - wiki_file_prune_regexp => qr{((^|/).svn/|\.\.|^\.|\/\.|\.x?html?$|\.rss$|\.atom$|.arch-ids/|{arch}/)}, - wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]]+)\]\]/, + wiki_file_prune_regexps => [qr/\.\./, qr/^\./, qr/\/\./, + qr/\.x?html?$/, qr/\.ikiwiki-new$/, + qr/(^|\/).svn\//, qr/.arch-ids\//, qr/{arch}\//], + wiki_link_regexp => qr/\[\[(?:([^\]\|]+)\|)?([^\s\]#]+)(?:#([^\s\]]+))?\]\]/, wiki_file_regexp => qr/(^[-[:alnum:]_.:\/+]+$)/, + web_commit_regexp => qr/^web commit (by (.*?(?=: |$))|from (\d+\.\d+\.\d+\.\d+)):?(.*)/, verbose => 0, syslog => 0, wikiname => "wiki", default_pageext => "mdwn", cgi => 0, - rcs => 'svn', + post_commit => 0, + rcs => '', notify => 0, url => '', cgiurl => '', historyurl => '', diffurl => '', - anonok => 0, rss => 0, atom => 0, discussion => 1, @@ -53,6 +56,8 @@ sub defaultconfig () { #{{{ wrappermode => undef, svnrepo => undef, svnpath => "trunk", + gitorigin_branch => "origin", + gitmaster_branch => "master", srcdir => undef, destdir => undef, pingurl => [], @@ -61,11 +66,13 @@ sub defaultconfig () { #{{{ setup => undef, adminuser => undef, adminemail => undef, - plugin => [qw{mdwn inline htmlscrubber}], + plugin => [qw{mdwn inline htmlscrubber passwordauth signinedit + lockedit conditional}], timeformat => '%c', locale => undef, sslcookie => 0, httpauth => 0, + userdir => "", } #}}} sub checkconfig () { #{{{ @@ -76,12 +83,16 @@ sub checkconfig () { #{{{ } if (defined $config{locale}) { eval q{use POSIX}; - $ENV{LANG} = $config{locale} - if POSIX::setlocale(&POSIX::LC_TIME, $config{locale}); + error($@) if $@; + if (POSIX::setlocale(&POSIX::LC_ALL, $config{locale})) { + $ENV{LANG}=$config{locale}; + $gettext_obj=undef; + } } if ($config{w3mmode}) { eval q{use Cwd q{abs_path}}; + error($@) if $@; $config{srcdir}=possibly_foolish_untaint(abs_path($config{srcdir})); $config{destdir}=possibly_foolish_untaint(abs_path($config{destdir})); $config{cgiurl}="file:///\$LIB/ikiwiki-w3m.cgi/".$config{cgiurl} @@ -90,10 +101,7 @@ sub checkconfig () { #{{{ } if ($config{cgi} && ! length $config{url}) { - error("Must specify url to wiki with --url when using --cgi\n"); - } - if (($config{rss} || $config{atom}) && ! length $config{url}) { - error("Must specify url to wiki with --url when using --rss or --atom\n"); + error(gettext("Must specify url to wiki with --url when using --cgi")); } $config{wikistatedir}="$config{srcdir}/.ikiwiki" @@ -113,13 +121,8 @@ sub checkconfig () { #{{{ } #}}} sub loadplugins () { #{{{ - foreach my $plugin (@{$config{plugin}}) { - my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin); - eval qq{use $mod}; - if ($@) { - error("Failed to load plugin $mod: $@"); - } - } + loadplugin($_) foreach @{$config{plugin}}; + run_hooks(getopt => sub { shift->() }); if (grep /^-/, @ARGV) { print STDERR "Unknown option: $_\n" @@ -128,13 +131,31 @@ sub loadplugins () { #{{{ } } #}}} -sub error ($) { #{{{ +sub loadplugin ($) { #{{{ + my $plugin=shift; + + return if grep { $_ eq $plugin} @{$config{disable_plugins}}; + + my $mod="IkiWiki::Plugin::".possibly_foolish_untaint($plugin); + eval qq{use $mod}; + if ($@) { + error("Failed to load plugin $mod: $@"); + } +} #}}} + +sub error ($;$) { #{{{ + my $message=shift; + my $cleaner=shift; if ($config{cgi}) { print "Content-type: text/html\n\n"; - print misctemplate("Error", "

Error: @_

"); + print misctemplate(gettext("Error"), + "

".gettext("Error").": $message

"); + } + log_message(debug => $message) if $config{syslog}; + if (defined $cleaner) { + $cleaner->(); } - log_message(error => @_); - exit(1); + die $message."\n"; } #}}} sub debug ($) { #{{{ @@ -154,7 +175,7 @@ sub log_message ($$) { #{{{ $log_open=1; } eval { - Sys::Syslog::syslog($type, join(" ", @_)); + Sys::Syslog::syslog($type, "%s", join(" ", @_)); } } elsif (! $config{cgi}) { @@ -217,9 +238,10 @@ sub srcfile ($) { #{{{ error("internal error: $file cannot be found"); } #}}} -sub readfile ($;$) { #{{{ +sub readfile ($;$$) { #{{{ my $file=shift; my $binary=shift; + my $wantfd=shift; if (-l $file) { error("cannot read a symlink ($file)"); @@ -228,16 +250,18 @@ sub readfile ($;$) { #{{{ local $/=undef; open (IN, $file) || error("failed to read $file: $!"); binmode(IN) if ($binary); + return \*IN if $wantfd; my $ret=; - close IN; + close IN || error("failed to read $file: $!"); return $ret; } #}}} -sub writefile ($$$;$) { #{{{ +sub writefile ($$$;$$) { #{{{ my $file=shift; # can include subdirs my $destdir=shift; # directory to put file in my $content=shift; my $binary=shift; + my $writer=shift; my $test=$file; while (length $test) { @@ -246,8 +270,12 @@ sub writefile ($$$;$) { #{{{ } $test=dirname($test); } + my $newfile="$destdir/$file.ikiwiki-new"; + if (-l $newfile) { + error("cannot write to a symlink ($newfile)"); + } - my $dir=dirname("$destdir/$file"); + my $dir=dirname($newfile); if (! -d $dir) { my $d=""; foreach my $s (split(m!/+!, $dir)) { @@ -257,13 +285,24 @@ sub writefile ($$$;$) { #{{{ } } } - - open (OUT, ">$destdir/$file") || error("failed to write $destdir/$file: $!"); + + my $cleanup = sub { unlink($newfile) }; + open (OUT, ">$newfile") || error("failed to write $newfile: $!", $cleanup); binmode(OUT) if ($binary); - print OUT $content; - close OUT; + if ($writer) { + $writer->(\*OUT, $cleanup); + } + else { + if (length $content) { + print OUT $content || error("failed writing to $newfile: $!", $cleanup); + } + } + close OUT || error("failed saving $newfile: $!", $cleanup); + rename($newfile, "$destdir/$file") || + error("failed renaming $newfile to $destdir/$file: $!", $cleanup); } #}}} +my %cleared; sub will_render ($$;$) { #{{{ my $page=shift; my $dest=shift; @@ -275,11 +314,12 @@ sub will_render ($$;$) { #{{{ error("$config{destdir}/$dest independently created, not overwriting with version from $page"); } - if (! $clear) { + if (! $clear || $cleared{$page}) { $renderedfiles{$page}=[$dest, grep { $_ ne $dest } @{$renderedfiles{$page}}]; } else { $renderedfiles{$page}=[$dest]; + $cleared{$page}=1; } } #}}} @@ -288,6 +328,11 @@ sub bestlink ($$) { #{{{ my $link=shift; my $cwd=$page; + if ($link=~s/^\/+//) { + # absolute links + $cwd=""; + } + do { my $l=$cwd; $l.="/" if length $l; @@ -301,6 +346,10 @@ sub bestlink ($$) { #{{{ } } while $cwd=~s!/?[^/]+$!!; + if (length $config{userdir} && exists $links{"$config{userdir}/".lc($link)}) { + return "$config{userdir}/".lc($link); + } + #print STDERR "warning: page $page, broken link: $link\n"; return ""; } #}}} @@ -311,17 +360,23 @@ sub isinlinableimage ($) { #{{{ $file=~/\.(png|gif|jpg|jpeg)$/i; } #}}} -sub pagetitle ($) { #{{{ +sub pagetitle ($;$) { #{{{ my $page=shift; - $page=~s/__(\d+)__/&#$1;/g; - $page=~y/_/ /; + my $unescaped=shift; + + if ($unescaped) { + $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : chr($2)/eg; + } + else { + $page=~s/(__(\d+)__|_)/$1 eq '_' ? ' ' : "&#$2;"/eg; + } + return $page; } #}}} sub titlepage ($) { #{{{ my $title=shift; - $title=~y/ /_/; - $title=~s/([^-[:alnum:]_:+\/.])/"__".ord($1)."__"/eg; + $title=~s/([^-[:alnum:]:+\/.])/$1 eq ' ' ? '_' : "__".ord($1)."__"/eg; return $title; } #}}} @@ -344,7 +399,7 @@ sub baseurl (;$) { #{{{ sub abs2rel ($$) { #{{{ # Work around very innefficient behavior in File::Spec if abs2rel # is passed two relative paths. It's much faster if paths are - # absolute! (Debian bug #376658) + # absolute! (Debian bug #376658; fixed in debian unstable now) my $path="/".shift; my $base="/".shift; @@ -358,40 +413,43 @@ sub displaytime ($) { #{{{ my $time=shift; eval q{use POSIX}; + error($@) if $@; # strftime doesn't know about encodings, so make sure # its output is properly treated as utf8 return decode_utf8(POSIX::strftime( $config{timeformat}, localtime($time))); } #}}} -sub htmllink ($$$;$$$) { #{{{ +sub htmllink ($$$;@) { #{{{ my $lpage=shift; # the page doing the linking my $page=shift; # the page that will contain the link (different for inline) my $link=shift; - my $noimageinline=shift; # don't turn links into inline html images - my $forcesubpage=shift; # force a link to a subpage - my $linktext=shift; # set to force the link text to something + my %opts=@_; my $bestlink; - if (! $forcesubpage) { + if (! $opts{forcesubpage}) { $bestlink=bestlink($lpage, $link); } else { $bestlink="$lpage/".lc($link); } - $linktext=pagetitle(basename($link)) unless defined $linktext; + my $linktext; + if (defined $opts{linktext}) { + $linktext=$opts{linktext}; + } + else { + $linktext=pagetitle(basename($link)); + } return "$linktext" if length $bestlink && $page eq $bestlink; - # TODO BUG: %renderedfiles may not have it, if the linked to page - # was also added and isn't yet rendered! Note that this bug is - # masked by the bug that makes all new files be rendered twice. if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) { $bestlink=htmlpage($bestlink); } if (! grep { $_ eq $bestlink } map { @{$_} } values %renderedfiles) { + return $linktext unless length $config{cgiurl}; return " "create", page => lc($link), from => $page). "\">?$linktext" @@ -399,9 +457,14 @@ sub htmllink ($$$;$$$) { #{{{ $bestlink=abs2rel($bestlink, dirname($page)); - if (! $noimageinline && isinlinableimage($bestlink)) { + if (! $opts{noimageinline} && isinlinableimage($bestlink)) { return "\"$linktext\""; } + + if (defined $opts{anchor}) { + $bestlink.="#".$opts{anchor}; + } + return "$linktext"; } #}}} @@ -436,18 +499,20 @@ sub linkify ($$$) { #{{{ my $content=shift; $content =~ s{(\\?)$config{wiki_link_regexp}}{ - $2 ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), 0, 0, pagetitle($2))) - : ( $1 ? "[[$3]]" : htmllink($lpage, $page, titlepage($3))) + defined $2 + ? ( $1 ? "[[$2|$3]]" : htmllink($lpage, $page, titlepage($3), anchor => $4, linktext => pagetitle($2))) + : ( $1 ? "[[$3]]" : htmllink($lpage, $page, titlepage($3), anchor => $4)) }eg; return $content; } #}}} my %preprocessing; -sub preprocess ($$$) { #{{{ +sub preprocess ($$$;$) { #{{{ my $page=shift; # the page the data comes from my $destpage=shift; # the page the data will appear in (different for inline) my $content=shift; + my $scan=shift; my $handle=sub { my $escape=shift; @@ -457,6 +522,7 @@ sub preprocess ($$$) { #{{{ return "[[$command $params]]"; } elsif (exists $hooks{preprocess}{$command}) { + return "" if $scan && ! $hooks{preprocess}{$command}{scan}; # Note: preserve order of params, some plugins may # consider it significant. my @params; @@ -486,7 +552,13 @@ sub preprocess ($$$) { #{{{ if ($preprocessing{$page}++ > 3) { # Avoid loops of preprocessed pages preprocessing # other pages that preprocess them, etc. - return "[[$command preprocessing loop detected on $page at depth $preprocessing{$page}]]"; + #translators: The first parameter is a + #translators: preprocessor directive name, + #translators: the second a page name, the + #translators: third a number. + return "[[".sprintf(gettext("%s preprocessing loop detected on %s at depth %i"), + $command, $page, $preprocessing{$page}). + "]]"; } my $ret=$hooks{preprocess}{$command}{call}->( @params, @@ -505,7 +577,7 @@ sub preprocess ($$$) { #{{{ return $content; } #}}} -sub filter ($$) { +sub filter ($$) { #{{{ my $page=shift; my $content=shift; @@ -514,7 +586,7 @@ sub filter ($$) { }); return $content; -} +} #}}} sub indexlink () { #{{{ return "$config{wikiname}"; @@ -528,11 +600,11 @@ sub lockwiki () { #{{{ } open(WIKILOCK, ">$config{wikistatedir}/lockfile") || error ("cannot write to $config{wikistatedir}/lockfile: $!"); - if (! flock(WIKILOCK, 2 | 4)) { + if (! flock(WIKILOCK, 2 | 4)) { # LOCK_EX | LOCK_NB debug("wiki seems to be locked, waiting for lock"); my $wait=600; # arbitrary, but don't hang forever to # prevent process pileup - for (1..600) { + for (1..$wait) { return if flock(WIKILOCK, 2 | 4); sleep 1; } @@ -544,6 +616,29 @@ sub unlockwiki () { #{{{ close WIKILOCK; } #}}} +sub commit_hook_enabled () { #{{{ + open(COMMITLOCK, "+>$config{wikistatedir}/commitlock") || + error ("cannot write to $config{wikistatedir}/commitlock: $!"); + if (! flock(COMMITLOCK, 1 | 4)) { # LOCK_SH | LOCK_NB to test + close COMMITLOCK; + return 0; + } + close COMMITLOCK; + return 1; +} #}}} + +sub disable_commit_hook () { #{{{ + open(COMMITLOCK, ">$config{wikistatedir}/commitlock") || + error ("cannot write to $config{wikistatedir}/commitlock: $!"); + if (! flock(COMMITLOCK, 2)) { # LOCK_EX + error("failed to get commit lock"); + } +} #}}} + +sub enable_commit_hook () { #{{{ + close COMMITLOCK; +} #}}} + sub loadindex () { #{{{ open (IN, "$config{wikistatedir}/index") || return; while () { @@ -581,35 +676,56 @@ sub saveindex () { #{{{ if (! -d $config{wikistatedir}) { mkdir($config{wikistatedir}); } - open (OUT, ">$config{wikistatedir}/index") || - error("cannot write to $config{wikistatedir}/index: $!"); + my $newfile="$config{wikistatedir}/index.new"; + my $cleanup = sub { unlink($newfile) }; + open (OUT, ">$newfile") || error("cannot write to $newfile: $!", $cleanup); foreach my $page (keys %oldpagemtime) { next unless $oldpagemtime{$page}; my $line="mtime=$oldpagemtime{$page} ". "ctime=$pagectime{$page} ". "src=$pagesources{$page}"; $line.=" dest=$_" foreach @{$renderedfiles{$page}}; - $line.=" link=$_" foreach @{$links{$page}}; + my %count; + $line.=" link=$_" foreach grep { ++$count{$_} == 1 } @{$links{$page}}; if (exists $depends{$page}) { $line.=" depends=".encode_entities($depends{$page}, " \t\n"); } - print OUT $line."\n"; + print OUT $line."\n" || error("failed writing to $newfile: $!", $cleanup); + } + close OUT || error("failed saving to $newfile: $!", $cleanup); + rename($newfile, "$config{wikistatedir}/index") || + error("failed renaming $newfile to $config{wikistatedir}/index", $cleanup); +} #}}} + +sub template_file ($) { #{{{ + my $template=shift; + + foreach my $dir ($config{templatedir}, "$installdir/share/ikiwiki/templates") { + return "$dir/$template" if -e "$dir/$template"; } - close OUT; + return undef; } #}}} sub template_params (@) { #{{{ - my $filename=shift; - + my $filename=template_file(shift); + + if (! defined $filename) { + return if wantarray; + return ""; + } + require HTML::Template; - return filter => sub { + my @ret=( + filter => sub { my $text_ref = shift; $$text_ref=&Encode::decode_utf8($$text_ref); }, - filename => "$config{templatedir}/$filename", + filename => $filename, loop_context_vars => 1, die_on_bad_params => 0, - @_; + @_ + ); + return wantarray ? @ret : {@ret}; } #}}} sub template ($;@) { #{{{ @@ -641,6 +757,8 @@ sub hook (@) { # {{{ if (! exists $param{type} || ! ref $param{call} || ! exists $param{id}) { error "hook requires type, call, and id parameters"; } + + return if $param{no_override} && exists $hooks{$param{type}}{$param{id}}; $hooks{$param{type}}{$param{id}}=\%param; } # }}} @@ -652,7 +770,15 @@ sub run_hooks ($$) { # {{{ my $sub=shift; if (exists $hooks{$type}) { + my @deferred; foreach my $id (keys %{$hooks{$type}}) { + if ($hooks{$type}{$id}{last}) { + push @deferred, $id; + next; + } + $sub->($hooks{$type}{$id}{call}); + } + foreach my $id (@deferred) { $sub->($hooks{$type}{$id}{call}); } } @@ -695,6 +821,49 @@ sub safequote ($) { #{{{ return "q{$s}"; } #}}} +sub add_depends ($$) { #{{{ + my $page=shift; + my $pagespec=shift; + + if (! exists $depends{$page}) { + $depends{$page}=$pagespec; + } + else { + $depends{$page}=pagespec_merge($depends{$page}, $pagespec); + } +} # }}} + +sub file_pruned ($$) { #{{{ + require File::Spec; + my $file=File::Spec->canonpath(shift); + my $base=File::Spec->canonpath(shift); + $file=~s#^\Q$base\E/*##; + + my $regexp='('.join('|', @{$config{wiki_file_prune_regexps}}).')'; + $file =~ m/$regexp/; +} #}}} + +sub gettext { #{{{ + # Only use gettext in the rare cases it's needed. + if (exists $ENV{LANG} || exists $ENV{LC_ALL} || exists $ENV{LC_MESSAGES}) { + if (! $gettext_obj) { + $gettext_obj=eval q{ + use Locale::gettext q{textdomain}; + Locale::gettext->domain('ikiwiki') + }; + if ($@) { + print STDERR "$@"; + $gettext_obj=undef; + return shift; + } + } + return $gettext_obj->get(shift); + } + else { + return shift; + } +} #}}} + sub pagespec_merge ($$) { #{{{ my $a=shift; my $b=shift; @@ -735,39 +904,46 @@ sub pagespec_translate ($) { #{{{ elsif ($word eq "(" || $word eq ")" || $word eq "!") { $code.=" ".$word; } - elsif ($word =~ /^(link|backlink|created_before|created_after|creation_month|creation_year|creation_day)\((.+)\)$/) { - $code.=" match_$1(\$page, ".safequote($2).")"; + elsif ($word =~ /^(\w+)\((.*)\)$/) { + if (exists $IkiWiki::PageSpec::{"match_$1"}) { + $code.=" IkiWiki::PageSpec::match_$1(\$page, ".safequote($2).")"; + } + else { + $code.=" 0"; + } } else { - $code.=" match_glob(\$page, ".safequote($word).")"; + $code.=" IkiWiki::PageSpec::match_glob(\$page, ".safequote($word).", \$from)"; } } return $code; } #}}} -sub add_depends ($$) { #{{{ - my $page=shift; - my $pagespec=shift; - - if (! exists $depends{$page}) { - $depends{$page}=$pagespec; - } - else { - $depends{$page}=pagespec_merge($depends{$page}, $pagespec); - } -} # }}} - -sub pagespec_match ($$) { #{{{ +sub pagespec_match ($$;$) { #{{{ my $page=shift; my $spec=shift; + my $from=shift; return eval pagespec_translate($spec); } #}}} -sub match_glob ($$) { #{{{ +package IkiWiki::PageSpec; + +sub match_glob ($$$) { #{{{ my $page=shift; my $glob=shift; + my $from=shift; + if (! defined $from){ + $from = ""; + } + + # relative matching + if ($glob =~ m!^\./!) { + $from=~s!/?[^/]+$!!; + $glob=~s!^\./!!; + $glob="$from/$glob" if length $from; + } # turn glob into safe regexp $glob=quotemeta($glob); @@ -781,7 +957,7 @@ sub match_link ($$) { #{{{ my $page=shift; my $link=lc(shift); - my $links = $links{$page} or return undef; + my $links = $IkiWiki::links{$page} or return undef; foreach my $p (@$links) { return 1 if lc $p eq $link; } @@ -796,8 +972,8 @@ sub match_created_before ($$) { #{{{ my $page=shift; my $testpage=shift; - if (exists $pagectime{$testpage}) { - return $pagectime{$page} < $pagectime{$testpage}; + if (exists $IkiWiki::pagectime{$testpage}) { + return $IkiWiki::pagectime{$page} < $IkiWiki::pagectime{$testpage}; } else { return 0; @@ -808,8 +984,8 @@ sub match_created_after ($$) { #{{{ my $page=shift; my $testpage=shift; - if (exists $pagectime{$testpage}) { - return $pagectime{$page} > $pagectime{$testpage}; + if (exists $IkiWiki::pagectime{$testpage}) { + return $IkiWiki::pagectime{$page} > $IkiWiki::pagectime{$testpage}; } else { return 0; @@ -817,15 +993,15 @@ sub match_created_after ($$) { #{{{ } #}}} sub match_creation_day ($$) { #{{{ - return ((gmtime($pagectime{shift()}))[3] == shift); + return ((gmtime($IkiWiki::pagectime{shift()}))[3] == shift); } #}}} sub match_creation_month ($$) { #{{{ - return ((gmtime($pagectime{shift()}))[4] + 1 == shift); + return ((gmtime($IkiWiki::pagectime{shift()}))[4] + 1 == shift); } #}}} sub match_creation_year ($$) { #{{{ - return ((gmtime($pagectime{shift()}))[5] + 1900 == shift); + return ((gmtime($IkiWiki::pagectime{shift()}))[5] + 1900 == shift); } #}}} 1