{"thread":{"id":"29864","subject":"[RFC/PATCHv3 0/2] gitweb: Beginnings of splitting gitweb into modules","startedAt":"2012-03-06T22:41:40Z","lastAt":"2012-03-06T22:41:42Z","messageCount":3,"participants":["Jakub Narebski"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"186270","messageId":"1331073702-9071-1-git-send-email-jnareb@gmail.com","threadId":"29864","inReplyTo":null,"subject":"[RFC/PATCHv3 0/2] gitweb: Beginnings of splitting gitweb into modules","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2012-03-06T22:41:40Z","receivedAt":"2012-03-06T22:41:40Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Gitweb is currently next to largest file (after gitk) in git sources,\nhaving more than 235KB, with more than 7,500 lines.\n\nTherefore adding any large feature, like gitweb caching by J.H. and my\nrewrite of it, or \"gitweb admin/write\" [failed] GSoC 2010 project by\nPavan Kumar Sunkara, would require for the new code to be added as a\nseparate module.  Otherwise gitweb would fast become unmaintainable.\n\nNot in all cases splitting gitweb upfront would be required.  If a new\nfeature is clearly separate from the rest of gitweb we can add this\nnew feature as a new module to be used by gitweb.  This is for example\nthe case for adding output caching support to gitweb.\n\nWhat must be done however is preparing the infrastructure for modular\ngitweb sources; to properly test such infrastructure we need at least\none gitweb module split off gitweb itself and used by it.  This patch\nseries implements this initial step.\n\nNote that JavaScript side of gitweb is already split into many smaller\nfiles for better maintability (and concatenated on build for better\nperformance) since 9a86dd5 (gitweb: Split JavaScript for maintability,\ncombining on build, 2011-04-28).\n\n\nThe major problem with splitting gitweb code into modules (similar to\nSVN::Web or Gitalist) is its error handling.  \n\nCurrently die_error() subroutine uses non-local jump out of subroutine\nto end of request.  This means however that die_error() must be in the\nsame file as run_request() subroutine, and therefore any module which\nmust use die_error() must include module with run_request()... and one\nof most natural places for run_request() is a main script.\n\nTo be able to decouple die_error() and run_request() we have to change\nthe way error handling is done.  \n\nOne approach is to go back to using 'exit' (which ModPerl::Registry\nredefines to not quit Perl interpreter) in die_error().  But this has\nserious disadvantages for other persistent environments: FastCGI (with\nCGI::Fast) and PSGI (emulated via CGI::Emulate::PSGI).  It wouldn't\neither solve problems with handling errors in capturing and caching\nlayer in the future gitweb output caching support.\n\nAnother solution, used e.g. by SVN::Web, is to handle errors and\ngenerate error pages using exceptions.  For handling exceptions we\nwould need something like Try::Tiny, for throwing them something like\nHTTP::Exception (which in turn uses Exception::Class), or\nHTTP::Throwable... though the latter is probably not so suitable for\nnon-ersistent environments like pure CGI (requires Moose and PSGI).\n\nWe should probably implement the latter solution... but this patch\nseries doesn't do that.\n\n\nThe previous version of this patch series was sent to git mailing list\nas\n\n  [PATCHv2 0/2] gitweb: Beginnings of splitting gitweb into modules\n  http://thread.gmane.org/gmane.comp.version-control.git/172659\n\n\nShortlog:\n~~~~~~~~~\nJakub Narebski (1):\n  gitweb: Prepare for splitting gitweb\n\nPavan Kumar Sunkara (1):\n  gitweb: Create Gitweb::Util module\n\nDiffstat:\n~~~~~~~~~\n gitweb/Makefile           |   22 ++-\n gitweb/gitweb.perl        |  401 ++---------------------------------------\n gitweb/lib/Gitweb/Util.pm |  438 +++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 472 insertions(+), 389 deletions(-)\n create mode 100755 gitweb/lib/Gitweb/Util.pm\n\n-- \n1.7.9\n"},{"id":"186271","messageId":"1331073702-9071-2-git-send-email-jnareb@gmail.com","threadId":"29864","inReplyTo":"1331073702-9071-1-git-send-email-jnareb@gmail.com","subject":"[RFC/PATCHv3 1/2] gitweb: Prepare for splitting gitweb","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2012-03-06T22:41:41Z","receivedAt":"2012-03-06T22:41:41Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"Prepare gitweb for being split into modules that would be installed\nin gitweblibdir, by default alongside gitweb in 'lib/' subdirectory.\n\nGitweb would search first in 'lib/' subdirectory from where it is\ninstalled, via\n\n  use lib __DIR__.'/lib';\n\n(This allow for tests to work with source version of gitweb without\nchanges.)  Then it searches in $(gitweblibdir) directory (set during\nbuild time), by default \"$(gitwebdir)/lib\", via\n\n  use lib \"++GITWEBLIBDIR++\";\n\nwhich is set to requested dir during building of gitweb.cgi.  Note\nthat 'use lib' assures no trailing duplicate entries in @INC.\n\n\nThis preparatory work allows to add new module to gitweb by simply\nadding\n\n  GITWEB_MODULES += <module>\n\nto gitweb/Makefile (assuming that the module is in 'gitweb/lib/'\ndirectory).\n\n\nAt Pavan Kumar Sankara suggestion gitweb/Makefile uses\n\n  install [OPTION]... SOURCE... DIRECTORY\n\nformat (2nd format) with single SOURCE rather than\n\n  install [OPTION]... SOURCE DEST\n\nformat (1st format) because of security reasons (race conditions).\nModern GNU install has `-T' / `--no-target-directory' option, but we\ncannot rely that the $(INSTALL) we are using supports this option.\n\nThe install-modules target in gitweb/Makefile uses shell 'for' loop,\ninstead of make's $(foreach) function, to avoid possible problem with\ngenerating a command line that exceeded the maximum argument list\nlength.\n\n\nWhile at it pass GITWEBLIBDIR environment variable in addition to\nGITWEB_TEST_INSTALLED down to $(MAKE) invocation in 'test-installed'\ntarget.  This is to allow testing installed version of gitweb and\ninstalled version of modules, for future tests which would check\nindividual gitweb (sub)modules.\n\nHelped-by: Pavan Kumar Sunkara <pavan.sss1991@gmail.com>\nHelped-by: Alejandro R. Sedeño <asedeno@mit.edu>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nChanges to v2:\n* Lost update to gitweb/INSTALL about possibility of installing\n  optional Perl modules for gitweb in 'lib/', alongside future\n  gitweb modules.\n\n gitweb/Makefile    |   20 ++++++++++++++++++--\n gitweb/gitweb.perl |   11 +++++++++++\n 2 files changed, 29 insertions(+), 2 deletions(-)\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex cd194d0..5f0a785 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -13,6 +13,7 @@ all::\n prefix ?= $(HOME)\n bindir ?= $(prefix)/bin\n gitwebdir ?= /var/www/cgi-bin\n+gitweblibdir ?= $(gitwebdir)/lib\n \n RM ?= rm -f\n INSTALL ?= install\n@@ -59,6 +60,7 @@ PERL_PATH  ?= /usr/bin/perl\n bindir_SQ = $(subst ','\\'',$(bindir))#'\n gitwebdir_SQ = $(subst ','\\'',$(gitwebdir))#'\n gitwebstaticdir_SQ = $(subst ','\\'',$(gitwebdir)/static)#'\n+gitweblibdir_SQ = $(subst ','\\'',$(gitweblibdir))#'\n SHELL_PATH_SQ = $(subst ','\\'',$(SHELL_PATH))#'\n PERL_PATH_SQ  = $(subst ','\\'',$(PERL_PATH))#'\n DESTDIR_SQ    = $(subst ','\\'',$(DESTDIR))#'\n@@ -129,6 +131,7 @@ GITWEB_JSLIB_FILES += static/js/blame_incremental.js\n GITWEB_REPLACE = \\\n \t-e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \\\n \t-e 's|++GIT_BINDIR++|$(bindir)|g' \\\n+\t-e 's|++GITWEBLIBDIR++|$(gitweblibdir)|g' \\\n \t-e 's|++GITWEB_CONFIG++|$(GITWEB_CONFIG)|g' \\\n \t-e 's|++GITWEB_CONFIG_SYSTEM++|$(GITWEB_CONFIG_SYSTEM)|g' \\\n \t-e 's|++GITWEB_CONFIG_COMMON++|$(GITWEB_CONFIG_COMMON)|g' \\\n@@ -174,16 +177,29 @@ test:\n \n test-installed:\n \tGITWEB_TEST_INSTALLED='$(DESTDIR_SQ)$(gitwebdir_SQ)' \\\n+\tGITWEBLIBDIR='$(DESTDIR_SQ)$(gitweblibdir_SQ)' \\\n \t\t$(MAKE) -C ../t gitweb-test\n \n ### Installation rules\n \n-install: all\n+install: all install-modules\n \t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebdir_SQ)'\n \t$(INSTALL) -m 755 $(GITWEB_PROGRAMS) '$(DESTDIR_SQ)$(gitwebdir_SQ)'\n \t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)'\n \t$(INSTALL) -m 644 $(GITWEB_FILES) '$(DESTDIR_SQ)$(gitwebstaticdir_SQ)'\n \n+install-modules:\n+\t$(INSTALL) -m 755 $(GITWEB_PROGRAMS) '$(DESTDIR_SQ)$(gitwebdir_SQ)/lib'\n+\tinstall_dirs=\"$(sort $(dir $(GITWEB_MODULES)))\" && \\\n+\tfor dir in $$install_dirs; do \\\n+\t\ttest -d '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$dir\" || \\\n+\t\t$(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$dir\"; \\\n+\tdone\n+\tgitweb_modules=\"$(GITWEB_MODULES)\" && \\\n+\tfor mod in $$gitweb_modules; do \\\n+\t\t$(INSTALL) -m 644 \"lib/$$mod\" '$(DESTDIR_SQ)$(gitweblibdir_SQ)'/\"$$(dirname $$mod)\"; \\\n+\tdone\n+\n ### Cleaning rules\n \n clean:\n@@ -191,5 +207,5 @@ clean:\n \t\tstatic/gitweb.min.js static/gitweb.min.css \\\n \t\tGITWEB-BUILD-OPTIONS\n \n-.PHONY: all clean install test test-installed .FORCE-GIT-VERSION-FILE FORCE\n+.PHONY: all clean install install-modules test test-installed .FORCE-GIT-VERSION-FILE FORCE\n \ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex e2e6a73..5902212 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -10,6 +10,17 @@\n use 5.008;\n use strict;\n use warnings;\n+\n+use File::Spec;\n+\n+# __DIR__ is excerpt from Dir::Self\n+sub __DIR__ () {\n+\tFile::Spec->rel2abs(join '', (File::Spec->splitpath(__FILE__))[0, 1]);\n+}\n+use lib \"++GITWEBLIBDIR++\";\n+use lib __DIR__ . '/lib';\n+\n+\n use CGI qw(:standard :escapeHTML -nosticky);\n use CGI::Util qw(unescape);\n use CGI::Carp qw(fatalsToBrowser set_message);\n-- \n1.7.9\n"},{"id":"186272","messageId":"1331073702-9071-3-git-send-email-jnareb@gmail.com","threadId":"29864","inReplyTo":"1331073702-9071-1-git-send-email-jnareb@gmail.com","subject":"[RFC/PATCHv3 2/2] gitweb: Create Gitweb::Util module","fromName":"Jakub Narebski","fromEmail":"jnareb@gmail.com","sentAt":"2012-03-06T22:41:42Z","receivedAt":"2012-03-06T22:41:42Z","isPatch":false,"sender":{"key":"jnareb@gmail.com","avatar":"https://avatars.githubusercontent.com/u/2706?v=4"},"body":"From: Pavan Kumar Sunkara <pavan.sss1991@gmail.com>\n\nCreate a Gitweb::Util module, which is meant to contain internal\nutilities used by gitweb.  Currently it includes all the\nquoting/unquoting and escaping subroutines that are used by the\ngitweb, HTML-aware string manipulation subroutines, and some file\nand filesystem operations.\n\nThe intent is to include in Gitweb::Util subroutines and variables\nthat do not require git or gitweb knowledge.\n\nUpdate gitweb/Makefile to install Gitweb::Util module alongside\ngitweb.\n\nThis was sort of part of [unfinished] Google Summer of Code 2010\nproject \"Splitting gitweb and developing write functionalities\n(Integrated web client for git)\" by Pavan Kumar Sunkara, mentored by\nChristian Couder and co-mentored by Petr Baudis.\n\n[This reduces size of gitweb.perl by around 5%]\n\nSigned-off-by: Pavan Kumar Sunkara <pavan.sss1991@gmail.com>\nSigned-off-by: Jakub Narebski <jnareb@gmail.com>\n---\nThis module was originally part of unfinished GSoC 2010 project with\nPavan Kumar Sunkara as a student\n\n  git://repo.or.cz/git/gsoc2010-gitweb.git\n\nThe module was originally named Gitweb::Escape.\n\n\nNOTE: I wanted to check if the change is pure code movement (and\nreorganization: in some places ordering of subroutines changed) with\ngit-blame (\"git blame -C -C -C -w\")... but it couldn't correctly\ndetect code movement of 5% of file.\n\n\nChanges from v2:\n* gitweb still needs Encoding module for marking query parameters as\n  UTF-8 with decode_utf8 (new in gitweb).\n\n* Move sanitize (new), chop_str, chop_and_escape_str,\n  esc_html_hl_regions (new), matchpos_list (new), esc_html_match_hl\n  (new), esc_html_match_hl_chopped (new), hash_set_multi,\n  get_file_owner, insert_file, normalize_link_target to Gitweb::Util\n\n gitweb/Makefile           |    2 +\n gitweb/gitweb.perl        |  390 +---------------------------------------\n gitweb/lib/Gitweb/Util.pm |  438 +++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 443 insertions(+), 387 deletions(-)\n create mode 100755 gitweb/lib/Gitweb/Util.pm\n\ndiff --git a/gitweb/Makefile b/gitweb/Makefile\nindex 5f0a785..df5bcdc 100644\n--- a/gitweb/Makefile\n+++ b/gitweb/Makefile\n@@ -127,6 +127,8 @@ GITWEB_JSLIB_FILES += static/js/javascript-detection.js\n GITWEB_JSLIB_FILES += static/js/adjust-timezone.js\n GITWEB_JSLIB_FILES += static/js/blame_incremental.js\n \n+# Modules: Gitweb::*\n+GITWEB_MODULES += Gitweb/Util.pm\n \n GITWEB_REPLACE = \\\n \t-e 's|++GIT_VERSION++|$(GIT_VERSION)|g' \\\ndiff --git a/gitweb/gitweb.perl b/gitweb/gitweb.perl\nindex 5902212..627364e 100755\n--- a/gitweb/gitweb.perl\n+++ b/gitweb/gitweb.perl\n@@ -29,6 +29,9 @@ use Fcntl ':mode';\n use File::Find qw();\n use File::Basename qw(basename);\n use Time::HiRes qw(gettimeofday tv_interval);\n+\n+use Gitweb::Util;\n+\n binmode STDOUT, ':utf8';\n \n our $t0 = [ gettimeofday() ];\n@@ -1467,139 +1470,6 @@ sub validate_refname {\n \treturn $input;\n }\n \n-# decode sequences of octets in utf8 into Perl's internal form,\n-# which is utf-8 with utf8 flag set if needed.  gitweb writes out\n-# in utf-8 thanks to \"binmode STDOUT, ':utf8'\" at beginning\n-sub to_utf8 {\n-\tmy $str = shift;\n-\treturn undef unless defined $str;\n-\n-\tif (utf8::is_utf8($str) || utf8::decode($str)) {\n-\t\treturn $str;\n-\t} else {\n-\t\treturn decode($fallback_encoding, $str, Encode::FB_DEFAULT);\n-\t}\n-}\n-\n-# quote unsafe chars, but keep the slash, even when it's not\n-# correct, but quoted slashes look too horrible in bookmarks\n-sub esc_param {\n-\tmy $str = shift;\n-\treturn undef unless defined $str;\n-\t$str =~ s/([^A-Za-z0-9\\-_.~()\\/:@ ]+)/CGI::escape($1)/eg;\n-\t$str =~ s/ /\\+/g;\n-\treturn $str;\n-}\n-\n-# the quoting rules for path_info fragment are slightly different\n-sub esc_path_info {\n-\tmy $str = shift;\n-\treturn undef unless defined $str;\n-\n-\t# path_info doesn't treat '+' as space (specially), but '?' must be escaped\n-\t$str =~ s/([^A-Za-z0-9\\-_.~();\\/;:@&= +]+)/CGI::escape($1)/eg;\n-\n-\treturn $str;\n-}\n-\n-# quote unsafe chars in whole URL, so some characters cannot be quoted\n-sub esc_url {\n-\tmy $str = shift;\n-\treturn undef unless defined $str;\n-\t$str =~ s/([^A-Za-z0-9\\-_.~();\\/;?:@&= ]+)/CGI::escape($1)/eg;\n-\t$str =~ s/ /\\+/g;\n-\treturn $str;\n-}\n-\n-# quote unsafe characters in HTML attributes\n-sub esc_attr {\n-\n-\t# for XHTML conformance escaping '\"' to '&quot;' is not enough\n-\treturn esc_html(@_);\n-}\n-\n-# replace invalid utf8 character with SUBSTITUTION sequence\n-sub esc_html {\n-\tmy $str = shift;\n-\tmy %opts = @_;\n-\n-\treturn undef unless defined $str;\n-\n-\t$str = to_utf8($str);\n-\t$str = $cgi->escapeHTML($str);\n-\tif ($opts{'-nbsp'}) {\n-\t\t$str =~ s/ /&nbsp;/g;\n-\t}\n-\t$str =~ s|([[:cntrl:]])|(($1 ne \"\\t\") ? quot_cec($1) : $1)|eg;\n-\treturn $str;\n-}\n-\n-# quote control characters and escape filename to HTML\n-sub esc_path {\n-\tmy $str = shift;\n-\tmy %opts = @_;\n-\n-\treturn undef unless defined $str;\n-\n-\t$str = to_utf8($str);\n-\t$str = $cgi->escapeHTML($str);\n-\tif ($opts{'-nbsp'}) {\n-\t\t$str =~ s/ /&nbsp;/g;\n-\t}\n-\t$str =~ s|([[:cntrl:]])|quot_cec($1)|eg;\n-\treturn $str;\n-}\n-\n-# Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)\n-sub sanitize {\n-\tmy $str = shift;\n-\n-\treturn undef unless defined $str;\n-\n-\t$str = to_utf8($str);\n-\t$str =~ s|([[:cntrl:]])|($1 =~ /[\\t\\n\\r]/ ? $1 : quot_cec($1))|eg;\n-\treturn $str;\n-}\n-\n-# Make control characters \"printable\", using character escape codes (CEC)\n-sub quot_cec {\n-\tmy $cntrl = shift;\n-\tmy %opts = @_;\n-\tmy %es = ( # character escape codes, aka escape sequences\n-\t\t\"\\t\" => '\\t',   # tab            (HT)\n-\t\t\"\\n\" => '\\n',   # line feed      (LF)\n-\t\t\"\\r\" => '\\r',   # carrige return (CR)\n-\t\t\"\\f\" => '\\f',   # form feed      (FF)\n-\t\t\"\\b\" => '\\b',   # backspace      (BS)\n-\t\t\"\\a\" => '\\a',   # alarm (bell)   (BEL)\n-\t\t\"\\e\" => '\\e',   # escape         (ESC)\n-\t\t\"\\013\" => '\\v', # vertical tab   (VT)\n-\t\t\"\\000\" => '\\0', # nul character  (NUL)\n-\t);\n-\tmy $chr = ( (exists $es{$cntrl})\n-\t\t    ? $es{$cntrl}\n-\t\t    : sprintf('\\%2x', ord($cntrl)) );\n-\tif ($opts{-nohtml}) {\n-\t\treturn $chr;\n-\t} else {\n-\t\treturn \"<span class=\\\"cntrl\\\">$chr</span>\";\n-\t}\n-}\n-\n-# Alternatively use unicode control pictures codepoints,\n-# Unicode \"printable representation\" (PR)\n-sub quot_upr {\n-\tmy $cntrl = shift;\n-\tmy %opts = @_;\n-\n-\tmy $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));\n-\tif ($opts{-nohtml}) {\n-\t\treturn $chr;\n-\t} else {\n-\t\treturn \"<span class=\\\"cntrl\\\">$chr</span>\";\n-\t}\n-}\n-\n # git may return quoted and escaped filenames\n sub unquote {\n \tmy $str = shift;\n@@ -1636,20 +1506,6 @@ sub unquote {\n \treturn $str;\n }\n \n-# escape tabs (convert tabs to spaces)\n-sub untabify {\n-\tmy $line = shift;\n-\n-\twhile ((my $pos = index($line, \"\\t\")) != -1) {\n-\t\tif (my $count = (8 - ($pos % 8))) {\n-\t\t\tmy $spaces = ' ' x $count;\n-\t\t\t$line =~ s/\\t/$spaces/;\n-\t\t}\n-\t}\n-\n-\treturn $line;\n-}\n-\n sub project_in_list {\n \tmy $project = shift;\n \tmy @list = git_get_projects_list();\n@@ -1657,167 +1513,6 @@ sub project_in_list {\n }\n \n ## ----------------------------------------------------------------------\n-## HTML aware string manipulation\n-\n-# Try to chop given string on a word boundary between position\n-# $len and $len+$add_len. If there is no word boundary there,\n-# chop at $len+$add_len. Do not chop if chopped part plus ellipsis\n-# (marking chopped part) would be longer than given string.\n-sub chop_str {\n-\tmy $str = shift;\n-\tmy $len = shift;\n-\tmy $add_len = shift || 10;\n-\tmy $where = shift || 'right'; # 'left' | 'center' | 'right'\n-\n-\t# Make sure perl knows it is utf8 encoded so we don't\n-\t# cut in the middle of a utf8 multibyte char.\n-\t$str = to_utf8($str);\n-\n-\t# allow only $len chars, but don't cut a word if it would fit in $add_len\n-\t# if it doesn't fit, cut it if it's still longer than the dots we would add\n-\t# remove chopped character entities entirely\n-\n-\t# when chopping in the middle, distribute $len into left and right part\n-\t# return early if chopping wouldn't make string shorter\n-\tif ($where eq 'center') {\n-\t\treturn $str if ($len + 5 >= length($str)); # filler is length 5\n-\t\t$len = int($len/2);\n-\t} else {\n-\t\treturn $str if ($len + 4 >= length($str)); # filler is length 4\n-\t}\n-\n-\t# regexps: ending and beginning with word part up to $add_len\n-\tmy $endre = qr/.{$len}\\w{0,$add_len}/;\n-\tmy $begre = qr/\\w{0,$add_len}.{$len}/;\n-\n-\tif ($where eq 'left') {\n-\t\t$str =~ m/^(.*?)($begre)$/;\n-\t\tmy ($lead, $body) = ($1, $2);\n-\t\tif (length($lead) > 4) {\n-\t\t\t$lead = \" ...\";\n-\t\t}\n-\t\treturn \"$lead$body\";\n-\n-\t} elsif ($where eq 'center') {\n-\t\t$str =~ m/^($endre)(.*)$/;\n-\t\tmy ($left, $str)  = ($1, $2);\n-\t\t$str =~ m/^(.*?)($begre)$/;\n-\t\tmy ($mid, $right) = ($1, $2);\n-\t\tif (length($mid) > 5) {\n-\t\t\t$mid = \" ... \";\n-\t\t}\n-\t\treturn \"$left$mid$right\";\n-\n-\t} else {\n-\t\t$str =~ m/^($endre)(.*)$/;\n-\t\tmy $body = $1;\n-\t\tmy $tail = $2;\n-\t\tif (length($tail) > 4) {\n-\t\t\t$tail = \"... \";\n-\t\t}\n-\t\treturn \"$body$tail\";\n-\t}\n-}\n-\n-# takes the same arguments as chop_str, but also wraps a <span> around the\n-# result with a title attribute if it does get chopped. Additionally, the\n-# string is HTML-escaped.\n-sub chop_and_escape_str {\n-\tmy ($str) = @_;\n-\n-\tmy $chopped = chop_str(@_);\n-\t$str = to_utf8($str);\n-\tif ($chopped eq $str) {\n-\t\treturn esc_html($chopped);\n-\t} else {\n-\t\t$str =~ s/[[:cntrl:]]/?/g;\n-\t\treturn $cgi->span({-title=>$str}, esc_html($chopped));\n-\t}\n-}\n-\n-# Highlight selected fragments of string, using given CSS class,\n-# and escape HTML.  It is assumed that fragments do not overlap.\n-# Regions are passed as list of pairs (array references).\n-#\n-# Example: esc_html_hl_regions(\"foobar\", \"mark\", [ 0, 3 ]) returns\n-# '<span class=\"mark\">foo</span>bar'\n-sub esc_html_hl_regions {\n-\tmy ($str, $css_class, @sel) = @_;\n-\treturn esc_html($str) unless @sel;\n-\n-\tmy $out = '';\n-\tmy $pos = 0;\n-\n-\tfor my $s (@sel) {\n-\t\t$out .= esc_html(substr($str, $pos, $s->[0] - $pos))\n-\t\t\tif ($s->[0] - $pos > 0);\n-\t\t$out .= $cgi->span({-class => $css_class},\n-\t\t                   esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));\n-\n-\t\t$pos = $s->[1];\n-\t}\n-\t$out .= esc_html(substr($str, $pos))\n-\t\tif ($pos < length($str));\n-\n-\treturn $out;\n-}\n-\n-# return positions of beginning and end of each match\n-sub matchpos_list {\n-\tmy ($str, $regexp) = @_;\n-\treturn unless (defined $str && defined $regexp);\n-\n-\tmy @matches;\n-\twhile ($str =~ /$regexp/g) {\n-\t\tpush @matches, [$-[0], $+[0]];\n-\t}\n-\treturn @matches;\n-}\n-\n-# highlight match (if any), and escape HTML\n-sub esc_html_match_hl {\n-\tmy ($str, $regexp) = @_;\n-\treturn esc_html($str) unless defined $regexp;\n-\n-\tmy @matches = matchpos_list($str, $regexp);\n-\treturn esc_html($str) unless @matches;\n-\n-\treturn esc_html_hl_regions($str, 'match', @matches);\n-}\n-\n-\n-# highlight match (if any) of shortened string, and escape HTML\n-sub esc_html_match_hl_chopped {\n-\tmy ($str, $chopped, $regexp) = @_;\n-\treturn esc_html_match_hl($str, $regexp) unless defined $chopped;\n-\n-\tmy @matches = matchpos_list($str, $regexp);\n-\treturn esc_html($chopped) unless @matches;\n-\n-\t# filter matches so that we mark chopped string\n-\tmy $tail = \"... \"; # see chop_str\n-\tunless ($chopped =~ s/\\Q$tail\\E$//) {\n-\t\t$tail = '';\n-\t}\n-\tmy $chop_len = length($chopped);\n-\tmy $tail_len = length($tail);\n-\tmy @filtered;\n-\n-\tfor my $m (@matches) {\n-\t\tif ($m->[0] > $chop_len) {\n-\t\t\tpush @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);\n-\t\t\tlast;\n-\t\t} elsif ($m->[1] > $chop_len) {\n-\t\t\tpush @filtered, [ $m->[0], $chop_len + $tail_len ];\n-\t\t\tlast;\n-\t\t}\n-\t\tpush @filtered, $m;\n-\t}\n-\n-\treturn esc_html_hl_regions($chopped . $tail, 'match', @filtered);\n-}\n-\n-## ----------------------------------------------------------------------\n ## functions returning short strings\n \n # CSS class for given age value (in seconds)\n@@ -2592,20 +2287,6 @@ sub git_get_type {\n our $config_file = '';\n our %config;\n \n-# store multiple values for single key as anonymous array reference\n-# single values stored directly in the hash, not as [ <value> ]\n-sub hash_set_multi {\n-\tmy ($hash, $key, $value) = @_;\n-\n-\tif (!exists $hash->{$key}) {\n-\t\t$hash->{$key} = $value;\n-\t} elsif (!ref $hash->{$key}) {\n-\t\t$hash->{$key} = [ $hash->{$key}, $value ];\n-\t} else {\n-\t\tpush @{$hash->{$key}}, $value;\n-\t}\n-}\n-\n # return hash of git project configuration\n # optionally limited to some section, e.g. 'gitweb'\n sub git_parse_project_config {\n@@ -3715,31 +3396,6 @@ sub git_get_tags_list {\n \treturn wantarray ? @tagslist : \\@tagslist;\n }\n \n-## ----------------------------------------------------------------------\n-## filesystem-related functions\n-\n-sub get_file_owner {\n-\tmy $path = shift;\n-\n-\tmy ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);\n-\tmy ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);\n-\tif (!defined $gcos) {\n-\t\treturn undef;\n-\t}\n-\tmy $owner = $gcos;\n-\t$owner =~ s/[,;].*$//;\n-\treturn to_utf8($owner);\n-}\n-\n-# assume that file exists\n-sub insert_file {\n-\tmy $filename = shift;\n-\n-\topen my $fd, '<', $filename;\n-\tprint map { to_utf8($_) } <$fd>;\n-\tclose $fd;\n-}\n-\n ## ......................................................................\n ## mimetype related functions\n \n@@ -4531,46 +4187,6 @@ sub git_get_link_target {\n \treturn $link_target;\n }\n \n-# given link target, and the directory (basedir) the link is in,\n-# return target of link relative to top directory (top tree);\n-# return undef if it is not possible (including absolute links).\n-sub normalize_link_target {\n-\tmy ($link_target, $basedir) = @_;\n-\n-\t# absolute symlinks (beginning with '/') cannot be normalized\n-\treturn if (substr($link_target, 0, 1) eq '/');\n-\n-\t# normalize link target to path from top (root) tree (dir)\n-\tmy $path;\n-\tif ($basedir) {\n-\t\t$path = $basedir . '/' . $link_target;\n-\t} else {\n-\t\t# we are in top (root) tree (dir)\n-\t\t$path = $link_target;\n-\t}\n-\n-\t# remove //, /./, and /../\n-\tmy @path_parts;\n-\tforeach my $part (split('/', $path)) {\n-\t\t# discard '.' and ''\n-\t\tnext if (!$part || $part eq '.');\n-\t\t# handle '..'\n-\t\tif ($part eq '..') {\n-\t\t\tif (@path_parts) {\n-\t\t\t\tpop @path_parts;\n-\t\t\t} else {\n-\t\t\t\t# link leads outside repository (outside top dir)\n-\t\t\t\treturn;\n-\t\t\t}\n-\t\t} else {\n-\t\t\tpush @path_parts, $part;\n-\t\t}\n-\t}\n-\t$path = join('/', @path_parts);\n-\n-\treturn $path;\n-}\n-\n # print tree entry (row of git_tree), but without encompassing <tr> element\n sub git_print_tree_entry {\n \tmy ($t, $basedir, $hash_base, $have_blame) = @_;\ndiff --git a/gitweb/lib/Gitweb/Util.pm b/gitweb/lib/Gitweb/Util.pm\nnew file mode 100755\nindex 0000000..b792200\n--- /dev/null\n+++ b/gitweb/lib/Gitweb/Util.pm\n@@ -0,0 +1,438 @@\n+# Gitweb::Util -- Internal utilities used by gitweb (git web interface)\n+#                 that do not contain Git- or Gitweb-specific knowledge\n+#\n+# This module is licensed under the GPLv2\n+\n+package Gitweb::Util;\n+\n+use strict;\n+use warnings;\n+use Exporter qw(import);\n+\n+our @EXPORT = qw(to_utf8\n+                 esc_param esc_path_info esc_url\n+                 esc_html esc_path esc_attr\n+                 sanitize untabify\n+                 chop_str chop_and_escape_str\n+                 esc_html_hl_regions matchpos_list\n+                 esc_html_match_hl esc_html_match_hl_chopped\n+                 hash_set_multi\n+                 get_file_owner insert_file normalize_link_target\n+                 $fallback_encoding);\n+our @EXPORT_OK = qw(quot_cec quot_upr);\n+\n+use Encode;\n+use CGI;\n+\n+# ......................................................................\n+# Perl encoding (utf-8)\n+\n+# decode sequences of octets in utf8 into Perl's internal form,\n+# which is utf-8 with utf8 flag set if needed.  gitweb writes out\n+# in utf-8 thanks to \"binmode STDOUT, ':utf8'\" at beginning of gitweb.perl\n+our $fallback_encoding = 'latin1';\n+sub to_utf8 {\n+\tmy $str = shift;\n+\treturn undef unless defined $str;\n+\n+\tif (utf8::is_utf8($str) || utf8::decode($str)) {\n+\t\treturn $str;\n+\t} else {\n+\t\treturn decode($fallback_encoding, $str, Encode::FB_DEFAULT);\n+\t}\n+}\n+\n+# ......................................................................\n+# CGI encoding\n+\n+# quote unsafe chars, but keep the slash, even when it's not\n+# correct, but quoted slashes look too horrible in bookmarks\n+sub esc_param {\n+\tmy $str = shift;\n+\treturn undef unless defined $str;\n+\n+\t$str =~ s/([^A-Za-z0-9\\-_.~()\\/:@ ]+)/CGI::escape($1)/eg;\n+\t$str =~ s/ /\\+/g;\n+\n+\treturn $str;\n+}\n+\n+# the quoting rules for path_info fragment are slightly different\n+sub esc_path_info {\n+\tmy $str = shift;\n+\treturn undef unless defined $str;\n+\n+\t# path_info doesn't treat '+' as space (specially), but '?' must be escaped\n+\t$str =~ s/([^A-Za-z0-9\\-_.~();\\/;:@&= +]+)/CGI::escape($1)/eg;\n+\n+\treturn $str;\n+}\n+\n+# quote unsafe chars in whole URL, so some characters cannot be quoted\n+sub esc_url {\n+\tmy $str = shift;\n+\treturn undef unless defined $str;\n+\n+\t$str =~ s/([^A-Za-z0-9\\-_.~();\\/;?:@&= ]+)/CGI::escape($1)/eg;\n+\t$str =~ s/ /\\+/g;\n+\n+\treturn $str;\n+}\n+\n+# ......................................................................\n+# (X)HTML escaping\n+\n+# replace invalid utf8 character with SUBSTITUTION sequence\n+sub esc_html {\n+\tmy $str = shift;\n+\tmy %opts = @_;\n+\n+\treturn undef unless defined $str;\n+\n+\t$str = to_utf8($str);\n+\t$str = CGI::escapeHTML($str);\n+\tif ($opts{'-nbsp'}) {\n+\t\t$str =~ s/ /&nbsp;/g;\n+\t}\n+\t$str =~ s|([[:cntrl:]])|(($1 ne \"\\t\") ? quot_cec($1) : $1)|eg;\n+\treturn $str;\n+}\n+\n+# quote unsafe characters in HTML attributes\n+sub esc_attr {\n+\n+\t# for XHTML conformance escaping '\"' to '&quot;' is not enough\n+\treturn esc_html(@_);\n+}\n+\n+# quote control characters and escape filename to HTML\n+sub esc_path {\n+\tmy $str = shift;\n+\tmy %opts = @_;\n+\n+\treturn undef unless defined $str;\n+\n+\t$str = to_utf8($str);\n+\t$str = CGI::escapeHTML($str);\n+\tif ($opts{'-nbsp'}) {\n+\t\t$str =~ s/ /&nbsp;/g;\n+\t}\n+\t$str =~ s|([[:cntrl:]])|quot_cec($1)|eg;\n+\treturn $str;\n+}\n+\n+# Sanitize for use in XHTML + application/xml+xhtm (valid XML 1.0)\n+sub sanitize {\n+\tmy $str = shift;\n+\n+\treturn undef unless defined $str;\n+\n+\t$str = to_utf8($str);\n+\t$str =~ s|([[:cntrl:]])|($1 =~ /[\\t\\n\\r]/ ? $1 : quot_cec($1))|eg;\n+\treturn $str;\n+}\n+\n+# ......................................................................\n+# Pretty-printing\n+\n+# escape tabs (convert tabs to spaces)\n+sub untabify {\n+\tmy $line = shift;\n+\n+\twhile ((my $pos = index($line, \"\\t\")) != -1) {\n+\t\tif (my $count = (8 - ($pos % 8))) {\n+\t\t\tmy $spaces = ' ' x $count;\n+\t\t\t$line =~ s/\\t/$spaces/;\n+\t\t}\n+\t}\n+\n+\treturn $line;\n+}\n+\n+# ......................................................................\n+# HTML aware string manipulation\n+\n+# Try to chop given string on a word boundary between position\n+# $len and $len+$add_len. If there is no word boundary there,\n+# chop at $len+$add_len. Do not chop if chopped part plus ellipsis\n+# (marking chopped part) would be longer than given string.\n+sub chop_str {\n+\tmy $str = shift;\n+\tmy $len = shift;\n+\tmy $add_len = shift || 10;\n+\tmy $where = shift || 'right'; # 'left' | 'center' | 'right'\n+\n+\t# Make sure perl knows it is utf8 encoded so we don't\n+\t# cut in the middle of a utf8 multibyte char.\n+\t$str = to_utf8($str);\n+\n+\t# allow only $len chars, but don't cut a word if it would fit in $add_len\n+\t# if it doesn't fit, cut it if it's still longer than the dots we would add\n+\t# remove chopped character entities entirely\n+\n+\t# when chopping in the middle, distribute $len into left and right part\n+\t# return early if chopping wouldn't make string shorter\n+\tif ($where eq 'center') {\n+\t\treturn $str if ($len + 5 >= length($str)); # filler is length 5\n+\t\t$len = int($len/2);\n+\t} else {\n+\t\treturn $str if ($len + 4 >= length($str)); # filler is length 4\n+\t}\n+\n+\t# regexps: ending and beginning with word part up to $add_len\n+\tmy $endre = qr/.{$len}\\w{0,$add_len}/;\n+\tmy $begre = qr/\\w{0,$add_len}.{$len}/;\n+\n+\tif ($where eq 'left') {\n+\t\t$str =~ m/^(.*?)($begre)$/;\n+\t\tmy ($lead, $body) = ($1, $2);\n+\t\tif (length($lead) > 4) {\n+\t\t\t$lead = \" ...\";\n+\t\t}\n+\t\treturn \"$lead$body\";\n+\n+\t} elsif ($where eq 'center') {\n+\t\t$str =~ m/^($endre)(.*)$/;\n+\t\tmy ($left, $str)  = ($1, $2);\n+\t\t$str =~ m/^(.*?)($begre)$/;\n+\t\tmy ($mid, $right) = ($1, $2);\n+\t\tif (length($mid) > 5) {\n+\t\t\t$mid = \" ... \";\n+\t\t}\n+\t\treturn \"$left$mid$right\";\n+\n+\t} else {\n+\t\t$str =~ m/^($endre)(.*)$/;\n+\t\tmy $body = $1;\n+\t\tmy $tail = $2;\n+\t\tif (length($tail) > 4) {\n+\t\t\t$tail = \"... \";\n+\t\t}\n+\t\treturn \"$body$tail\";\n+\t}\n+}\n+\n+# takes the same arguments as chop_str, but also wraps a <span> around the\n+# result with a title attribute if it does get chopped. Additionally, the\n+# string is HTML-escaped.\n+sub chop_and_escape_str {\n+\tmy ($str) = @_;\n+\n+\tmy $chopped = chop_str(@_);\n+\t$str = to_utf8($str);\n+\tif ($chopped eq $str) {\n+\t\treturn esc_html($chopped);\n+\t} else {\n+\t\t$str =~ s/[[:cntrl:]]/?/g;\n+\t\treturn CGI::span({-title=>$str}, esc_html($chopped));\n+\t}\n+}\n+\n+# Highlight selected fragments of string, using given CSS class,\n+# and escape HTML.  It is assumed that fragments do not overlap.\n+# Regions are passed as list of pairs (array references).\n+#\n+# Example: esc_html_hl_regions(\"foobar\", \"mark\", [ 0, 3 ]) returns\n+# '<span class=\"mark\">foo</span>bar'\n+sub esc_html_hl_regions {\n+\tmy ($str, $css_class, @sel) = @_;\n+\treturn esc_html($str) unless @sel;\n+\n+\tmy $out = '';\n+\tmy $pos = 0;\n+\n+\tfor my $s (@sel) {\n+\t\t$out .= esc_html(substr($str, $pos, $s->[0] - $pos))\n+\t\t\tif ($s->[0] - $pos > 0);\n+\t\t$out .= CGI::span({-class => $css_class},\n+\t\t                  esc_html(substr($str, $s->[0], $s->[1] - $s->[0])));\n+\n+\t\t$pos = $s->[1];\n+\t}\n+\t$out .= esc_html(substr($str, $pos))\n+\t\tif ($pos < length($str));\n+\n+\treturn $out;\n+}\n+\n+# return positions of beginning and end of each match\n+sub matchpos_list {\n+\tmy ($str, $regexp) = @_;\n+\treturn unless (defined $str && defined $regexp);\n+\n+\tmy @matches;\n+\twhile ($str =~ /$regexp/g) {\n+\t\tpush @matches, [$-[0], $+[0]];\n+\t}\n+\treturn @matches;\n+}\n+\n+# highlight match (if any), and escape HTML\n+sub esc_html_match_hl {\n+\tmy ($str, $regexp) = @_;\n+\treturn esc_html($str) unless defined $regexp;\n+\n+\tmy @matches = matchpos_list($str, $regexp);\n+\treturn esc_html($str) unless @matches;\n+\n+\treturn esc_html_hl_regions($str, 'match', @matches);\n+}\n+\n+\n+# highlight match (if any) of shortened string, and escape HTML\n+sub esc_html_match_hl_chopped {\n+\tmy ($str, $chopped, $regexp) = @_;\n+\treturn esc_html_match_hl($str, $regexp) unless defined $chopped;\n+\n+\tmy @matches = matchpos_list($str, $regexp);\n+\treturn esc_html($chopped) unless @matches;\n+\n+\t# filter matches so that we mark chopped string\n+\tmy $tail = \"... \"; # see chop_str\n+\tunless ($chopped =~ s/\\Q$tail\\E$//) {\n+\t\t$tail = '';\n+\t}\n+\tmy $chop_len = length($chopped);\n+\tmy $tail_len = length($tail);\n+\tmy @filtered;\n+\n+\tfor my $m (@matches) {\n+\t\tif ($m->[0] > $chop_len) {\n+\t\t\tpush @filtered, [ $chop_len, $chop_len + $tail_len ] if ($tail_len > 0);\n+\t\t\tlast;\n+\t\t} elsif ($m->[1] > $chop_len) {\n+\t\t\tpush @filtered, [ $m->[0], $chop_len + $tail_len ];\n+\t\t\tlast;\n+\t\t}\n+\t\tpush @filtered, $m;\n+\t}\n+\n+\treturn esc_html_hl_regions($chopped . $tail, 'match', @filtered);\n+}\n+\n+# ......................................................................\n+# Data structures manipulation\n+\n+# store multiple values for single key as anonymous array reference\n+# single values stored directly in the hash, not as [ <value> ]\n+sub hash_set_multi {\n+\tmy ($hash, $key, $value) = @_;\n+\n+\tif (!exists $hash->{$key}) {\n+\t\t$hash->{$key} = $value;\n+\t} elsif (!ref $hash->{$key}) {\n+\t\t$hash->{$key} = [ $hash->{$key}, $value ];\n+\t} else {\n+\t\tpush @{$hash->{$key}}, $value;\n+\t}\n+}\n+\n+# ......................................................................\n+# filesystem-related functions\n+\n+sub get_file_owner {\n+\tmy $path = shift;\n+\n+\tmy ($dev, $ino, $mode, $nlink, $st_uid, $st_gid, $rdev, $size) = stat($path);\n+\tmy ($name, $passwd, $uid, $gid, $quota, $comment, $gcos, $dir, $shell) = getpwuid($st_uid);\n+\tif (!defined $gcos) {\n+\t\treturn undef;\n+\t}\n+\tmy $owner = $gcos;\n+\t$owner =~ s/[,;].*$//;\n+\treturn to_utf8($owner);\n+}\n+\n+# assume that file exists\n+sub insert_file {\n+\tmy $filename = shift;\n+\n+\topen my $fd, '<', $filename;\n+\tprint map { to_utf8($_) } <$fd>;\n+\tclose $fd;\n+}\n+\n+# given link target, and the directory (basedir) the link is in,\n+# return target of link relative to top directory (top tree);\n+# return undef if it is not possible (including absolute links).\n+sub normalize_link_target {\n+\tmy ($link_target, $basedir) = @_;\n+\n+\t# absolute symlinks (beginning with '/') cannot be normalized\n+\treturn if (substr($link_target, 0, 1) eq '/');\n+\n+\t# normalize link target to path from top (root) tree (dir)\n+\tmy $path;\n+\tif ($basedir) {\n+\t\t$path = $basedir . '/' . $link_target;\n+\t} else {\n+\t\t# we are in top (root) tree (dir)\n+\t\t$path = $link_target;\n+\t}\n+\n+\t# remove //, /./, and /../\n+\tmy @path_parts;\n+\tforeach my $part (split('/', $path)) {\n+\t\t# discard '.' and ''\n+\t\tnext if (!$part || $part eq '.');\n+\t\t# handle '..'\n+\t\tif ($part eq '..') {\n+\t\t\tif (@path_parts) {\n+\t\t\t\tpop @path_parts;\n+\t\t\t} else {\n+\t\t\t\t# link leads outside repository (outside top dir)\n+\t\t\t\treturn;\n+\t\t\t}\n+\t\t} else {\n+\t\t\tpush @path_parts, $part;\n+\t\t}\n+\t}\n+\t$path = join('/', @path_parts);\n+\n+\treturn $path;\n+}\n+\n+# ----------------------------------------------------------------------\n+# ......................................................................\n+# Showing \"unprintable\" characters (utility functions)\n+\n+# Make control characters \"printable\", using character escape codes (CEC)\n+sub quot_cec {\n+\tmy $cntrl = shift;\n+\tmy %opts = @_;\n+\tmy %es = ( # character escape codes, aka escape sequences\n+\t\t\"\\t\" => '\\t',   # tab            (HT)\n+\t\t\"\\n\" => '\\n',   # line feed      (LF)\n+\t\t\"\\r\" => '\\r',   # carrige return (CR)\n+\t\t\"\\f\" => '\\f',   # form feed      (FF)\n+\t\t\"\\b\" => '\\b',   # backspace      (BS)\n+\t\t\"\\a\" => '\\a',   # alarm (bell)   (BEL)\n+\t\t\"\\e\" => '\\e',   # escape         (ESC)\n+\t\t\"\\013\" => '\\v', # vertical tab   (VT)\n+\t\t\"\\000\" => '\\0', # nul character  (NUL)\n+\t);\n+\tmy $chr = ( (exists $es{$cntrl})\n+\t\t    ? $es{$cntrl}\n+\t\t    : sprintf('\\%2x', ord($cntrl)) );\n+\tif ($opts{-nohtml}) {\n+\t\treturn $chr;\n+\t} else {\n+\t\treturn \"<span class=\\\"cntrl\\\">$chr</span>\";\n+\t}\n+}\n+\n+# Alternatively use unicode control pictures codepoints,\n+# Unicode \"printable representation\" (PR)\n+sub quot_upr {\n+\tmy $cntrl = shift;\n+\tmy %opts = @_;\n+\n+\tmy $chr = sprintf('&#%04d;', 0x2400+ord($cntrl));\n+\tif ($opts{-nohtml}) {\n+\t\treturn $chr;\n+\t} else {\n+\t\treturn \"<span class=\\\"cntrl\\\">$chr</span>\";\n+\t}\n+}\n+\n+1;\n-- \n1.7.9\n"}]}