diff --git a/fhem/www/codemirror/01_FHEMWEB.pm b/fhem/www/codemirror/01_FHEMWEB.pm new file mode 100644 index 000000000..0d6fb7776 --- /dev/null +++ b/fhem/www/codemirror/01_FHEMWEB.pm @@ -0,0 +1,3494 @@ +### +### special version for testing codemirror integration +### derived from SVN 5080 +### +### modified by betateilchen, justme1968, papa and others +### +### see: http://forum.fhem.de/index.php/topic,20444.0.html +### +### + +############################################## +# $Id: 01_FHEMWEB.pm 5080 2014-03-01 08:02:45Z rudolfkoenig $ +package main; + +use strict; +use warnings; +use TcpServerUtils; +use HttpUtils; + +######################### +# Forward declaration +sub FW_IconURL($); +sub FW_iconName($); +sub FW_iconPath($); +sub FW_answerCall($); +sub FW_codemirrorLink($); +sub FW_codemirrorParams($); +sub FW_dev2image($;$); +sub FW_devState($$@); +sub FW_digestCgi($); +sub FW_doDetail($); +sub FW_fatal($); +sub FW_fileList($); +sub FW_parseColumns(); +sub FW_htmlEscape($); +sub FW_logWrapper($); +sub FW_makeEdit($$$); +sub FW_makeImage(@); +sub FW_makeTable($$$@); +sub FW_makeTableFromArray($$@); +sub FW_pF($@); +sub FW_pH(@); +sub FW_pHPlain(@); +sub FW_pO(@); +sub FW_readIcons($); +sub FW_readIconsFrom($$); +sub FW_returnFileAsStream($$$$$); +sub FW_roomOverview($); +sub FW_select($$$$$@); +sub FW_serveSpecial($$$$); +sub FW_showRoom(); +sub FW_style($$); +sub FW_submit($$@); +sub FW_textfield($$$); +sub FW_textfieldv($$$$); +sub FW_updateHashes(); +sub FW_visibleDevices(;$); + +use vars qw($FW_dir); # base directory for web server +use vars qw($FW_icondir); # icon base directory +use vars qw($FW_cssdir); # css directory +use vars qw($FW_gplotdir);# gplot directory +use vars qw($MW_dir); # moddir (./FHEM), needed by edit Files in new + # structure + +use vars qw($FW_ME); # webname (default is fhem), used by 97_GROUP/weblink +use vars qw($FW_ss); # is smallscreen, needed by 97_GROUP/95_VIEW +use vars qw($FW_tp); # is touchpad (iPad / etc) +use vars qw($FW_sp); # stylesheetPrefix + +# global variables, also used by 97_GROUP/95_VIEW/95_FLOORPLAN +use vars qw(%FW_types); # device types, +use vars qw($FW_RET); # Returned data (html) +use vars qw($FW_RETTYPE); # image/png or the like +use vars qw($FW_wname); # Web instance +use vars qw($FW_subdir); # Sub-path in URL, used by FLOORPLAN/weblink +use vars qw(%FW_pos); # scroll position +use vars qw($FW_cname); # Current connection name +use vars qw(%FW_hiddenroom); # hash of hidden rooms, used by weblink +use vars qw($FW_plotmode);# Global plot mode (WEB attribute), used by SVG +use vars qw($FW_plotsize);# Global plot size (WEB attribute), used by SVG +use vars qw(%FW_webArgs); # all arguments specified in the GET +use vars qw(@FW_fhemwebjs);# List of fhemweb*js scripts to load +use vars qw($FW_detail); # currently selected device for detail view +use vars qw($FW_cmdret); # Returned data by the fhem call +use vars qw($FW_room); # currently selected room +use vars qw($FW_formmethod); +use vars qw(%FW_visibleDeviceHash); + +$FW_formmethod = "post"; + +my $FW_zlib_checked; +my $FW_use_zlib = 1; +my $FW_activateInform = 0; + +######################### +# As we are _not_ multithreaded, it is safe to use global variables. +# Note: for delivering SVG plots we fork +my @FW_httpheader; # HTTP header, line by line +my @FW_enc; # Accepted encodings (browser header) +my $FW_data; # Filecontent from browser when editing a file +my %FW_icons; # List of icons +my @FW_iconDirs; # Directory search order for icons +my $FW_RETTYPE; # image/png or the like +my %FW_rooms; # hash of all rooms +my %FW_types; # device types, for sorting +my %FW_hiddengroup;# hash of hidden groups +my $FW_inform; +my $FW_XHR; # Data only answer, no HTML +my $FW_jsonp; # jasonp answer (sending function calls to the client) +my $FW_headercors; # +my $FW_chash; # client fhem hash +my $FW_encoding="UTF-8"; + + +##################################### +sub +FHEMWEB_Initialize($) +{ + my ($hash) = @_; + + $hash->{ReadFn} = "FW_Read"; + $hash->{GetFn} = "FW_Get"; + $hash->{SetFn} = "FW_Set"; + $hash->{AttrFn} = "FW_Attr"; + $hash->{DefFn} = "FW_Define"; + $hash->{UndefFn} = "FW_Undef"; + $hash->{NotifyFn}= "FW_SecurityCheck"; + $hash->{ActivateInformFn} = "FW_ActivateInform"; + no warnings 'qw'; + my @attrList = qw( + CORS:0,1 + HTTPS:1,0 + SVGcache:1,0 + allowedCommands + allowfrom + basicAuth + basicAuthMsg + column + defaultRoom + endPlotNow:1,0 + endPlotToday:1,0 + enhancedEditor:1,0 + enhancedEditorTheme + fwcompress:0,1 + hiddengroup + hiddenroom + iconPath + longpoll:0,1 + longpollSVG:1,0 + menuEntries + ploteditor:always,onClick,never + plotfork:1,0 + plotmode:gnuplot,gnuplot-scroll,SVG + plotsize + nrAxis + redirectCmds:0,1 + refresh + reverseLogs:0,1 + roomIcons + sortRooms + smallscreen:unused + stylesheetPrefix + touchpad:unused + webname + ); + use warnings 'qw'; + $hash->{AttrList} = join(" ", @attrList); + + + ############### + # Initialize internal structures + map { addToAttrList($_) } ( "webCmd", "icon", "devStateIcon", + "sortby", "devStateStyle"); + InternalTimer(time()+60, "FW_closeOldClients", 0, 0); + + $FW_dir = "$attr{global}{modpath}/www"; + $FW_icondir = "$FW_dir/images"; + $FW_cssdir = "$FW_dir/pgm2"; + $FW_gplotdir = "$FW_dir/gplot"; + if(opendir(DH, "$FW_dir/pgm2")) { + @FW_fhemwebjs = sort grep /^fhemweb.*js$/, readdir(DH); + closedir(DH); + } + + $data{webCmdFn}{slider} = "FW_sliderFn"; + $data{webCmdFn}{timepicker} = "FW_timepickerFn"; + $data{webCmdFn}{noArg} = "FW_noArgFn"; + $data{webCmdFn}{textField} = "FW_textFieldFn"; + $data{webCmdFn}{"~dropdown"}= "FW_dropdownFn"; # Should be the last +} + +##################################### +sub +FW_SecurityCheck($$) +{ + my ($ntfy, $dev) = @_; + return if($dev->{NAME} ne "global" || + !grep(m/^INITIALIZED$/, @{$dev->{CHANGED}})); + my $motd = AttrVal("global", "motd", ""); + if($motd =~ "^SecurityCheck") { + my @list = grep { !AttrVal($_, "basicAuth", undef) } + devspec2array("TYPE=FHEMWEB"); + $motd .= (join(",", sort @list)." has no basicAuth attribute.\n") + if(@list); + $attr{global}{motd} = $motd; + } + $modules{FHEMWEB}{NotifyFn}= "FW_Notify"; + return; +} + +##################################### +sub +FW_Define($$) +{ + my ($hash, $def) = @_; + my ($name, $type, $port, $global) = split("[ \t]+", $def); + return "Usage: define FHEMWEB [IPV6:] [global]" + if($port !~ m/^(IPV6:)?\d+$/ || ($global && $global ne "global")); + + foreach my $pe ("fhemSVG", "openautomation", "default") { + FW_readIcons($pe); + } + + my $ret = TcpServer_Open($hash, $port, $global); + + # Make sure that fhem only runs once + if($ret && !$init_done) { + Log3 $hash, 1, "$ret. Exiting."; + exit(1); + } + + return $ret; +} + +##################################### +sub +FW_Undef($$) +{ + my ($hash, $arg) = @_; + return TcpServer_Close($hash); +} + +##################################### +sub +FW_Read($) +{ + my ($hash) = @_; + my $name = $hash->{NAME}; + + if($hash->{SERVERSOCKET}) { # Accept and create a child + TcpServer_Accept($hash, "FHEMWEB"); + return; + } + + $FW_chash = $hash; + $FW_wname = $hash->{SNAME}; + $FW_cname = $name; + $FW_subdir = ""; + + my $c = $hash->{CD}; + if(!$FW_zlib_checked) { + $FW_zlib_checked = 1; + $FW_use_zlib = AttrVal($FW_wname, "fwcompress", 1); + if($FW_use_zlib) { + eval { require Compress::Zlib; }; + if($@) { + $FW_use_zlib = 0; + Log3 $FW_wname, 1, $@; + Log3 $FW_wname, 1, + "$FW_wname: Can't load Compress::Zlib, deactivating compression"; + $attr{$FW_wname}{fwcompress} = 0; + } + } + } + + + + # Data from HTTP Client + my $buf; + my $ret = sysread($c, $buf, 1024); + + if(!defined($ret) || $ret <= 0) { + CommandDelete(undef, $name); + Log3 $FW_wname, 4, "Connection closed for $name"; + return; + } + + $hash->{BUF} .= $buf; + if($defs{$FW_wname}{SSL}) { + while($c->pending()) { + sysread($c, $buf, 1024); + $hash->{BUF} .= $buf; + } + } + + if(!$hash->{HDR}) { + return if($hash->{BUF} !~ m/^(.*)(\n\n|\r\n\r\n)(.*)$/s); + $hash->{HDR} = $1; + $hash->{BUF} = $3; + if($hash->{HDR} =~ m/Content-Length: ([^\r\n]*)/s) { + $hash->{CONTENT_LENGTH} = $1; + } + } + return if($hash->{CONTENT_LENGTH} && + length($hash->{BUF})<$hash->{CONTENT_LENGTH}); + + @FW_httpheader = split("[\r\n]", $hash->{HDR}); + delete($hash->{HDR}); + + my @origin = grep /Origin/, @FW_httpheader; + $FW_headercors = (AttrVal($FW_wname, "CORS", 0) ? + "Access-Control-Allow-".$origin[0]."\r\n". + "Access-Control-Allow-Methods: GET OPTIONS\r\n". + "Access-Control-Allow-Headers: Origin, Authorization, Accept\r\n". + "Access-Control-Allow-Credentials: true\r\n". + "Access-Control-Max-Age:86400\r\n" : ""); + + + ############################# + # BASIC HTTP AUTH + my $basicAuth = AttrVal($FW_wname, "basicAuth", undef); + my @headerOptions = grep /OPTIONS/, @FW_httpheader; + if($basicAuth) { + my @authLine = grep /Authorization: Basic/, @FW_httpheader; + my $secret = $authLine[0]; + $secret =~ s/^Authorization: Basic // if($secret); + my $pwok = ($secret && $secret eq $basicAuth); + if($secret && $basicAuth =~ m/^{.*}$/ || $headerOptions[0]) { + eval "use MIME::Base64"; + if($@) { + Log3 $FW_wname, 1, $@; + + } else { + my ($user, $password) = split(":", decode_base64($secret)); + $pwok = eval $basicAuth; + Log3 $FW_wname, 1, "basicAuth expression: $@" if($@); + } + } + if($headerOptions[0]) { + print $c "HTTP/1.1 200 OK\r\n", + $FW_headercors, + "Content-Length: 0\r\n\r\n"; + delete $hash->{CONTENT_LENGTH}; + delete $hash->{BUF}; + return; + exit(1); + }; + if(!$pwok) { + my $msg = AttrVal($FW_wname, "basicAuthMsg", "Fhem: login required"); + print $c "HTTP/1.1 401 Authorization Required\r\n", + "WWW-Authenticate: Basic realm=\"$msg\"\r\n", + $FW_headercors, + "Content-Length: 0\r\n\r\n"; + delete $hash->{CONTENT_LENGTH}; + delete $hash->{BUF}; + return; + }; + } + ############################# + + my $now = time(); + @FW_enc = grep /Accept-Encoding/, @FW_httpheader; + my ($method, $arg, $httpvers) = split(" ", $FW_httpheader[0], 3); + $arg .= "&".$hash->{BUF} if($hash->{CONTENT_LENGTH}); + delete $hash->{CONTENT_LENGTH}; + delete $hash->{BUF}; + $hash->{LASTACCESS} = $now; + + $arg = "" if(!defined($arg)); + Log3 $FW_wname, 4, "HTTP $name GET $arg"; + my $pid; + if(AttrVal($FW_wname, "plotfork", undef)) { + # Process SVG rendering as a parallel process + return if(($arg =~ m+/SVG_showLog+) && ($pid = fork)); + } + + my $cacheable = FW_answerCall($arg); + return if($cacheable == -1); # Longpoll / inform request; + + my $compressed = ""; + if(($FW_RETTYPE =~ m/text/i || + $FW_RETTYPE =~ m/svg/i || + $FW_RETTYPE =~ m/script/i) && + (int(@FW_enc) == 1 && $FW_enc[0] =~ m/gzip/) && + $FW_use_zlib) { + $FW_RET = Compress::Zlib::memGzip($FW_RET); + $compressed = "Content-Encoding: gzip\r\n"; + } + + my $length = length($FW_RET); + my $expires = ($cacheable? + ("Expires: ".localtime($now+900)." GMT\r\n") : ""); + Log3 $FW_wname, 4, "$arg / RL:$length / $FW_RETTYPE / $compressed / $expires"; + print $c "HTTP/1.1 200 OK\r\n", + "Content-Length: $length\r\n", + $expires, $compressed, $FW_headercors, + "Content-Type: $FW_RETTYPE\r\n\r\n", + $FW_RET; + exit if(defined($pid)); +} + +########################### +sub +FW_serveSpecial($$$$) +{ + my ($file,$ext,$dir,$cacheable)= @_; + $file =~ s,\.\./,,g; # little bit of security + + $file = "$FW_sp$file" if($ext eq "css" && -f "$dir/$FW_sp$file.$ext"); + $FW_RETTYPE = ext2MIMEType($ext); + return FW_returnFileAsStream("$dir/$file.$ext", "", + $FW_RETTYPE, 0, $cacheable); +} + +sub +FW_answerCall($) +{ + my ($arg) = @_; + my $me=$defs{$FW_cname}; # cache, else rereadcfg will delete us + + $FW_RET = ""; + $FW_RETTYPE = "text/html; charset=$FW_encoding"; + $FW_ME = "/" . AttrVal($FW_wname, "webname", "fhem"); + + $MW_dir = "$attr{global}{modpath}/FHEM"; + $FW_sp = AttrVal($FW_wname, "stylesheetPrefix", ""); + $FW_ss = ($FW_sp =~ m/smallscreen/); + $FW_tp = ($FW_sp =~ m/smallscreen|touchpad/); + @FW_iconDirs = grep { $_ } split(":", AttrVal($FW_wname, "iconPath", + "$FW_sp:default:fhemSVG:openautomation")); + if($arg =~ m,$FW_ME/floorplan/([a-z0-9.:_]+),i) { # FLOORPLAN: special icondir + unshift @FW_iconDirs, $1; + FW_readIcons($1); + } + + # /icons/... => current state of ... + # also used for static images: unintended, but too late to change + if($arg =~ m,^$FW_ME/icons/(.*)$,) { + my ($icon,$cacheable) = (urlDecode($1), 1); + my $iconPath = FW_iconPath($icon); + + # if we do not have the icon, we convert the device state to the icon name + if(!$iconPath) { + ($icon, undef, undef) = FW_dev2image($icon); + $cacheable = 0; + return 0 if(!$icon); + $iconPath = FW_iconPath($icon); + } + $iconPath =~ m/(.*)\.([^.]*)/; + return FW_serveSpecial($1, $2, $FW_icondir, $cacheable); + + } elsif($arg =~ m,^$FW_ME/(.*)/([^/]*)$,) { # the "normal" case + my ($dir, $ofile, $ext) = ($1, $2, ""); + $dir =~ s/\.\.//g; + $dir =~ s,www/,,g; # Want commandref.html to work from file://... + + my $file = $ofile; + $file =~ s/\?.*//; # Remove timestamp of CSS reloader + if($file =~ m/^(.*)\.([^.]*)$/) { + $file = $1; $ext = $2; + } + my $ldir = "$FW_dir/$dir"; + $ldir = "$FW_dir/pgm2" if($dir eq "css" || $dir eq "js"); # FLOORPLAN compat + $ldir = "$attr{global}{modpath}/docs" if($dir eq "docs"); + + if(-r "$ldir/$file.$ext") { # no return for FLOORPLAN + return FW_serveSpecial($file, $ext, $ldir, ($arg =~ m/nocache/) ? 0 : 1); + } + $arg = "/$dir/$ofile"; + + } elsif($arg =~ m/^$FW_ME(.*)/) { + $arg = $1; # The stuff behind FW_ME, continue to check for commands/FWEXT + + } else { + my $c = $me->{CD}; + Log3 $FW_wname, 4, "$FW_wname: redirecting $arg to $FW_ME"; + print $c "HTTP/1.1 302 Found\r\n", + "Content-Length: 0\r\n", $FW_headercors, + "Location: $FW_ME\r\n\r\n"; + return -1; + + } + + + $FW_plotmode = AttrVal($FW_wname, "plotmode", "SVG"); + $FW_plotsize = AttrVal($FW_wname, "plotsize", $FW_ss ? "480,160" : + $FW_tp ? "640,160" : "800,160"); + my ($cmd, $cmddev) = FW_digestCgi($arg); + + + if($FW_inform) { # Longpoll header + if($FW_inform =~ /type=/) { + foreach my $kv (split(";", $FW_inform)) { + my ($key,$value) = split("=", $kv, 2); + $me->{inform}{$key} = $value; + } + + } else { # Compatibility mode + $me->{inform}{type} = ($FW_room ? "status" : "raw"); + $me->{inform}{filter} = ($FW_room ? $FW_room : ".*"); + } + my $filter = $me->{inform}{filter}; + $filter = "NAME=.*" if($filter eq "room=all"); + $filter = "room!=.*" if($filter eq "room=Unsorted"); + + my %h = map { $_ => 1 } devspec2array($filter); + $me->{inform}{devices} = \%h; + %FW_visibleDeviceHash = FW_visibleDevices(); + + # NTFY_ORDER is larger than the normal order (50-) + $me->{NTFY_ORDER} = $FW_cname; # else notifyfn won't be called + %ntfyHash = (); + + my $c = $me->{CD}; + print $c "HTTP/1.1 200 OK\r\n", + $FW_headercors, + "Content-Type: application/octet-stream; charset=$FW_encoding\r\n\r\n", + FW_roomStatesForInform($me); + return -1; + } + + my $docmd = 0; + $docmd = 1 if($cmd && + $cmd !~ /^showlog/ && + $cmd !~ /^style / && + $cmd !~ /^edit/); + + #If we are in XHR or json mode, execute the command directly + if($FW_XHR || $FW_jsonp) { + $FW_cmdret = $docmd ? FW_fC($cmd, $cmddev) : ""; + $FW_RETTYPE = "text/plain; charset=$FW_encoding"; + if($FW_jsonp) { + $FW_cmdret =~ s/'/\\'/g; + # Escape newlines in JavaScript string + $FW_cmdret =~ s/\n/\\\n/g; + FW_pO "$FW_jsonp('$FW_cmdret');"; + } else { + FW_pO $FW_cmdret; + } + return 0; + } + + ############################## + # FHEMWEB extensions (FLOORPLOAN, SVG_WriteGplot, etc) + my $FW_contentFunc; + if(defined($data{FWEXT})) { + foreach my $k (sort keys %{$data{FWEXT}}) { + my $h = $data{FWEXT}{$k}; + next if($arg !~ m/^$k/); + $FW_contentFunc = $h->{CONTENTFUNC}; + next if($h !~ m/HASH/ || !$h->{FUNC}); + #Returns undef as FW_RETTYPE if it already sent a HTTP header + no strict "refs"; + ($FW_RETTYPE, $FW_RET) = &{$h->{FUNC}}($arg); + use strict "refs"; + return defined($FW_RETTYPE) ? 0 : -1; + } + } + + + #Now execute the command + $FW_cmdret = ""; + if($docmd) { + $FW_cmdret = FW_fC($cmd, $cmddev); + if($cmd =~ m/^define +([^ ]+) /) { # "redirect" after define to details + $FW_detail = $1; + } + } + + # Redirect after a command, to clean the browser URL window + if($docmd && !$FW_cmdret && AttrVal($FW_wname, "redirectCmds", 1)) { + my $tgt = $FW_ME; + if($FW_detail) { $tgt .= "?detail=$FW_detail" } + elsif($FW_room) { $tgt .= "?room=$FW_room" } + my $c = $me->{CD}; + print $c "HTTP/1.1 302 Found\r\n", + "Content-Length: 0\r\n", $FW_headercors, + "Location: $tgt\r\n", + "\r\n"; + return -1; + } + + FW_updateHashes(); + + my $t = AttrVal("global", "title", "Home, Sweet Home"); + + FW_pO ''; + FW_pO ''; + FW_pO "\n$t"; + FW_pO ''; + + # Enable WebApps + if($FW_tp || $FW_ss) { + my $icon = $FW_ME."/images/default/fhemicon_ios.png"; + if($FW_ss) { + FW_pO ''; + } elsif($FW_tp) { + FW_pO ''; + } + FW_pO ''; + FW_pO ''; + FW_pO ''; + } + + # meta refresh in rooms only + if ($FW_room) { + my $rf = AttrVal($FW_wname, "refresh", ""); + FW_pO "" if($rf); + } + + FW_pO ""; + + ######################## + # FW Extensions + my $jsTemplate = ''; + if(defined($data{FWEXT})) { + foreach my $k (sort keys %{$data{FWEXT}}) { + my $h = $data{FWEXT}{$k}; + next if($h !~ m/HASH/ || !$h->{SCRIPT}); + my $script = $h->{SCRIPT}; + $script = ($script =~ m,^/,) ? "$FW_ME$script" : "$FW_ME/pgm2/$script"; + FW_pO sprintf($jsTemplate, $script); + } + } + + FW_pO sprintf($jsTemplate, "$FW_ME/pgm2/svg.js") if($FW_plotmode eq "SVG"); + if($FW_plotmode eq"jsSVG") { + FW_pO sprintf($jsTemplate, "$FW_ME/pgm2/jsSVG.js"); + FW_pO sprintf($jsTemplate, "$FW_ME/pgm2/jquery.min.js"); + } + foreach my $js (@FW_fhemwebjs) { + FW_pO sprintf($jsTemplate, "$FW_ME/pgm2/$js"); + } + + my $onload = AttrVal($FW_wname, "longpoll", 1) ? + "onload=\"FW_delayedStart()\"" : ""; + FW_pO "\n"; + + if($FW_activateInform) { + $FW_cmdret = $FW_activateInform = ""; + $cmd = "style eventMonitor"; + } + + if($FW_cmdret) { + $FW_detail = ""; + $FW_room = ""; + $FW_cmdret = FW_htmlEscape($FW_cmdret); + $FW_cmdret =~ s/>/>/g; + FW_pO "
"; + $FW_cmdret = "
$FW_cmdret
" if($FW_cmdret =~ m/\n/); + if($FW_ss) { + FW_pO "
$FW_cmdret
"; + } else { + FW_pO $FW_cmdret; + } + FW_pO "
"; + + } + + FW_roomOverview($cmd); + if($FW_contentFunc) { + no strict "refs"; + my $ret = &{$FW_contentFunc}($arg); + use strict "refs"; + return $ret if($ret); + } + + if($cmd =~ m/^style /) { FW_style($cmd,undef); } + elsif($FW_detail) { FW_doDetail($FW_detail); } + elsif($FW_room) { FW_showRoom(); } + elsif(!$FW_cmdret && + !$FW_contentFunc) { + + $FW_room = AttrVal($FW_wname, "defaultRoom", ''); + if($FW_room ne '') { + FW_showRoom(); + + } else { + my $motd = AttrVal("global","motd","none"); + if($motd ne "none") { + $motd =~ s/\n/
/g; + FW_pO "
$motd
"; + } + } + } + FW_pO ""; + return 0; +} + + +########################### +# Digest CGI parameters +sub +FW_digestCgi($) +{ + my ($arg) = @_; + my (%arg, %val, %dev); + my ($cmd, $c) = ("","",""); + + %FW_pos = (); + $FW_room = ""; + $FW_detail = ""; + $FW_XHR = undef; + $FW_jsonp = undef; + $FW_inform = undef; + + %FW_webArgs = (); + #Remove (nongreedy) everything including the first '?' + $arg =~ s,^.*?[?],,; + foreach my $pv (split("&", $arg)) { + next if($pv eq ""); # happens when post forgot to set FW_ME + $pv =~ s/\+/ /g; + $pv =~ s/%([\dA-F][\dA-F])/chr(hex($1))/ige; + my ($p,$v) = split("=",$pv, 2); + + # Multiline: escape the NL for fhem + $v =~ s/[\r]//g if($v && $p && $p ne "data"); + $FW_webArgs{$p} = $v; + + if($p eq "detail") { $FW_detail = $v; } + if($p eq "room") { $FW_room = $v; } + if($p eq "cmd") { $cmd = $v; } + if($p =~ m/^arg\.(.*)$/) { $arg{$1} = $v; } + if($p =~ m/^val\.(.*)$/) { $val{$1} = ($val{$1} ? $val{$1}.",$v" : $v) } + if($p =~ m/^dev\.(.*)$/) { $dev{$1} = $v; } + if($p =~ m/^cmd\.(.*)$/) { $cmd = $v; $c = $1; } + if($p eq "pos") { %FW_pos = split(/[=;]/, $v); } + if($p eq "data") { $FW_data = $v; } + if($p eq "XHR") { $FW_XHR = 1; } + if($p eq "jsonp") { $FW_jsonp = $v; } + if($p eq "inform") { $FW_inform = $v; } + + } + $cmd.=" $dev{$c}" if(defined($dev{$c})); + $cmd.=" $arg{$c}" if(defined($arg{$c}) && + ($arg{$c} ne "state" || $cmd !~ m/^set/)); + $cmd.=" $val{$c}" if(defined($val{$c})); + return ($cmd, $c); +} + +##################### +# create FW_rooms && FW_types +sub +FW_updateHashes() +{ + ################# + # Make a room hash + %FW_rooms = (); + foreach my $d (keys %defs ) { + next if(IsIgnored($d)); + foreach my $r (split(",", AttrVal($d, "room", "Unsorted"))) { + $FW_rooms{$r}{$d} = 1; + } + } + + ############### + # Needed for type sorting + %FW_types = (); + foreach my $d (sort keys %defs ) { + next if(IsIgnored($d)); + my $t = AttrVal($d, "subType", $defs{$d}{TYPE}); + $t = AttrVal($d, "model", $t) if($t && $t eq "unknown"); # RKO: ??? + $FW_types{$d} = $t; + } + + $FW_room = AttrVal($FW_detail, "room", "Unsorted") if($FW_detail); +} + +############################## +sub +FW_makeTable($$$@) +{ + my($title, $name, $hash, $cmd) = (@_); + + return if(!$hash || !int(keys %{$hash})); + my $class = lc($title); + $class =~ s/[^A-Za-z]/_/g; + FW_pO "
"; + FW_pO $title; + FW_pO ""; + my $si = AttrVal("global", "showInternalValues", 0); + + my $row = 1; + foreach my $n (sort keys %{$hash}) { + next if(!$si && $n =~ m/^\./); # Skip "hidden" Values + my $val = $hash->{$n}; + $val = "" if(!defined($val)); + + $val = $hash->{$n}{NAME} # Exception + if($n eq "IODev" && ref($val) eq "HASH" && defined($hash->{$n}{NAME})); + + my $r = ref($val); + next if($r && ($r ne "HASH" || !defined($hash->{$n}{VAL}))); + + FW_pF "", ($row&1)?"odd":"even"; + $row++; + + if($n eq "DEF" && !$FW_hiddenroom{input}) { + FW_makeEdit($name, $n, $val); + + } else { + if( $title eq "Attributes" ) { + FW_pO ""; + } else { + FW_pO ""; + } + + if(ref($val)) { #handle readings + my ($v, $t) = ($val->{VAL}, $val->{TIME}); + $v = FW_htmlEscape($v); + if($FW_ss) { + $t = ($t ? "
$t
" : ""); + FW_pO ""; + } else { + $t = "" if(!$t); + FW_pO ""; + FW_pO ""; + } + } else { + $val = FW_htmlEscape($val); + + # if possible provide som links + if ($n eq "room"){ + FW_pO ""; + + } elsif ($n eq "webCmd"){ + my $lc = "detail=$name&cmd.$name=set $name"; + FW_pO ""; + + } elsif ($n =~ m/^fp_(.*)/ && $defs{$1}){ #special for Floorplan + FW_pH "detail=$1", $val,1; + + } else { + FW_pO ""; + } + } + + } + + FW_pH "cmd.$name=$cmd $name $n&detail=$name", $cmd, 1 + if($cmd && !$FW_ss); + FW_pO ""; + } + FW_pO "
". + "". + "$n
$n
$v$t
$v
$t
". + join(",", map { FW_pH("room=$_",$_,0,"",1,1) } split(",",$val)). + "
". + join(":", map {FW_pH("$lc $_",$_,0,"",1,1)} split(":",$val) ). + "
". + join(",", map { ($_ ne $name && $defs{$_}) ? + FW_pH( "detail=$_", $_ ,0,"",1,1) : $_ } split(",",$val)). + "
"; + FW_pO "
"; + +} + +############################## +# Used only for set or attr lists. +sub +FW_makeSelect($$$$) +{ + my ($d, $cmd, $list,$class) = @_; + return if(!$list || $FW_hiddenroom{input}); + my @al = sort map { s/:.*//;$_ } split(" ", $list); + + my $selEl = (defined($al[0]) ? $al[0] : " "); + $selEl = $1 if($list =~ m/([^ ]*):slider,/); # promote a slider if available + $selEl = "room" if($list =~ m/room:/); + + FW_pO "
"; + FW_pO "
"; + FW_pO FW_hidden("detail", $d); + FW_pO FW_hidden("dev.$cmd$d", $d); + FW_pO FW_submit("cmd.$cmd$d", $cmd, $class); + FW_pO "
 $d 
"; + FW_pO FW_select("sel.$cmd$d","arg.$cmd$d",\@al, $selEl, $class, + "FW_selChange(this.options[selectedIndex].text,'$list','val.$cmd$d')"); + FW_pO FW_textfield("val.$cmd$d", 30, $class); + # Initial setting + FW_pO ""; + FW_pO "
"; +} + +############################## +sub +FW_doDetail($) +{ + my ($d) = @_; + + my $h = $defs{$d}; + my $t = $h->{TYPE}; + $t = "MISSING" if(!defined($t)); + FW_pO "
"; + + if($FW_ss) { # FS20MS2 special: on and off, is not the same as toggle + my $webCmd = AttrVal($d, "webCmd", undef); + if($webCmd) { + FW_pO ""; + foreach my $cmd (split(":", $webCmd)) { + FW_pO ""; + FW_pH "cmd.$d=set $d $cmd&detail=$d", $cmd, 1, "col1"; + FW_pO ""; + } + FW_pO "
"; + } + } + FW_pO "
"; + + if($modules{$t}{FW_detailFn}) { + no strict "refs"; + my $txt = &{$modules{$t}{FW_detailFn}}($FW_wname, $d, $FW_room); + FW_pO "$txt
" if(defined($txt)); + use strict "refs"; + } + + FW_pO "
"; + FW_pO FW_hidden("detail", $d); + + FW_makeSelect($d, "set", getAllSets($d), "set"); + FW_makeSelect($d, "get", getAllGets($d), "get"); + + FW_makeTable("Internals", $d, $h); + FW_makeTable("Readings", $d, $h->{READINGS}); + + my $attrList = getAllAttr($d); + my $roomList = "multiple,".join(",", sort map { $_ =~ s/ /#/g ;$_} keys %FW_rooms); + $attrList =~ s/room /room:$roomList /; + FW_makeSelect($d, "attr", $attrList,"attr"); + + FW_makeTable("Attributes", $d, $attr{$d}, "deleteattr"); + ## dependent objects + my @dob; # dependent objects - triggered by current device + foreach my $dn (sort keys %defs) { + next if(!$dn || $dn eq $d); + my $dh = $defs{$dn}; + if(($dh->{DEF} && $dh->{DEF} =~ m/\b$d\b/) || + ($h->{DEF} && $h->{DEF} =~ m/\b$dn\b/)) { + push(@dob, $dn); + } + } + FW_pO "
"; + FW_makeTableFromArray("Probably associated with", "assoc", @dob,); + + FW_pO "
"; + + FW_pH "cmd=style iconFor $d", "Select icon"; + FW_pH "cmd=style showDSI $d", "Extend devStateIcon"; + FW_pH "$FW_ME/docs/commandref.html#${t}", "Device specific help"; + FW_pO "

"; + FW_pO "
"; + +} + +############################## +sub +FW_makeTableFromArray($$@) { + my ($txt,$class,@obj) = @_; + if (@obj>0) { + my $row=1; + FW_pO "
"; + FW_pO "$txt"; + FW_pO ""; + foreach (sort @obj) { + FW_pF ""; + FW_pO ""; + } + FW_pO "
", ($row&1)?"odd":"even"; + $row++; + FW_pH "detail=$_", $_; + FW_pO "$defs{$_}{TYPE}
"; + } +} + +sub +FW_roomIdx(\@$) +{ + my ($arr,$v) = @_; + my ($index) = grep { $v =~ /^$arr->[$_]$/ } 0..$#$arr; + + if( !defined($index) ) { + $index = 9999; + } else { + $index = sprintf( "%03i", $index ); + } + + return "$index-$v"; +} + + +############## +# Header, Zoom-Icons & list of rooms at the left. +sub +FW_roomOverview($) +{ + my ($cmd) = @_; + + %FW_hiddenroom = (); + foreach my $r (split(",",AttrVal($FW_wname, "hiddenroom", ""))) { + $FW_hiddenroom{$r} = 1; + } + + ############## + # LOGO + my $hasMenuScroll; + if($FW_detail && $FW_ss) { + $FW_room = AttrVal($FW_detail, "room", undef); + $FW_room = $1 if($FW_room && $FW_room =~ m/^([^,]*),/); + $FW_room = "" if(!$FW_room); + FW_pO(FW_pHPlain("room=$FW_room", + "
" . FW_makeImage("back") . "
")); + FW_pO "
$FW_detail details
"; + return; + + } else { + $hasMenuScroll = 1; + FW_pO '" if($hasMenuScroll); + + ############## + # HEADER + FW_pO "
"; + FW_pO '
'; + FW_pO "
"; + FW_pO FW_hidden("room", "$FW_room") if($FW_room); + FW_pO FW_textfield("cmd", $FW_ss ? 25 : 40, "maininput"); + FW_pO "
"; + FW_pO "
"; + FW_pO "
"; + +} + + +######################## +# Show the overview of devices in one room +# room can be a room, all or Unsorted +sub +FW_showRoom() +{ + return if(!$FW_room); + + %FW_hiddengroup = (); + foreach my $r (split(",",AttrVal($FW_wname, "hiddengroup", ""))) { + $FW_hiddengroup{$r} = 1; + } + + FW_pO "
"; + FW_pO "
"; + FW_pO ""; # Need for equal width of subtables + + my $rf = ($FW_room ? "&room=$FW_room" : ""); # stay in the room + + # array of all device names in the room (exception weblinks without group + # attribute) + my @devs= grep { ($FW_rooms{$FW_room}{$_}||$FW_room eq "all") && + !IsIgnored($_) } keys %defs; + my (%group, @atEnds, %usuallyAtEnd); + foreach my $dev (@devs) { + if($modules{$defs{$dev}{TYPE}}{FW_atPageEnd}) { + $usuallyAtEnd{$dev} = 1; + if(!AttrVal($dev, "group", undef)) { + push @atEnds, $dev; + next; + } + } + foreach my $grp (split(",", AttrVal($dev, "group", $FW_types{$dev}))) { + next if($FW_hiddengroup{$grp}); + $group{$grp}{$dev} = 1; + } + } + + # row counter + my $row=1; + my %extPage = (); + + my ($columns, $maxc) = FW_parseColumns(); + FW_pO "" if($maxc != -1); + for(my $col=1; $col < ($maxc==-1 ? 2 : $maxc); $col++) { + FW_pO "" if($maxc != -1); # Column + } + FW_pO ""; + + FW_pO "
" if($maxc != -1); + + # iterate over the distinct groups + foreach my $g (sort keys %group) { + + next if($maxc != -1 && (!$columns->{$g} || $columns->{$g} != $col)); + + ################# + # Check if there is a device of this type in the room + FW_pO "\n"; + FW_pO ""; + } + FW_pO "
$g
"; + FW_pO ""; + + foreach my $d (sort { lc(AttrVal($a,"sortby",AttrVal($a,"alias",$a))) cmp + lc(AttrVal($b,"sortby",AttrVal($b,"alias",$b))) } + keys %{$group{$g}}) { + my $type = $defs{$d}{TYPE}; + + FW_pF "\n", ($row&1)?"odd":"even"; + my $devName = AttrVal($d, "alias", $d); + my $icon = AttrVal($d, "icon", ""); + $icon = FW_makeImage($icon,$icon,"icon") . " " if($icon); + + if($FW_hiddenroom{detail}) { + FW_pO ""; + } else { + FW_pH "detail=$d", "$icon$devName", 1, "col1" if(!$usuallyAtEnd{$d}); + } + $row++; + + my ($allSets, $cmdlist, $txt) = FW_devState($d, $rf, \%extPage); + + my $colSpan = ($usuallyAtEnd{$d} ? ' colspan="2"' : ''); + FW_pO ""; + + ###### + # Commands, slider, dropdown + if(!$FW_ss && $cmdlist) { + foreach my $cmd (split(":", $cmdlist)) { + my $htmlTxt; + my @c = split(' ', $cmd); # @c==0 if $cmd==" "; + if(int(@c) && $allSets && $allSets =~ m/$c[0]:([^ ]*)/) { + my $values = $1; + foreach my $fn (sort keys %{$data{webCmdFn}}) { + no strict "refs"; + $htmlTxt = &{$data{webCmdFn}{$fn}}($FW_wname, + $d, $FW_room, $cmd, $values); + use strict "refs"; + last if(defined($htmlTxt)); + } + } + if($htmlTxt) { + FW_pO $htmlTxt; + + } else { + FW_pH "cmd.$d=set $d $cmd$rf", $cmd, 1, "col3"; + } + } + } + FW_pO ""; + } + FW_pO "
$icon$devName
$txt
"; + FW_pO "

"; + + # Now the "atEnds" + foreach my $d (sort { lc(AttrVal($a, "sortby", AttrVal($a,"alias",$a))) cmp + lc(AttrVal($b, "sortby", AttrVal($b,"alias",$b))) } + @atEnds) { + no strict "refs"; + FW_pO &{$modules{$defs{$d}{TYPE}}{FW_summaryFn}}($FW_wname, $d, + $FW_room, \%extPage); + use strict "refs"; + } + FW_pO "
"; + FW_pO "
"; +} + +sub +FW_parseColumns() +{ + my %columns; + my $colNo = -1; + + foreach my $roomgroup (split("[ \t\r\n]+", AttrVal($FW_wname,"column",""))) { + my ($room, $groupcolumn)=split(":",$roomgroup); + last if(!defined($room) || !defined($groupcolumn)); + next if($room ne $FW_room); + $colNo = 1; + foreach my $groups (split(/\|/,$groupcolumn)) { + foreach my $group (split(",",$groups)) { + $columns{$group} = $colNo; + } + $colNo++; + } + } + return (\%columns, $colNo); +} + + +################# +# return a sorted list of actual files for a given regexp +sub +FW_fileList($) +{ + my ($fname) = @_; + $fname =~ m,^(.*)/([^/]*)$,; # Split into dir and file + my ($dir,$re) = ($1, $2); + return if(!$re); + $dir =~ s/%L/$attr{global}{logdir}/g if($dir =~ m/%/ && $attr{global}{logdir}); # %L present and log directory defined + $re =~ s/%./[A-Za-z0-9]*/g; # logfile magic (%Y, etc) + my @ret; + return @ret if(!opendir(DH, $dir)); + while(my $f = readdir(DH)) { + next if($f !~ m,^$re$,); + push(@ret, $f); + } + closedir(DH); + return sort @ret; +} + + +################################### +# Stream big files in chunks, to avoid bloating ourselves. +# This is a "terminal" function, no data can be appended after it is called. +sub +FW_outputChunk($$$) +{ + my ($c, $buf, $d) = @_; + $buf = $d->deflate($buf) if($d); + print $c sprintf("%x\r\n", length($buf)), $buf, "\r\n" if(length($buf)); +} + +sub +FW_returnFileAsStream($$$$$) +{ + my ($path, $suffix, $type, $doEsc, $cacheable) = @_; + + my $etag; + my $c = $FW_chash->{CD}; + + if($cacheable) { + #Check for If-None-Match header (ETag) + my @if_none_match_lines = grep /If-None-Match/, @FW_httpheader; + my $if_none_match = undef; + if(@if_none_match_lines) { + $if_none_match = $if_none_match_lines[0]; + $if_none_match =~ s/If-None-Match: \"(.*)\"/$1/; + } + + $etag = (stat($path))[9]; #mtime + if(defined($etag) && defined($if_none_match) && $etag eq $if_none_match) { + print $c "HTTP/1.1 304 Not Modified\r\n", + $FW_headercors, "\r\n"; + return -1; + } + } + + if(!open(FH, $path)) { + Log3 $FW_wname, 2, "FHEMWEB $FW_wname $path: $!"; + FW_pO "
$path: $!
"; + return 0; + } + binmode(FH) if($type !~ m/text/); # necessary for Windows + + $etag = defined($etag) ? "ETag: \"$etag\"\r\n" : ""; + my $expires = $cacheable ? ("Expires: ".gmtime(time()+900)." GMT\r\n"): ""; + my $compr = ((int(@FW_enc) == 1 && $FW_enc[0] =~ m/gzip/) && $FW_use_zlib) ? + "Content-Encoding: gzip\r\n" : ""; + print $c "HTTP/1.1 200 OK\r\n", + $compr, $expires, $FW_headercors, $etag, + "Transfer-Encoding: chunked\r\n", + "Content-Type: $type; charset=$FW_encoding\r\n\r\n"; + + my $d = Compress::Zlib::deflateInit(-WindowBits=>31) if($compr); + FW_outputChunk($c, $FW_RET, $d); + my $buf; + while(sysread(FH, $buf, 2048)) { + if($doEsc) { # FileLog special + $buf =~ s//>/g; + } + FW_outputChunk($c, $buf, $d); + } + close(FH); + FW_outputChunk($c, $suffix, $d); + + if($compr) { + $buf = $d->flush(); + print $c sprintf("%x\r\n", length($buf)), $buf, "\r\n" if($buf); + } + print $c "0\r\n\r\n"; + return -1; +} + + +################## +sub +FW_fatal($) +{ + my ($msg) = @_; + FW_pO "$msg"; +} + +################## +sub +FW_hidden($$) +{ + my ($n, $v) = @_; + return ""; +} + +################## +# Generate a select field with option list +sub +FW_select($$$$$@) +{ + my ($id, $n, $va, $def, $class, $jSelFn) = @_; + $jSelFn = ($jSelFn ? "onchange=\"$jSelFn\"" : ""); + $id = ($id ? "id=\"$id\" informId=\"$id\"" : ""); + my $s = ""; + return $s; +} + +################## +sub +FW_textfieldv($$$$) +{ + my ($n, $z, $class, $value) = @_; + my $v; + $v=" value=\"$value\"" if(defined($value)); + return if($FW_hiddenroom{input}); + my $s = ""; + return $s; +} + +sub +FW_textfield($$$) +{ + return FW_textfieldv($_[0], $_[1], $_[2], ""); +} + +################## +sub +FW_submit($$@) +{ + my ($n, $v, $class) = @_; + $class = ($class ? "class=\"$class\"" : ""); + my $s =""; + return $s; +} + +################## +sub +FW_displayFileList($@) +{ + my ($heading,@files)= @_; + my $hid = lc($heading); + $hid =~ s/[^A-Za-z]/_/g; + FW_pO "
$heading
"; + FW_pO ""; + my $row = 0; + foreach my $f (@files) { + FW_pO ""; + FW_pH "cmd=style edit $f", $f, 1; + FW_pO ""; + $row = ($row+1)%2; + } + FW_pO "
"; + FW_pO "
"; +} + +################## +sub +FW_fileNameToPath($) +{ + my $name = shift; + + $attr{global}{configfile} =~ m,([^/]*)$,; + my $cfgFileName = $1; + if($name eq $cfgFileName) { + return $attr{global}{configfile}; + } elsif($name =~ m/.*(css|svg)$/) { + return "$FW_cssdir/$name"; + } elsif($name =~ m/.*gplot$/) { + return "$FW_gplotdir/$name"; + } else { + return "$MW_dir/$name"; + } +} + +################## +# List/Edit/Save css and gnuplot files +sub +FW_style($$) +{ + my ($cmd, $msg) = @_; + my @a = split(" ", $cmd); + + my $ac = AttrVal($FW_wname,"allowedCommands",""); + return if($ac && $ac !~ m/\b$a[0]\b/); + + my $start = "
"; + my $end = "
"; + + if($a[1] eq "list") { + FW_pO $start; + FW_pO "$msg

" if($msg); + + $attr{global}{configfile} =~ m,([^/]*)$,; + my $cfgFileName = $1; + FW_displayFileList("config file", $cfgFileName) + if($attr{global}{configfile} ne 'configDB'); + FW_displayFileList("Own modules and helper files", + FW_fileList("$MW_dir/^(.*sh|[0-9][0-9].*Util.*pm|.*cfg|.*holiday". + "|.*layout)\$")); + FW_displayFileList("styles", + FW_fileList("$FW_cssdir/^.*(css|svg)\$")); + FW_displayFileList("gplot files", + FW_fileList("$FW_gplotdir/^.*gplot\$")); + FW_pO $end; + + } elsif($a[1] eq "select") { + my @fl = grep { $_ !~ m/(floorplan|dashboard)/ } FW_fileList("$FW_cssdir/.*style.css"); + FW_pO "$start"; + my $row = 0; + foreach my $file (@fl) { + next if($file =~ m/svg_/); + $file =~ s/style.css//; + $file = "default" if($file eq ""); + FW_pO ""; + FW_pH "cmd=style set $file", "$file", 1; + FW_pO ""; + $row = ($row+1)%2; + } + FW_pO "
$end"; + + } elsif($a[1] eq "set") { + if($a[2] eq "default") { + CommandDeleteAttr(undef, "$FW_wname stylesheetPrefix"); + } else { + CommandAttr(undef, "$FW_wname stylesheetPrefix $a[2]"); + } + FW_pO "${start}Reload the page in the browser.$end"; + + } elsif($a[1] eq "edit") { + my $fileName = $a[2]; + $fileName =~ s,.*/,,g; # Little bit of security + my $filePath = FW_fileNameToPath($fileName); + if(!open(FH, $filePath)) { + FW_pO "
$filePath: $!
"; + return; + } + my $data = join("", ); + close(FH); + + $data =~ s/&/&/g; + + my $ncols = $FW_ss ? 40 : 80; + FW_pO "
"; + FW_pO "
"; + FW_pO FW_submit("save", "Save $fileName"); + FW_pO "  "; + FW_pO FW_submit("saveAs", "Save as"); + FW_pO FW_textfieldv("saveName", 30, "saveName", $fileName); + FW_pO "

"; + FW_pO FW_hidden("cmd", "style save $fileName"); + FW_pO ""; + FW_pO "
"; + + if( AttrVal($FW_wname,'enhancedEditor',0) ) { + my $ext = ($fileName =~ m/\.(.*)$/)[0]; + FW_codemirrorLink($ext); + FW_pO "". + ""; + } + + FW_pO "
"; + + } elsif($a[1] eq "save") { + my $fileName = $a[2]; + $fileName = $FW_webArgs{saveName} + if($FW_webArgs{saveAs} && $FW_webArgs{saveName}); + $fileName =~ s,.*/,,g; # Little bit of security + my $filePath = FW_fileNameToPath($fileName); + + if(!open(FH, ">$filePath")) { + FW_pO "
$filePath: $!
"; + return; + } + $FW_data =~ s/\r//g if($^O !~ m/Win/); + binmode (FH); + print FH $FW_data; + close(FH); + + my $ret = FW_fC("rereadcfg") if($filePath eq $attr{global}{configfile}); + $ret = FW_fC("reload $fileName") if($fileName =~ m,\.pm$,); + $ret = ($ret ? "

ERROR:

$ret" : "Saved the file $fileName"); + FW_style("style list", $ret); + $ret = ""; + + } elsif($a[1] eq "iconFor") { + FW_iconTable("iconFor", "icon", "style setIF $a[2] %s", undef); + + } elsif($a[1] eq "setIF") { + FW_fC("attr $a[2] icon $a[3]"); + FW_doDetail($a[2]); + + } elsif($a[1] eq "showDSI") { + FW_iconTable("devStateIcon", "", + "style addDSI $a[2] %s", "Enter value/regexp for STATE"); + + } elsif($a[1] eq "addDSI") { + my $dsi = AttrVal($a[2], "devStateIcon", ""); + $dsi .= " " if($dsi); + FW_fC("attr $a[2] devStateIcon $dsi$FW_data:$a[3]"); + FW_doDetail($a[2]); + + } elsif($a[1] eq "eventMonitor") { + FW_pO ""; + FW_pO "
"; + FW_pO "
"; + FW_pO "Events:
\n"; + FW_pO "
"; + FW_pO "
"; + + } + +} + +sub +FW_iconTable($$$$) +{ + my ($name, $class, $cmdFmt, $textfield) = @_; + + my %icoList = (); + foreach my $style (@FW_iconDirs) { + foreach my $imgName (sort keys %{$FW_icons{$style}}) { + $imgName =~ s/\.[^.]*$//; # Cut extension + next if(!$FW_icons{$style}{$imgName}); # Dont cut it twice: FS20.on.png + next if($FW_icons{$style}{$imgName} !~ m/$imgName/); # Skip alias + next if($imgName=~m+^(weather/|shutter.*big|fhemicon|favicon|ws_.*_kl)+); + next if($imgName=~m+^(dashboardicons)+); + $icoList{$imgName} = 1; + } + } + + FW_pO "
"; + FW_pO "
"; + if($textfield) { + FW_pO "$textfield: ".FW_textfieldv("data",20,"iconTable",".*")."
"; + } + foreach my $i (sort keys %icoList) { + FW_pF "", $i, $i, FW_makeImage($i,$i,$class); + } + FW_pO "
"; + FW_pO "
"; +} + +################## +# print (append) to output +sub +FW_pO(@) +{ + my $arg = shift; + return if(!defined($arg)); + $FW_RET .= $arg; + $FW_RET .= "\n"; +} + +################# +# add href +sub +FW_pH(@) +{ + my ($link, $txt, $td, $class, $doRet,$nonl) = @_; + my $ret; + + $link = ($link =~ m,^/,) ? $link : "$FW_ME$FW_subdir?$link"; + + # Using onclick, as href starts safari in a webapp. + # Known issue: the pointer won't change + if($FW_ss || $FW_tp) { + $ret = "$txt"; + } else { + $ret = "$txt"; + } + + #actually 'div' should be removed if no class is defined + # as I can't check all code for consistancy I add nonl instead + $class = ($class)?" class=\"$class\"":""; + $ret = "$ret" if (!$nonl); + + $ret = "$ret" if($td); + return $ret if($doRet); + FW_pO $ret; +} + +################# +# href without class/div, returned as a string +sub +FW_pHPlain(@) +{ + my ($link, $txt, $td) = @_; + + $link = "?$link" if($link !~ m+^/+); + my $ret = ""; + $ret .= "" if($td); + if($FW_ss || $FW_tp) { + $ret .= "$txt"; + } else { + $ret .= "$txt"; + } + $ret .= "" if($td); + return $ret; +} + + +############################## +sub +FW_makeImage(@) +{ + my ($name, $txt, $class)= @_; + + $txt = $name if(!defined($txt)); + $class = "" if(!$class); + $class = "$class $name"; + $class =~ s/\./_/g; + $class =~ s/@/ /g; + + my $p = FW_iconPath($name); + return $name if(!$p); + if($p =~ m/\.svg$/i) { + if(open(FH, "$FW_icondir/$p")) { + ; ; ; # Skip the first 3 lines; + my $data = join("", ); + close(FH); + $data =~ s/[\r\n]/ /g; + $data =~ s/ *$//g; + $data =~ s/"; + } +} + +#### +sub +FW_IconURL($) +{ + my ($name)= @_; + return "$FW_ME/icons/$name"; +} + +################## +# print formatted +sub +FW_pF($@) +{ + my $fmt = shift; + $FW_RET .= sprintf $fmt, @_; +} + +################## +# fhem command +sub +FW_fC($@) +{ + my ($cmd, $unique) = @_; + my $ret; + if($unique) { + $ret = AnalyzeCommand($FW_chash, $cmd, + AttrVal($FW_wname,"allowedCommands",undef)); + } else { + $ret = AnalyzeCommandChain($FW_chash, $cmd, + AttrVal($FW_wname,"allowedCommands",undef)); + } + return $ret; +} + +sub +FW_Attr(@) +{ + my @a = @_; + my $hash = $defs{$a[1]}; + my $name = $hash->{NAME}; + my $sP = "stylesheetPrefix"; + my $retMsg; + + if($a[0] eq "set" && $a[2] eq "HTTPS") { + TcpServer_SetSSL($hash); + } + + if($a[0] eq "set") { # Converting styles + if($a[2] eq "smallscreen" || $a[2] eq "touchpad") { + $attr{$name}{$sP} = $a[2]; + $retMsg="$name: attribute $a[2] deprecated, converted to $sP"; + $a[3] = $a[2]; $a[2] = $sP; + } + } + if($a[2] eq $sP) { + # AttrFn is called too early, we have to set/del the attr here + if($a[0] eq "set") { + $attr{$name}{$sP} = (defined($a[3]) ? $a[3] : "default"); + FW_readIcons($attr{$name}{$sP}); + } else { + delete $attr{$name}{$sP}; + } + } + + if($a[2] eq "iconPath" && $a[0] eq "set") { + foreach my $pe (split(":", $a[3])) { + $pe =~ s+\.\.++g; + FW_readIcons($pe); + } + } + + return $retMsg; +} + + +# recursion starts at $FW_icondir/$dir +# filenames are relative to $FW_icondir +sub +FW_readIconsFrom($$) +{ + my ($dir,$subdir)= @_; + + my $ldir = ($subdir ? "$dir/$subdir" : $dir); + my @entries; + if(opendir(DH, "$FW_icondir/$ldir")) { + @entries= sort readdir(DH); # assures order: .gif .ico .jpg .png .svg + closedir(DH); + } + + foreach my $entry (@entries) { + if( -d "$FW_icondir/$ldir/$entry" ) { # directory -> recurse + FW_readIconsFrom($dir, $subdir ? "$subdir/$entry" : $entry) + unless($entry eq "." || $entry eq ".." || $entry eq ".svn"); + + } else { + if($entry =~ m/^iconalias.txt$/i && open(FH, "$FW_icondir/$ldir/$entry")){ + while(my $l = ) { + chomp($l); + my @a = split(" ", $l); + next if($l =~ m/^#/ || @a < 2); + $FW_icons{$dir}{$a[0]} = $a[1]; + } + close(FH); + } elsif($entry =~ m/(gif|ico|jpg|png|jpeg|svg)$/i) { + my $filename = $subdir ? "$subdir/$entry" : $entry; + $FW_icons{$dir}{$filename} = $filename; + + my $tag = $filename; # Add it without extension too + $tag =~ s/\.[^.]*$//; + $FW_icons{$dir}{$tag} = $filename; + } + } + } + $FW_icons{$dir}{""} = 1; # Do not check empty directories again. +} + +sub +FW_readIcons($) +{ + my ($dir)= @_; + return if($FW_icons{$dir}); + FW_readIconsFrom($dir, ""); +} + + +# check if the icon exists, and if yes, returns its "logical" name; +sub +FW_iconName($) +{ + my ($name)= @_; + $name =~ s/@.*//; + foreach my $pe (@FW_iconDirs) { + return $name if($pe && $FW_icons{$pe} && $FW_icons{$pe}{$name}); + } + return undef; +} + +# returns the physical absolute path relative for the logical path +# examples: +# FS20.on -> dark/FS20.on.png +# weather/sunny -> default/weather/sunny.gif +sub +FW_iconPath($) +{ + my ($name) = @_; + $name =~ s/@.*//; + foreach my $pe (@FW_iconDirs) { + return "$pe/$FW_icons{$pe}{$name}" + if($pe && $FW_icons{$pe} && $FW_icons{$pe}{$name}); + } + return undef; +} + +sub +FW_dev2image($;$) +{ + my ($name, $state) = @_; + my $d = $defs{$name}; + return "" if(!$name || !$d); + + my $type = $d->{TYPE}; + $state = $d->{STATE} if(!defined($state)); + return "" if(!$type || !defined($state)); + + my $model = $attr{$name}{model} if(defined($attr{$name}{model})); + + my (undef, $rstate) = ReplaceEventMap($name, [undef, $state], 0); + + my ($icon, $rlink); + my $devStateIcon = AttrVal($name, "devStateIcon", undef); + if(defined($devStateIcon) && $devStateIcon =~ m/^{.*}$/) { + my ($html, $link) = eval $devStateIcon; + Log3 $FW_wname, 1, "devStateIcon $name: $@" if($@); + return ($html, $link, 1) if(defined($html) && $html =~ m/^<.*>$/s); + $devStateIcon = $html; + } + + if(defined($devStateIcon)) { + my @list = split(" ", $devStateIcon); + foreach my $l (@list) { + my ($re, $iconName, $link) = split(":", $l, 3); + if(defined($re) && $state =~ m/^$re$/) { + if($iconName eq "") { + $rlink = $link; + last; + } + if(defined(FW_iconName($iconName))) { + return ($iconName, $link, 0); + } else { + return ($state, $link, 1); + } + } + } + } + + $state =~ s/ .*//; # Want to be able to have icons for "on-for-timer xxx" + + $icon = FW_iconName("$name.$state") if(!$icon); # lamp.Aus.png + $icon = FW_iconName("$name.$rstate") if(!$icon); # lamp.on.png + $icon = FW_iconName($name) if(!$icon); # lamp.png + $icon = FW_iconName("$model.$state") if(!$icon && $model); # fs20st.off.png + $icon = FW_iconName($model) if(!$icon && $model); # fs20st.png + $icon = FW_iconName("$type.$state") if(!$icon); # FS20.Aus.png + $icon = FW_iconName("$type.$rstate") if(!$icon); # FS20.on.png + $icon = FW_iconName($type) if(!$icon); # FS20.png + $icon = FW_iconName($state) if(!$icon); # Aus.png + $icon = FW_iconName($rstate) if(!$icon); # on.png + return ($icon, $rlink, 0); +} + +sub +FW_makeEdit($$$) +{ + my ($name, $n, $val) = @_; + + # Toggle Edit-Window visibility script. + my $pgm = "Javascript:" . + "s=document.getElementById('edit').style;". + "s.display = s.display=='none' ? 'block' : 'none';". + "s=document.getElementById('disp').style;". + "s.display = s.display=='none' ? 'block' : 'none';"; + $pgm .= "s=document.getElementById('editarea');". + "s.editor ? s.editor: s.editor = CodeMirror.fromTextArea(s,". FW_codemirrorParams("pm") .");" + if AttrVal($FW_wname,'enhancedEditor',0); + + FW_pO ""; + FW_pO "$n"; + FW_pO ""; + + $val =~ s,\\\n,\n,g; + my $eval = $val; + $eval = "
$eval
" if($eval =~ m/\n/); + FW_pO ""; + FW_pO "
$eval
"; + FW_pO ""; + + FW_pO ""; + FW_pO "
"; + FW_pO "
"; + FW_pO FW_hidden("detail", $name); + my $cmd = "modify"; + my $ncols = $FW_ss ? 30 : 60; + FW_pO ""; + FW_pO "
" . FW_submit("cmd.${cmd}$name", "$cmd $name"); + FW_pO "
"; + FW_codemirrorLink("pm") if( AttrVal($FW_wname,'enhancedEditor',0) ); + FW_pO ""; +} + + +sub +FW_roomStatesForInform($) +{ + my ($me) = @_; + return "" if($me->{inform}{type} !~ m/status/); + + my %extPage = (); + my @data; + foreach my $dn (keys %{$me->{inform}{devices}}) { + next if(!defined($defs{$dn})); + my $t = $defs{$dn}{TYPE}; + next if(!$t || $modules{$t}{FW_atPageEnd}); + my ($allSet, $cmdlist, $txt) = FW_devState($dn, "", \%extPage); + if($defs{$dn} && $defs{$dn}{STATE} && $defs{$dn}{TYPE} ne "weblink") { + push @data, "$dn<<$defs{$dn}{STATE}<<$txt"; + } + } + my $data = join("\n", map { s/\n/ /gm; $_ } @data)."\n"; + return $data; +} + +sub +FW_Notify($$) +{ + my ($ntfy, $dev) = @_; + + my $h = $ntfy->{inform}; + return undef if(!$h); + + my $dn = $dev->{NAME}; + return undef if(!$h->{devices}{$dn}); + + my @data; + my %extPage; + + if($h->{type} =~ m/status/) { + # Why is saving this stuff needed? FLOORPLAN? + my @old = ($FW_wname, $FW_ME, $FW_ss, $FW_tp, $FW_subdir); + $FW_wname = $ntfy->{SNAME}; + $FW_ME = "/" . AttrVal($FW_wname, "webname", "fhem"); + $FW_subdir = ""; + $FW_sp = AttrVal($FW_wname, "stylesheetPrefix", 0); + $FW_ss = ($FW_sp =~ m/smallscreen/); + $FW_tp = ($FW_sp =~ m/smallscreen|touchpad/); + @FW_iconDirs = grep { $_ } split(":", AttrVal($FW_wname, "iconPath", + "$FW_sp:default:fhemSVG:openautomation")); + if($h->{iconPath}) { + unshift @FW_iconDirs, $h->{iconPath}; + FW_readIcons($h->{iconPath}); + } + + my ($allSet, $cmdlist, $txt) = FW_devState($dn, "", \%extPage); + ($FW_wname, $FW_ME, $FW_ss, $FW_tp, $FW_subdir) = @old; + push @data, "$dn<<$dev->{STATE}<<$txt"; + + #Add READINGS + if($dev->{CHANGED}) { # It gets deleted sometimes (?) + my $tn = TimeNow(); + my $max = int(@{$dev->{CHANGED}}); + for(my $i = 0; $i < $max; $i++) { + if( $dev->{CHANGED}[$i] !~ /: /) { + next; #ignore 'set' commands + } + my ($readingName,$readingVal) = split(": ",$dev->{CHANGED}[$i],2); + push @data, "$dn-$readingName<<$readingVal<<$readingVal"; + push @data, "$dn-$readingName-ts<<$tn<<$tn"; + } + } + } + + if($h->{type} =~ m/raw/) { + if($dev->{CHANGED}) { # It gets deleted sometimes (?) + my $tn = TimeNow(); + if($attr{global}{mseclog}) { + my ($seconds, $microseconds) = gettimeofday(); + $tn .= sprintf(".%03d", $microseconds/1000); + } + my $max = int(@{$dev->{CHANGED}}); + my $dt = $dev->{TYPE}; + for(my $i = 0; $i < $max; $i++) { + push @data,("$tn $dt $dn ".$dev->{CHANGED}[$i]."
"); + } + } + } + + addToWritebuffer($ntfy, join("\n", map { s/\n/ /gm; $_ } @data)."\n") + if(@data); + return undef; +} + +################### +# Compute the state (==second) column +sub +FW_devState($$@) +{ + my ($d, $rf, $extPage) = @_; + + my ($hasOnOff, $link); + + my $cmdList = AttrVal($d, "webCmd", ""); + my $allSets = getAllSets($d); + my $state = $defs{$d}{STATE}; + $state = "" if(!defined($state)); + + $hasOnOff = ($allSets =~ m/(^| )on(:[^ ]*)?( |$)/ && + $allSets =~ m/(^| )off(:[^ ]*)?( |$)/); + my $txt = $state; + my $dsi = AttrVal($d, "devStateIcon", undef); + + if(defined(AttrVal($d, "showtime", undef))) { + my $v = $defs{$d}{READINGS}{state}{TIME}; + $txt = $v if(defined($v)); + + } elsif(!$dsi && $allSets =~ m/\bdesired-temp:/) { + $txt = "$1 °C" if($txt =~ m/^measured-temp: (.*)/); # FHT fix + $cmdList = "desired-temp" if(!$cmdList); + + } elsif(!$dsi && $allSets =~ m/\bdesiredTemperature:/) { + $txt = ReadingsVal($d, "temperature", ""); # ignores stateFormat!!! + $txt =~ s/ .*//; + $txt .= "°C"; + $cmdList = "desiredTemperature" if(!$cmdList); + + } else { + my ($icon, $isHtml); + ($icon, $link, $isHtml) = FW_dev2image($d); + $txt = ($isHtml ? $icon : FW_makeImage($icon, $state)) if($icon); + $link = "cmd.$d=set $d $link" if($link); + + } + + + if($hasOnOff) { + # Have to cover: "on:An off:Aus", "A0:Aus AI:An Aus:off An:on" + my $on = ReplaceEventMap($d, "on", 1); + my $off = ReplaceEventMap($d, "off", 1); + $link = "cmd.$d=set $d " . ($state eq $on ? $off : $on) if(!$link); + $cmdList = "$on:$off" if(!$cmdList); + + } + + if($link) { # Have command to execute + my $room = AttrVal($d, "room", undef); + if($room) { + if($FW_room && $room =~ m/\b$FW_room\b/) { + $room = $FW_room; + } else { + $room =~ s/,.*//; + } + $link .= "&room=$room"; + } + if(AttrVal($FW_wname, "longpoll", 1)) { + $txt = "$txt"; + + } elsif($FW_ss || $FW_tp) { + $txt ="$txt"; + + } else { + $txt = "$txt"; + + } + } + + my $style = AttrVal($d, "devStateStyle", ""); + $txt = "
$txt
"; + + my $type = $defs{$d}{TYPE}; + my $sfn = $modules{$type}{FW_summaryFn}; + if($sfn) { + if(!defined($extPage)) { + my %hash; + $extPage = \%hash; + } + no strict "refs"; + my $newtxt = &{$sfn}($FW_wname, $d, $FW_room, $extPage); + use strict "refs"; + $txt = $newtxt if(defined($newtxt)); # As specified + } + + return ($allSets, $cmdList, $txt); +} + + +sub +FW_Get($@) +{ + my ($hash, @a) = @_; + $FW_wname= $hash->{NAME}; + + my $arg = (defined($a[1]) ? $a[1] : ""); + if($arg eq "icon") { + return "need one icon as argument" if(int(@a) != 3); + my $icon = FW_iconPath($a[2]); + return defined($icon) ? "$FW_icondir/$icon" : "no such icon"; + + } elsif($arg eq "pathlist") { + return "web server root: $FW_dir\n". + "icon directory: $FW_icondir\n". + "css directory: $FW_cssdir\n". + "gplot directory: $FW_gplotdir"; + + } else { + return "Unknown argument $arg choose one of icon pathlist:noArg"; + + } +} + + +##################################### +sub +FW_Set($@) +{ + my ($hash, @a) = @_; + my %cmd = ("rereadicons" => 1, "clearSvgCache" => 1); + + return "no set value specified" if(@a < 2); + return ("Unknown argument $a[1], choose one of ". + join(" ", map { "$_:noArg" } sort keys %cmd)) + if(!$cmd{$a[1]}); + + if($a[1] eq "rereadicons") { + my @dirs = keys %FW_icons; + %FW_icons = (); + foreach my $d (@dirs) { + FW_readIcons($d); + } + } + if($a[1] eq "clearSvgCache") { + my $cDir = "$FW_dir/SVGcache"; + if(opendir(DH, $cDir)) { + map { my $n="$cDir/$_"; unlink($n) if(-f $n); } readdir(DH);; + closedir(DH); + } else { + return "Can't open $cDir: $!"; + } + } + return undef; +} + +##################################### +sub +FW_closeOldClients() +{ + my $now = time(); + foreach my $dev (keys %defs) { + next if(!$defs{$dev}{TYPE} || $defs{$dev}{TYPE} ne "FHEMWEB" || + !$defs{$dev}{LASTACCESS} || $defs{$dev}{inform} || + ($now - $defs{$dev}{LASTACCESS}) < 60); + Log3 $FW_wname, 4, "Closing connection $dev"; + FW_Undef($defs{$dev}, ""); + delete $defs{$dev}; + } + InternalTimer($now+60, "FW_closeOldClients", 0, 0); +} + +sub +FW_htmlEscape($) +{ + my ($txt) = @_; + $txt =~ s//>/g; + return $txt; +} + +########################### +# Widgets START +sub +FW_sliderFn($$$$$) +{ + my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; + + return undef if($values !~ m/^slider,(.*),(.*),(.*)$/); + return "" if($cmd =~ m/ /); # webCmd pct 30 should generate a link + my ($min,$stp, $max) = ($1, $2, $3); + my $srf = $FW_room ? "&room=$FW_room" : ""; + my $cv = ReadingsVal($d, $cmd, Value($d)); + my $id = ($cmd eq "state") ? "" : "-$cmd"; + $cmd = "" if($cmd eq "state"); + $cv =~ s/.*?([.\-\d]+).*/$1/; # get first number + $cv = 0 if($cv !~ m/\d/); + return "". + "
". + "
$min
". + "
". + "". + ""; +} + +sub +FW_noArgFn($$$$$) +{ + my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; + + return undef if($values !~ m/^noArg$/); + return ""; +} + +sub +FW_timepickerFn() +{ + my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; + + return undef if($values ne "time"); + return "" if($cmd =~ m/ /); # webCmd on-for-timer 30 should generate a link + my $srf = $FW_room ? "&room=$FW_room" : ""; + my $cv = ReadingsVal($d, $cmd, Value($d)); + $cmd = "" if($cmd eq "state"); + my $c = "\"$FW_ME?cmd=set $d $cmd %$srf\""; + return "". + "". + "". + ""; +} + +sub +FW_dropdownFn() +{ + my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; + + return "" if($cmd =~ m/ /); # webCmd temp 30 should generate a link + my @tv = split(",", $values); + # Hack: eventmap (translation only) should not result in a + # dropdown. eventMap/webCmd/etc handling must be cleaned up. + if(@tv > 1) { + my $txt; + if($cmd eq "desired-temp" || $cmd eq "desiredTemperature") { + $txt = ReadingsVal($d, $cmd, 20); + $txt =~ s/ .*//; # Cut off Celsius + $txt = sprintf("%2.1f", int(2*$txt)/2) if($txt =~ m/[0-9.-]/); + } else { + $txt = ReadingsVal($d, $cmd, Value($d)); + $txt =~ s/$cmd //; + } + + my $fpname = $FW_wname; + $fpname =~ s/.*floorplan\/(\w+)$/$1/; #allow usage of attr fp_setbutton + my $fwsel; + $fwsel = ($cmd eq "state" ? "" : "$cmd ") . + FW_select("$d-$cmd","val.$d", \@tv, $txt,"dropdown","submit()"). + FW_hidden("cmd.$d", "set"); + + return "
". + FW_hidden("arg.$d", $cmd) . + FW_hidden("dev.$d", $d) . + ($FW_room ? FW_hidden("room", $FW_room) : "") . + "$fwsel
"; + } + return undef; +} + +sub +FW_textFieldFn($$$$) +{ + my ($FW_wname, $d, $FW_room, $cmd, $values) = @_; + + my @args = split("[ \t]+", $cmd); + + return undef if($values !~ m/^textField$/); + return "" if($cmd =~ m/ /); + my $srf = $FW_room ? "&room=$FW_room" : ""; + my $cv = ReadingsVal($d, $cmd, ""); + my $id = ($cmd eq "state") ? "" : "-$cmd"; + + my $c = "$FW_ME?XHR=1&cmd=setreading $d $cmd %$srf"; + return ''. + "
$cmd:
". + ''; +} + +# Widgets END +########################### + +sub +FW_visibleDevices(;$) +{ + my($FW_wname) = @_; + + my %devices = (); + foreach my $d (sort keys %defs) { + next if(!defined($defs{$d})); + my $h = $defs{$d}; + next if(!$h->{TEMPORARY}); + next if($h->{TYPE} ne "FHEMWEB"); + next if(defined($FW_wname) && $h->{SNAME} ne $FW_wname); + + next if(!defined($h->{inform})); + + @devices{ keys %{$h->{inform}->{devices}} } = + values %{$h->{inform}->{devices}}; + } + return %devices; +} + +sub +FW_ActivateInform() +{ + $FW_activateInform = 1; +} + +my %codemirrorMapping = ( pm => "fhem", + cfg => "fhem", + layout => "fhem", + css => "css", + svg => "xml", +); + +sub FW_codemirrorLink($) +{ + my ($ext) = @_; + + my $lang = $codemirrorMapping{$ext}; + my $theme = AttrVal($FW_wname,'enhancedEditorTheme','blackboard'); + + $lang = "perl" if( !$lang ); + + FW_pO ""; + FW_pO ""; + FW_pO ""; + FW_pO ""; + FW_pO ""; + FW_pO ""; + FW_pO ""; + FW_pO ""; + FW_pO "" if( $lang ); + return; +} + +sub FW_codemirrorParams($) +{ + my ($ext) = @_; + + my $theme = AttrVal($FW_wname,'enhancedEditorTheme','blackboard'); + return "{theme: '". $theme. "', lineNumbers: true, matchBrackets: true, autoCloseBrackets: true, ". + "extraKeys: {'Ctrl-Space': 'autocomplete'} }"; +} + +1; + +=pod +=begin html + + +

FHEMWEB

+
    + FHEMWEB is the builtin web-frontend, it also implements a simple web + server (optionally with Basic-Auth and HTTPS). +

    + + + Define +
      + define <name> FHEMWEB <tcp-portnr> [global] +

      + Enable the webfrontend on port <tcp-portnr>. If global is specified, + then requests from all interfaces (not only localhost / 127.0.0.1) are + serviced.
      + To enable listening on IPV6 see the comments here. +
      +
    +
    + + + Set +
      +
    • rereadicons
      + reads the names of the icons from the icon path. Use after adding or + deleting icons. +
    • +
    • clearSvgCache
      + delete all files found in the www/SVGcache directory, which is used to + cache SVG data, if the SVGcache attribute is set. +
    • +
    +
    + + + Get +
      +
    • icon <logical icon>
      + returns the absolute path to the logical icon. Example: +
        + get myFHEMWEB icon FS20.on
        + /data/Homeautomation/fhem/FHEM/FS20.on.png +
        +
      +
    • +
    • pathlist
      + return FHEMWEB specific directories, where files for given types are + located +

      + +
    + + + Attributes +
      + +
    • webname
      + Path after the http://hostname:port/ specification. Defaults to fhem, + i.e the default http address is http://localhost:8083/fhem +

    • + + +
    • refresh
      + If set, a http-equiv="refresh" entry will be genererated with the given + argument (i.e. the browser will reload the page after the given + seconds). +

    • + + +
    • plotmode
      + Specifies how to generate the plots: +
        +
      • SVG
        + The plots are created with the SVG module. + This is the default.
      • + +
      • gnuplot
        + The plots are created with the gnuplot program. Note: this mode + ist only available due to historic reasons.
      • + +
      • gnuplot-scroll
        + Like the gnuplot-mode, but scrolling to historical values is alos + possible, just like with SVG.
      • +
      +

    • + + +
    • plotsize
      + the default size of the plot, in pixels, separated by comma: + width,height. You can set individual sizes by setting the plotsize of + the SVG. Default is 800,160 for desktop, and 480,160 for + smallscreen. +

    • + + +
    • nrAxis
      + the number of axis for which space should be reserved on the left and + right sides of a plot and optionaly how many axes should realy be used + on each side, separated by comma: left,right[,useLeft,useRight]. You + can set individual numbers by setting the nrAxis of the SVG. Default is + 1,1. +

    • + + +
    • SVGcache
      + if set, cache plots which won't change any more (the end-date is prior + to the current timestamp). The files are written to the www/SVGcache + directory. Default is off.
      + See also the clearSvgCache command for clearing the cache. +

    • + + +
    • endPlotToday
      + If this FHEMWEB attribute is set to 1, then week and month plots will + end today. Else the current week (starting at Sunday) or the current + month will be shown.
      +

    • + + +
    • endPlotNow
      + If this FHEMWEB attribute is set to 1, then day and hour plots will + end at current time. Else the whole day, the 6 hour period starting at + 0, 6, 12 or 18 hour or the whole hour will be shown. This attribute + is not used if the SVG has the attribute startDate defined.
      +

    • + + +
    • ploteditor
      + Configures if the Plot editor should be shown + in the SVG detail view. + Can be set to always, onClick or never. Default is always. +

    • + + +
    • plotfork
      + If set, generate the logs in a parallel process. Note: do not use it + on Windows and on systems with small memory foorprint. +

    • + + +
    • basicAuth, basicAuthMsg
      + request a username/password authentication for access. You have to set + the basicAuth attribute to the Base64 encoded value of + <user>:<password>, e.g.:
        + # Calculate first the encoded string with the commandline program
        + $ echo -n fhemuser:secret | base64
        + ZmhlbXVzZXI6c2VjcmV0
        + fhem.cfg:
        + attr WEB basicAuth ZmhlbXVzZXI6c2VjcmV0 +
      + You can of course use other means of base64 encoding, e.g. online + Base64 encoders. If basicAuthMsg is set, it will be displayed in the + popup window when requesting the username/password.
      +
      + If the argument of basicAuth is enclosed in {}, then it will be + evaluated, and the $user and $password variable will be set to the + values entered. If the return value is true, then the password will be + accepted. + Example:
      + + attr WEB basicAuth { "$user:$password" eq "admin:secret" }
      +
      +

    • + + +
    • HTTPS
      + Enable HTTPS connections. This feature requires the perl module + IO::Socket::SSL, to be installed with cpan -i IO::Socket::SSL or + apt-get install libio-socket-ssl-perl; OSX and the FritzBox-7390 + already have this module.
      + + A local certificate has to be generated into a directory called certs, + this directory must be in the modpath + directory, at the same level as the FHEM directory. +
        + mkdir certs
        + cd certs
        + openssl req -new -x509 -nodes -out server-cert.pem -days 3650 -keyout server-key.pem +
      +
      +
    • + + +
    • allowedCommands
      + A comma separated list of commands allowed from this FHEMWEB + instance.
      If set to an empty list , (i.e. comma only) + then this FHEMWEB instance will be read-only.
      If set to + get,set, then this FHEMWEB instance will only allow + regular usage of the frontend by clicking the icons/buttons/sliders but + not changing any configuration.
      + + + This attribute intended to be used together with hiddenroom/hiddengroup +
      + + Note:allowedCommands should work as intended, but no guarantee + can be given that there is no way to circumvent it. If a command is + allowed it can be issued by URL manipulation also for devices that are + hidden.

    • + +
    • allowfrom
    • +
      + + +
    • stylesheetPrefix
      + prefix for the files style.css, svg_style.css and svg_defs.svg. If the + file with the prefix is missing, the default file (without prefix) will + be used. These files have to be placed into the FHEM directory, and can + be selected directly from the "Select style" FHEMWEB menu entry. Example: +
        + attr WEB stylesheetPrefix dark
        +
        + Referenced files:
        +
          + darksvg_defs.svg
          + darksvg_style.css
          + darkstyle.css
          +
        +
        +
      + Note:if the argument contains the string smallscreen or touchpad, + then FHEMWEB will optimize the layout/access for small screen size (i.e. + smartphones) or touchpad devices (i.e. tablets)
      + + The default configuration installs 3 FHEMWEB instances: port 8083 for + desktop browsers, port 8084 for smallscreen, and 8085 for touchpad.
      + + If touchpad or smallscreen is specified, then WebApp support is + activated: After viewing the site on the iPhone or iPad in Safari, you + can add a link to the home-screen to get full-screen support. Links are + rendered differently in this mode to avoid switching back to the "normal" + browser. +
    • +
      + + +
    • iconPath
      + colon separated list of directories where the icons are read from. + The directories start in the fhem/www/images directory. The default is + $styleSheetPrefix:default:fhemSVG:openautomation
      + Set it to fhemSVG:openautomation to get only SVG images. +
    • +
      + + +
    • hiddenroom
      + Comma separated list of rooms to "hide", i.e. not to show. Special + values are input, detail and save, in which case the input areas, link + to the detailed views or save button is hidden (although each aspect + still can be addressed through URL manipulation).
      + The list can also contain values from the additional "Howto/Wiki/FAQ" + block. +
    • +
      + + +
    • hiddengroup
      + Comma separated list of groups to "hide", i.e. not to show in any room + of this FHEMWEB instance.
      + Example: attr WEBtablet hiddengroup FileLog,dummy,at,notify +
    • +
      + + +
    • menuEntries
      + Comma separated list of name,html-link pairs to display in the + left-side list. Example:
      + attr WEB menuEntries fhem.de,http://fhem.de,culfw.de,http://culfw.de
      + attr WEB menuEntries AlarmOn,http://fhemhost:8083/fhem?cmd=set%20alarm%20on
      +
    • +
      + + +
    • longpoll
      + Affects devices states in the room overview only.
      + In this mode status update is refreshed more or less instantaneously, + and state change (on/off only) is done without requesting a complete + refresh from the server. + Default is on. +
    • +
      + + +
    • longpollSVG
      + Reloads an SVG weblink, if an event should modify its content. Since + an exact determination of the affected events is too complicated, we + need some help from the #FileLog definition in the .gplot file: the + filter used there (second parameter) must either contain only the + deviceName or have the form deviceName.event or deviceName.*. This is + always the case when using the Plot + editor. The SVG will be reloaded for any event triggered by + this deviceName. + Default is off. +
    • +
      + + + +
    • redirectCmds
      + Clear the browser URL window after issuing the command by redirecting + the browser, as a reload for the same site might have unintended + side-effects. Default is 1 (enabled). Disable it by setting this + attribute to 0 if you want to study the command syntax, in order to + communicate with FHEMWEB. +
    • +
      + + +
    • fwcompress
      + Enable compressing the HTML data (default is 1, i.e. yes, use 0 to switch it off). +
    • +
      + + +
    • reverseLogs
      + Display the lines from the logfile in a reversed order, newest on the + top, so that you dont have to scroll down to look at the latest entries. + Note: enabling this attribute will prevent FHEMWEB from streaming + logfiles, resulting in a considerably increased memory consumption + (about 6 times the size of the file on the disk). +
    • +
      + + +
    • CORS
      + If set to 1, FHEMWEB will supply a "Cross origin resource sharing" + header, see the wikipedia for details. +
    • +
      + + +
    • icon
      + Set the icon for a device in the room overview. There is an + icon-chooser in FHEMWEB to ease this task. Setting icons for the room + itself is indirect: there must exist an icon with the name + ico.png in the iconPath. +
    • +
      + + +
    • roomIcons
      + Space separated list of room:icon pairs, to override the default + behaviour of showing an icon, if there is one with the name of + "icoRoomName". This is the correct way to remove the icon for the room + Everything, or to set one for rooms with / in the name (e.g. + Anlagen/EDV). The first part is treated as regexp, so space is + represented by a dot. Example:
      + attr WEB roomIcons Anlagen.EDV:icoEverything +
    • +
      + + +
    • sortRooms
      + Space separated list of rooms to override the default + sort order of the room links. Example:
      + attr WEB sortRooms DG OG EG Keller +
    • +
      + + +
    • defaultRoom
      + show the specified room if no room selected, e.g. on execution of some + commands. If set hides the motd. Example:
      + attr WEB defaultRoom Zentrale +
    • +
      + + +
    • sortby
      + Take the value of this attribute when sorting the devices in the room + overview instead of the alias, or if that is missing the devicename + itself. +
    • +
      + + +
    • devStateIcon
      + First form:
      +
        + Space separated list of regexp:icon-name:cmd triples, icon-name and cmd + may be empty.
        + If the state of the device matches regexp, then icon-name will be + displayed as the status icon in the room, and (if specified) clicking + on the icon executes cmd. If fhem cannot find icon-name, then the + status text will be displayed. + Example:
        +
          + attr lamp devStateIcon on:closed off:open
          + attr lamp devStateIcon on::A0 off::AI
          + attr lamp devStateIcon .*:noIcon
          +
        + Note: if the image is referencing an SVG icon, then you can use the + @colorname suffix to color the image. E.g.:
        +
          + attr Fax devStateIcon on:control_building_empty@red off:control_building_filled:278727 +
        + +
      + Second form:
      +
        + Perl regexp enclosed in {}. If the code returns undef, then the default + icon is used, if it retuns a string enclosed in <>, then it is + interpreted as an html string. Else the string is interpreted as a + devStateIcon of the first fom, see above. + Example:
        + {'<div style="width:32px;height:32px;background-color:green"></div>'} +
      +
    • +
      + + +
    • devStateStyle
      + Specify an HTML style for the given device, e.g.:
      +
        + attr sensor devStateStyle style="text-align:left;;font-weight:bold;;"
        +
      +
    • +
      + + +
    • webCmd
      + Colon separated list of commands to be shown in the room overview for a + certain device. Has no effect on smallscreen devices, see the + devStateIcon command for an alternative.
      + Example: +
        + attr lamp webCmd on:off:on-for-timer 10
        +
      +
      + + The first specified command is looked up in the "set device ?" list + (see the setList attribute for dummy devices). + If there it contains some known modifiers (colon, followed + by a comma separated list), then a different widget will be displayed: +
        +
      • if the modifier is ":noArg", then no further input field is + displayed
      • +
      • if the modifier is ":time", then a javascript driven timepicker is + displayed.
      • +
      • if the modifier is ":textField", an input field is displayed.
      • +
      • if the modifier is of the form + ":slider,<min>,<step>,<max>", then a javascript + driven slider is displayed
      • +
      • if the modifier is of the form ":multiple,val1,val2,...", then + multiple values can be selected, the result is comma separated.
      • +
      • else a dropdown with all the modifier values is displayed
      • +
      + If the command is state, then the value will be used as a command.
      + Examples for the modifier: +
        + define d1 dummy
        + attr d1 webCmd state
        + attr d1 setList state:on,off
        + define d2 dummy
        + attr d2 webCmd state
        + attr d2 setList state:slider,0,1,10
        + define d3 dummy
        + attr d3 webCmd state
        + attr d3 setList state:time
        +
      + Note: this is an attribute for the displayed device, not for the FHEMWEB + instance. +
    • +
      + + +
    • column
      + Allows to display more than one column per room overview, by specifying + the groups for the columns. Example:
      +
        + attr WEB column LivingRoom:FS20,notify|FHZ,notify DiningRoom:FS20|FHZ +
      + In this example in the LivingRoom the FS20 and the notify group is in + the first column, the FHZ and the notify in the second.
      + Note: some elements like SVG plots and readingsGroup can only be part of + a column if they are part of a group. +
    • +
      + +
    • enhancedEditor
      + Activates enhanced editor codemirror to edit files and DEFs. Example:
      +
      +
        attr WEB enhancedEditor <1|0>
      +
      + Set to 1 to activate, 0 to deactivate the enhanced editor.
      + Defaults to 0 if not set and internal editing will be used.
      +
    • +
      + +
    • enhancedEditorTheme
      + Select theme to be used in enhanced editor. Example:
      +
      +
        attr WEB enhancedEditorTheme blackboard
      +
      + Default theme is "blackboard".
      + Check directory ./www/codemirror/theme for available themes.
      +
    • +
    +
    + + Enhanced editor codemirror +
      +
      + Enhanced editor codemirror can be used for editing files and DEFs in fhem frontend.
      + Includes: syntax highlighting, bracket control, auto-complete and much more.
      + See codemirror website for more informations about editor's functionality and features.
      +
    +
+ +=end html + +=begin html_DE + + +

FHEMWEB

+
    + FHEMWEB ist das default WEB-Frontend, es implementiert auch einen einfachen + Webserver (optional mit Basic-Auth und HTTPS). +

    + + + Define +
      + define <name> FHEMWEB <tcp-portnr> [global] +

      + Aktiviert das Webfrontend auf dem Port <tcp-portnr>. Mit dem + Parameter global werden Anfragen von allen Netzwerkschnittstellen + akzeptiert (nicht nur vom localhost / 127.0.0.1) .
      + + Informationen für den Betrieb mit IPv6 finden Sie hier.
      +
    +
    + + + Set +
      +
    • rereadicons
      + Damit wird die Liste der Icons neu eingelesen, für den Fall, dass + Sie Icons löschen oder hinzufügen. +
    • +
    • clearSvgCache
      + Im Verzeichnis www/SVGcache werden SVG Daten zwischengespeichert, wenn + das Attribut SVGcache gesetzt ist. Mit diesem Befehl leeren Sie diesen + Zwischenspeicher. +
    • +
    +
    + + + Get +
      +
    • icon <logical icon>
      + Liefert den absoluten Pfad des (logischen) Icons zurück. Beispiel: +
        + get myFHEMWEB icon FS20.on
        + /data/Homeautomation/fhem/FHEM/FS20.on.png +
        +
    • +
    • pathlist
      + Zeigt diejenigen Verzeichnisse an, in welchen die verschiedenen Dateien + für FHEMWEB liegen. +
    • +

      + +
    + + + Attributes +
      + +
    • webname
      + Der Pfad nach http://hostname:port/ . Standard ist fhem, + so ist die Standard HTTP Adresse http://localhost:8083/fhem +

    • + + +
    • refresh
      + Damit erzeugen Sie auf den ausgegebenen Webseiten einen automatischen + Refresh, z.B. nach 5 Sekunden. +

    • + + +
    • plotmode
      + Spezifiziert, wie Plots erzeugt werden sollen: +
        +
      • SVG
        + Die Plots werden mit Hilfe des SVG Moduls als SVG + Grafik gerendert. Das ist die Standardeinstellung.
      • + +
      • gnuplot
        + Die Plots werden mit Hilfe des gnuplot Programmes erzeugt. Diese + Option ist aus historischen Gründen vorhanden. +
      • + +
      • gnuplot-scroll
        + Wie gnuplot, der einfache Zugriff auf historische Daten ist aber + genauso möglich wie mit dem SVG Modul.
      • + +
      +

    • + + +
    • plotsize
      + gibt die Standardbildgröße aller erzeugten Plots an als + Breite,Höhe an. Um einem individuellen Plot die Größe zu + ändern muss dieses Attribut bei der entsprechenden SVG Instanz + gesetzt werden. Default sind 800,160 für Desktop und 480,160 + für Smallscreen +

    • + + +
    • nrAxis
      + (bei mehrfach-Y-Achsen im SVG-Plot) Die Darstellung der Y Achsen + benötigt Platz. Hierdurch geben Sie an wie viele Achsen Sie + links,rechts [useLeft,useRight] benötigen. Default ist 1,1 (also 1 + Achse links, 1 Achse rechts). +

    • + + +
    • SVGcache
      + Plots die sich nicht mehr ändern, werden im SVGCache Verzeichnis + (www/SVGcache) gespeichert, um die erneute, rechenintensive + Berechnung der Grafiken zu vermeiden. Default ist 0, d.h. aus.
      + Siehe den clearSvgCache Befehl um diese Daten zu löschen. +

    • + + +
    • endPlotToday
      + Wird dieses FHEMWEB Attribut gesetzt, so enden Wochen- bzw. Monatsplots + am aktuellen Tag, sonst wird die aktuelle Woche/Monat angezeigt. +

    • + + +
    • endPlotNow
      + Wenn Sie dieses FHEMWEB Attribut auf 1 setzen, werden Tages und + Stunden-Plots zur aktuellen Zeit beendet. (Ähnlich wie + endPlotToday, nur eben minütlich). + Ansonsten wird der gesamte Tag oder eine 6 Stunden Periode (0, 6, 12, + 18 Stunde) gezeigt. Dieses Attribut wird nicht verwendet, wenn das SVG + Attribut startDate benutzt wird.
      +

    • + + +
    • ploteditor
      + Gibt an ob der Plot Editor in der SVG detail + ansicht angezeigt werden soll. Kann auf always, onClick oder never + gesetzt werden. Der Default ist always. +

    • + + +
    • plotfork
      + Normalerweise wird die Ploterstellung im Hauptprozess ausgeführt, + FHEM wird wärend dieser Zeit nicht auf andere Ereignisse + reagieren. Auf Rechnern mit sehr wenig Speicher (z.Bsp. FRITZ!Box 7170) + kann das zum automatischen Abschuss des FHEM Prozesses durch das OS + führen. +

    • + + +
    • basicAuth, basicAuthMsg
      + Fragt username/password zur Autentifizierung ab. Es gibt mehrere + Varianten: +
        +
      • falls das Argument nicht in {} eingeschlossen ist, dann wird + es als base64 kodiertes benutzername:passwort interpretiert. + Um sowas zu erzeugen kann man entweder einen der zahlreichen + Webdienste verwenden, oder das base64 Programm. Beispiel: +
          + $ echo -n fhemuser:secret | base64
          + ZmhlbXVzZXI6c2VjcmV0
          + fhem.cfg:
          + attr WEB basicAuth ZmhlbXVzZXI6c2VjcmV0 +
        +
      • +
      • Werden die Argumente in {} angegeben, wird es als perl-Ausdruck + ausgewertet, die Variablen $user and $password werden auf die + eingegebenen Werte gesetzt. Falls der Rückgabewert wahr ist, + wird die Anmeldung akzeptiert. + + Beispiel:
        + + attr WEB basicAuth { "$user:$password" eq "admin:secret" }
        +
        +
      • +
      +

    • + + +
    • HTTPS
      + Ermöglicht HTTPS Verbindungen. Es werden die Perl Module + IO::Socket::SSL benötigt, installierbar mit cpan -i + IO::Socket::SSL oder apt-get install libio-socket-ssl-perl; (OSX und + die FritzBox-7390 haben dieses Modul schon installiert.)
      + + Ein lokales Zertifikat muss im Verzeichis certs erzeugt werden. + Dieses Verzeichnis muss im modpath + angegeben werden, also auf der gleichen Ebene wie das FHEM Verzeichnis. + Beispiel: +
        + mkdir certs
        + cd certs
        + openssl req -new -x509 -nodes -out server-cert.pem -days 3650 -keyout server-key.pem +
      + +
      +
    • + + +
    • allowedCommands
      + Eine Komma getrennte Liste der erlaubten Befehle. Bei einer leeren + Liste (, dh. nur ein Komma) wird dieser FHEMWEB-Instanz "read-only". +
      Falls es auf get,set gesetzt ist, dann sind in dieser + FHEMWEB Instanz keine Konfigurationsänderungen möglich, nur + "normale" Bedienung der Schalter/etc.
      + + Dieses Attribut sollte zusammen mit dem hiddenroom/hiddengroup + Attributen verwendet werden.
      + + Achtung: allowedCommands sollte wie hier beschrieben + funktionieren, allerdings können wir keine Garantie geben, + daß man sie nicht überlisten, und Schaden anrichten kann. +

    • + +
    • allowfrom +

    • + + +
    • stylesheetPrefix
      + Präfix für die Dateien style.css, svg_style.css und + svg_defs.svg. Wenn die Datei mit dem Präfix fehlt, wird die Default + Datei (ohne Präfix) verwendet. Diese Dateien müssen im FHEM + Ordner liegen und können direkt mit "Select style" im FHEMWEB + Menüeintrag ausgewählt werden. Beispiel: +
        + attr WEB stylesheetPrefix dark
        +
        + Referenzdateien:
        +
          + darksvg_defs.svg
          + darksvg_style.css
          + darkstyle.css
          +
        +
        +
      + Anmerkung:Wenn der Parametername smallscreen oder touchpad + enthält, wird FHEMWEB das Layout/den Zugriff für entsprechende + Geräte (Smartphones oder Touchpads) optimieren
      + + Standardmäßig werden 3 FHEMWEB Instanzen aktiviert: Port 8083 + für Desktop Browser, Port 8084 für Smallscreen, und 8085 + für Touchpad.
      + + Wenn touchpad oder smallscreen benutzt werden, wird WebApp support + aktiviert: Nachdem Sie eine Seite am iPhone oder iPad mit Safari + angesehen haben, können Sie einen Link auf den Homescreen anlegen um + die Seite im Fullscreen Modus zu sehen. Links werden in diesem Modus + anders gerendert, um ein "Zurückfallen" in den "normalen" Browser zu + verhindern. +

    • + + +
    • iconPath
      + Durch Doppelpunkt getrennte Aufzählung der Verzeichnisse, in + welchen nach Icons gesucht wird. Die Verzeichnisse müssen unter + fhem/www/images angelegt sein. Standardeinstellung ist: + $styleSheetPrefix:default:fhemSVG:openautomation
      + Setzen Sie den Wert auf fhemSVG:openautomation um nur SVG Bilder zu + benutzen. +

    • + + +
    • hiddenroom
      + Eine Komma getrennte Liste, um Räume zu verstecken, d.h. nicht + anzuzeigen. Besondere Werte sind input, detail und save. In diesem + Fall werden diverse Eingabefelder ausgeblendent. Durch direktes Aufrufen + der URL sind diese Räume weiterhin erreichbar!
      + Ebenso können Einträge in den Logfile/Commandref/etc Block + versteckt werden.

    • + + +
    • hiddengroup
      + Wie hiddenroom (siehe oben), jedoch auf Gerätegruppen bezogen. +
      + Beispiel: attr WEBtablet hiddengroup FileLog,dummy,at,notify +

    • + + +
    • menuEntries
      + Komma getrennte Liste; diese Links werden im linken Menü angezeigt. + Beispiel:
      + attr WEB menuEntries fhem.de,http://fhem.de,culfw.de,http://culfw.de
      + attr WEB menuEntries AlarmOn,http://fhemhost:8083/fhem?cmd=set%20alarm%20on
      +

    • + + +
    • longpoll
      + Dies betrifft die Aktualisierung der Gerätestati in der + Weboberfläche. Ist longpoll aktiviert, werden + Statusänderungen sofort im Browser dargestellt. ohne die Seite + manuell neu laden zu müssen. Standard ist aktiviert. +

    • + + + +
    • longpollSVG
      + Lädt SVG Instanzen erneut, falls ein Ereignis dessen Inhalt + ändert. Funktioniert nur, falls der dazugehörige #FileLog + Definition in der .gplot Datei folgenden Form hat: deviceName.Event + bzw. deviceName.*. Wenn man den Plot + Editor benutzt, ist das übrigens immer der Fall. Die SVG Datei + wird bei jedem auslösenden Event dieses Gerätes neu + geladen. Standard ist aus. +

    • + + +
    • redirectCmds
      + Damit wird das URL Eingabefeld des Browser nach einem Befehl geleert. + Standard ist eingeschaltet (1), ausschalten kann man es durch + setzen des Attributs auf 0, z.Bsp. um den Syntax der Kommunikation mit + FHEMWEB zu untersuchen. +

    • + + +
    • fwcompress
      + Aktiviert die HTML Datenkompression (Standard ist 1, also ja, 0 stellt + die Kompression aus). +

    • + + +
    • reverseLogs
      + Damit wird das Logfile umsortiert, die neuesten Einträge stehen + oben. Der Vorteil ist, dass man nicht runterscrollen muss um den + neuesten Eintrag zu sehen, der Nachteil dass FHEM damit deutlich mehr + Hauptspeicher benötigt, etwa 6 mal so viel, wie das Logfile auf + dem Datenträger groß ist. Das kann auf Systemen mit wenig + Speicher (FRITZ!Box) zum Terminieren des FHEM Prozesses durch das + Betriebssystem führen. +

    • + + +
    • CORS
      + Wenn auf 1 gestellt, wird FHEMWEB einen "Cross origin resource sharing" + Header bereitstellen, näheres siehe Wikipedia. +

    • + + +
    • icon
      + Damit definiert man ein Icon für die einzelnen Geräte in der + Raumübersicht. Es gibt einen passenden Link in der Detailansicht + um das zu vereinfachen. Um ein Bild für die Räume selbst zu + definieren muss ein Icon mit dem Namen ico<Raumname>.png im + iconPath existieren (oder man verwendet roomIcons, s.u.) +

    • + + +
    • roomIcons
      + Leerzeichen getrennte Liste von room:icon Zuordnungen + Der erste Teil wird als regexp interpretiert, daher muss ein + Leerzeichen als Punkt geschrieben werden. Beispiel:
      + attr WEB roomIcons Anlagen.EDV:icoEverything +

    • + + +
    • sortRooms
      + Durch Leerzeichen getrennte Liste von Räumen, um deren Reihenfolge + zu definieren. Beispiel:
      + attr WEB sortRooms DG OG EG Keller +

    • + + +
    • defaultRoom
      + Zeigt den angegebenen Raum an falls kein Raum explizit ausgewählt + wurde. Achtung: falls gesetzt, wird motd nicht mehr angezeigt. + Beispiel:
      + attr WEB defaultRoom Zentrale +

    • + + +
    • sortby
      + Der Wert dieses Attributs wird zum sortieren von Geräten in + Räumen verwendet, sonst wäre es der Alias oder, wenn keiner + da ist, der Gerätename selbst. +

    • + + +
    • devStateIcon
      + Erste Variante:
      +
        + Leerzeichen getrennte Auflistung von regexp:icon-name:cmd + Dreierpärchen, icon-name und cmd dürfen leer sein.
        + + Wenn der Zustand des Gerätes mit der regexp übereinstimmt, + wird als icon-name das entsprechende Status Icon angezeigt, und (falls + definiert), löst ein Klick auf das Icon das entsprechende cmd aus. + Wenn fhem icon-name nicht finden kann, wird der Status als Text + angezeigt. + Beispiel:
        +
          + attr lamp devStateIcon on:closed off:open
          + attr lamp devStateIcon on::A0 off::AI
          + attr lamp devStateIcon .*:noIcon
          +
        + Anmerkung: Wenn das Icon ein SVG Bild ist, kann das @colorname Suffix + verwendet werden um das Icon einzufärben. Z.B.:
        +
          + attr Fax devStateIcon on:control_building_empty@red off:control_building_filled:278727 +
        + +
      + Zweite Variante:
      +
        + Perl regexp eingeschlossen in {}. Wenn der Code undef + zurückliefert, wird das Standard Icon verwendet; wird ein String + in <> zurück geliefert, wird dieser als HTML String interpretiert. + Andernfalls wird der String als devStateIcon gemäß der + ersten Variante interpretiert, siehe oben. Beispiel:
        + + {'<div style="width:32px;height:32px;background-color:green"></div>'} +
      +

    • + + +
    • devStateStyle
      + Für ein best. Gerät einen best. HTML-Style benutzen. + Beispiel:
      +
        + attr sensor devStateStyle style="text-align:left;;font-weight:bold;;"
        +
      +

    • + + +
    • webCmd
      + Durch Doppelpunkte getrennte Auflistung von Befehlen, die für ein + bestimmtes Gerät gelten sollen. Funktioniert nicht mit + smallscreen, ein Ersatz dafür ist der devStateIcon Befehl.
      + Beispiel: +
        + attr lamp webCmd on:off:on-for-timer 10
        +
      +
      + + Der erste angegebene Befehl wird in der "set device ?" list + nachgeschlagen (Siehe das setList Attrib + für Dummy Geräte). Wenn dort bekannte Modifier sind, + wird ein anderes Widget angezeigt: +
        +
      • Ist der Modifier ":noArg", wird kein weiteres Eingabefeld + angezeigt.
      • + +
      • Ist der Modifier ":time", wird ein in Javaskript geschreibenes + Zeitauswahlmenü angezeigt.
      • + +
      • Ist der Modifier ":textField", wird ein Eingabefeld + angezeigt.
      • + +
      • Ist der Modifier in der Form + ":slider,<min>,<step>,<max>", so wird ein in + JavaScript programmierter Slider angezeigt
      • + +
      • Ist der Modifier ":multiple,val1,val2,...", dann ein + Mehrfachauswahl ist möglich, das Ergebnis ist Komma-separiert.
      • + +
      • In allen anderen Fällen erscheint ein Dropdown mit allen + Modifier Werten.
      • +
      + + Wenn der Befehl state ist, wird der Wert als Kommando interpretiert.
      + Beispiele für modifier: +
        + define d1 dummy
        + attr d1 webCmd state
        + attr d1 setList state:on,off
        + define d2 dummy
        + attr d2 webCmd state
        + attr d2 setList state:slider,0,1,10
        + define d3 dummy
        + attr d3 webCmd state
        + attr d3 setList state:time
        +
      + + Anmerkung: dies ist ein Attribut für das anzuzeigende Gerät, + nicht für die FHEMWEBInstanz. +

    • + + +
    • column
      + Damit werden mehrere Spalten für einen Raum angezeigt, indem + sie verschiedene Gruppen Spalten zuordnen. Beispiel:
      +
        + attr WEB column LivingRoom:FS20,notify|FHZ,notify DiningRoom:FS20|FHZ +
      + + In diesem Beispiel werden im Raum LivingRoom die FS20 sowie die notify + Gruppe in der ersten Spalte, die FHZ und das notify in der zweiten + Spalte angezeigt.
      + + Anmerkung: einige Elemente, wie SVG Plots und readingsGroup können + nur Teil einer Spalte sein wenn sie in group + stehen. +

    • + +
    +
+ +=end html_DE + +=cut diff --git a/fhem/www/codemirror/AUTHORS b/fhem/www/codemirror/AUTHORS new file mode 100644 index 000000000..737acedab --- /dev/null +++ b/fhem/www/codemirror/AUTHORS @@ -0,0 +1,300 @@ +List of CodeMirror contributors. Updated before every release. + +4r2r +Aaron Brooks +Abe Fettig +Adam King +adanlobato +Adán Lobato +aeroson +Ahmad Amireh +Ahmad M. Zawawi +ahoward +Akeksandr Motsjonov +Alberto Pose +Albert Xing +Alexander Pavlov +Alexander Schepanovski +Alexander Solovyov +alexey-k +Alex Piggott +Amy +Ananya Sen +AndersMad +Anderson Mesquita +Andre von Houck +Andrey Lushnikov +Andy Joslin +Andy Kimball +Andy Li +angelozerr +angelo.zerr@gmail.com +Ankit Ahuja +Ansel Santosa +Anthony Grimes +Anton Kovalyov +areos +AtomicPages LLC +Atul Bhouraskar +Aurelian Oancea +Bastian Müller +benbro +Beni Cherniavsky-Paskin +Benjamin DeCoste +Ben Keen +boomyjee +borawjm +Brandon Frohs +Brandon Wamboldt +Brett Zamir +Brian Sletten +Bruce Mitchener +Chandra Sekhar Pydi +Charles Skelton +Chris Coyier +Chris Granger +Chris Morgan +Christopher Brown +ciaranj +CodeAnimal +ComFreek +dagsta +Dan Heberden +Daniel, Dao Quang Minh +Daniel Faust +Daniel Huigens +Daniel KJ +Daniel Neel +Daniel Parnell +Danny Yoo +Darius Roberts +David Mignot +David Pathakjee +deebugger +Deep Thought +Dominator008 +Domizio Demichelis +Drew Bratcher +Drew Hintz +Drew Khoury +Dror BG +duralog +eborden +edsharp +ekhaled +Enam Mijbah Noor +Eric Allam +eustas +Fabio Zendhi Nagao +Fauntleroy +fbuchinger +feizhang365 +Felipe Lalanne +Felix Raab +Filip Noetzel +flack +ForbesLindesay +Forbes Lindesay +Ford_Lawnmower +Gabriel Nahmias +galambalazs +Gautam Mehta +Glenn Jorde +Glenn Ruehle +Golevka +Gordon Smith +greengiant +Guillaume Massé +Guillaume Massé +Hans Engel +Hardest +Hasan Karahan +hitsthings +Hocdoc +Ian Beck +Ian Dickinson +Ian Wehrman +Ian Wetherbee +Ice White +ICHIKAWA, Yuji +ilvalle +Ingo Richter +Irakli Gozalishvili +Ivan Kurnosov +Jacob Lee +Jakub Vrana +James Campos +James Thorne +Jamie Hill +Jan Jongboom +jankeromnes +Jan Keromnes +Jan Odvarko +Jan T. Sott +Jason +Jason Grout +Jason Johnston +Jason San Jose +Jason Siefken +Jean Boussier +jeffkenton +Jeff Pickhardt +jem (graphite) +Jochen Berger +Johan Ask +John Connor +John Lees-Miller +John Snelson +John Van Der Loo +jongalloway +Jon Malmaud +Joost-Wim Boekesteijn +Joseph Pecoraro +Joshua Newman +jots +jsoojeon +Juan Benavides Romero +Jucovschi Constantin +Juho Vuori +jwallers@gmail.com +kaniga +Ken Newman +Ken Rockot +Kevin Sawicki +Klaus Silveira +Koh Zi Han, Cliff +komakino +Konstantin Lopuhin +koops +ks-ifware +kubelsmieci +Lanny +Laszlo Vidacs +leaf corcoran +Leonya Khachaturov +Liam Newman +LM +Lorenzo Stoakes +Luciano Longo +lynschinzer +Maksim Lin +Maksym Taran +Marat Dreizin +Marco Aurélio +Marijn Haverbeke +Mario Pietsch +Mark Lentczner +Marko Bonaci +Martin Balek +Martín Gaitán +Martin Hasoň +Mason Malone +Mateusz Paprocki +mats cronqvist +Matthew Beale +Matthias BUSSONNIER +Matt McDonald +Matt Pass +Matt Sacks +Maximilian Hils +Maxim Kraev +Max Kirsch +mbarkhau +Metatheos +Micah Dubinko +Michael Lehenbauer +Michael Zhou +Mighty Guava +Miguel Castillo +Mike +Mike Brevoort +Mike Diaz +Mike Ivanov +Mike Kadin +MinRK +Miraculix87 +misfo +mloginov +mps +Narciso Jaramillo +Nathan Williams +nerbert +nguillaumin +Niels van Groningen +Nikita Beloglazov +Nikita Vasilyev +Nikolay Kostov +nlwillia +pablo +Page +Patrick Strawderman +Paul Garvin +Paul Ivanov +Pavel Feldman +Pavel Strashkin +Paweł Bartkiewicz +peteguhl +peterkroon +Peter Kroon +prasanthj +Prasanth J +Rahul +Randy Edmunds +Richard Z.H. Wang +robertop23 +Robert Plummer +Ruslan Osmanov +sabaca +Samuel Ainsworth +sandeepshetty +santec +Sascha Peilicke +satchmorun +sathyamoorthi +SCLINIC\jdecker +Sebastian Zaha +shaund +shaun gilchrist +Shawn A +Shiv Deepak +Shmuel Englard +soliton4 +sonson +spastorelli +Stanislav Oaserele +Stas Kobzar +Stefan Borsje +Steffen Beyer +Steve O'Hara +stoskov +Taha Jahangir +Tarmil +tfjgeorge +Thaddee Tyl +think +Thomas Dvornik +Thomas Schmid +Tim Baumann +Timothy Farrell +Timothy Hatcher +TobiasBg +Tomas-A +Tomas Varaneckas +Tom Erik Støwer +Tom MacWright +Tony Jian +Travis Heppe +Vestimir Markov +vf +Volker Mische +wenli +Wesley Wiser +William Jamieson +Wojtek Ptak +Xavier Mendez +YNH Webdev +Yunchi Luo +Yuvi Panda +Zachary Dremann +zziuni +魏鹏刚 diff --git a/fhem/www/codemirror/LICENSE b/fhem/www/codemirror/LICENSE new file mode 100644 index 000000000..442d11cdc --- /dev/null +++ b/fhem/www/codemirror/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2013 by Marijn Haverbeke and others + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/fhem/www/codemirror/README.md b/fhem/www/codemirror/README.md new file mode 100644 index 000000000..61f6b6452 --- /dev/null +++ b/fhem/www/codemirror/README.md @@ -0,0 +1,11 @@ +# CodeMirror +[![Build Status](https://secure.travis-ci.org/marijnh/CodeMirror.png?branch=master)](http://travis-ci.org/marijnh/CodeMirror) +[![NPM version](https://badge.fury.io/js/codemirror.png)](http://badge.fury.io/js/codemirror) + +CodeMirror is a JavaScript component that provides a code editor in +the browser. When a mode is available for the language you are coding +in, it will color your code, and optionally help with indentation. + +The project page is http://codemirror.net +The manual is at http://codemirror.net/doc/manual.html +The contributing guidelines are in [CONTRIBUTING.md](https://github.com/marijnh/CodeMirror/blob/master/CONTRIBUTING.md) diff --git a/fhem/www/codemirror/addon/edit/closebrackets.js b/fhem/www/codemirror/addon/edit/closebrackets.js new file mode 100644 index 000000000..0575222be --- /dev/null +++ b/fhem/www/codemirror/addon/edit/closebrackets.js @@ -0,0 +1,84 @@ +(function() { + var DEFAULT_BRACKETS = "()[]{}''\"\""; + var DEFAULT_EXPLODE_ON_ENTER = "[]{}"; + var SPACE_CHAR_REGEX = /\s/; + + CodeMirror.defineOption("autoCloseBrackets", false, function(cm, val, old) { + if (old != CodeMirror.Init && old) + cm.removeKeyMap("autoCloseBrackets"); + if (!val) return; + var pairs = DEFAULT_BRACKETS, explode = DEFAULT_EXPLODE_ON_ENTER; + if (typeof val == "string") pairs = val; + else if (typeof val == "object") { + if (val.pairs != null) pairs = val.pairs; + if (val.explode != null) explode = val.explode; + } + var map = buildKeymap(pairs); + if (explode) map.Enter = buildExplodeHandler(explode); + cm.addKeyMap(map); + }); + + function charsAround(cm, pos) { + var str = cm.getRange(CodeMirror.Pos(pos.line, pos.ch - 1), + CodeMirror.Pos(pos.line, pos.ch + 1)); + return str.length == 2 ? str : null; + } + + function buildKeymap(pairs) { + var map = { + name : "autoCloseBrackets", + Backspace: function(cm) { + if (cm.somethingSelected() || cm.getOption("disableInput")) return CodeMirror.Pass; + var cur = cm.getCursor(), around = charsAround(cm, cur); + if (around && pairs.indexOf(around) % 2 == 0) + cm.replaceRange("", CodeMirror.Pos(cur.line, cur.ch - 1), CodeMirror.Pos(cur.line, cur.ch + 1)); + else + return CodeMirror.Pass; + } + }; + var closingBrackets = ""; + for (var i = 0; i < pairs.length; i += 2) (function(left, right) { + if (left != right) closingBrackets += right; + function surround(cm) { + var selection = cm.getSelection(); + cm.replaceSelection(left + selection + right); + } + function maybeOverwrite(cm) { + var cur = cm.getCursor(), ahead = cm.getRange(cur, CodeMirror.Pos(cur.line, cur.ch + 1)); + if (ahead != right || cm.somethingSelected()) return CodeMirror.Pass; + else cm.execCommand("goCharRight"); + } + map["'" + left + "'"] = function(cm) { + if (left == "'" && cm.getTokenAt(cm.getCursor()).type == "comment" || + cm.getOption("disableInput")) + return CodeMirror.Pass; + if (cm.somethingSelected()) return surround(cm); + if (left == right && maybeOverwrite(cm) != CodeMirror.Pass) return; + var cur = cm.getCursor(), ahead = CodeMirror.Pos(cur.line, cur.ch + 1); + var line = cm.getLine(cur.line), nextChar = line.charAt(cur.ch), curChar = cur.ch > 0 ? line.charAt(cur.ch - 1) : ""; + if (left == right && CodeMirror.isWordChar(curChar)) + return CodeMirror.Pass; + if (line.length == cur.ch || closingBrackets.indexOf(nextChar) >= 0 || SPACE_CHAR_REGEX.test(nextChar)) + cm.replaceSelection(left + right, {head: ahead, anchor: ahead}); + else + return CodeMirror.Pass; + }; + if (left != right) map["'" + right + "'"] = maybeOverwrite; + })(pairs.charAt(i), pairs.charAt(i + 1)); + return map; + } + + function buildExplodeHandler(pairs) { + return function(cm) { + var cur = cm.getCursor(), around = charsAround(cm, cur); + if (!around || pairs.indexOf(around) % 2 != 0 || cm.getOption("disableInput")) + return CodeMirror.Pass; + cm.operation(function() { + var newPos = CodeMirror.Pos(cur.line + 1, 0); + cm.replaceSelection("\n\n", {anchor: newPos, head: newPos}, "+input"); + cm.indentLine(cur.line + 1, null, true); + cm.indentLine(cur.line + 2, null, true); + }); + }; + } +})(); diff --git a/fhem/www/codemirror/addon/edit/closetag.js b/fhem/www/codemirror/addon/edit/closetag.js new file mode 100644 index 000000000..cad776a78 --- /dev/null +++ b/fhem/www/codemirror/addon/edit/closetag.js @@ -0,0 +1,93 @@ +/** + * Tag-closer extension for CodeMirror. + * + * This extension adds an "autoCloseTags" option that can be set to + * either true to get the default behavior, or an object to further + * configure its behavior. + * + * These are supported options: + * + * `whenClosing` (default true) + * Whether to autoclose when the '/' of a closing tag is typed. + * `whenOpening` (default true) + * Whether to autoclose the tag when the final '>' of an opening + * tag is typed. + * `dontCloseTags` (default is empty tags for HTML, none for XML) + * An array of tag names that should not be autoclosed. + * `indentTags` (default is block tags for HTML, none for XML) + * An array of tag names that should, when opened, cause a + * blank line to be added inside the tag, and the blank line and + * closing line to be indented. + * + * See demos/closetag.html for a usage example. + */ + +(function() { + CodeMirror.defineOption("autoCloseTags", false, function(cm, val, old) { + if (old != CodeMirror.Init && old) + cm.removeKeyMap("autoCloseTags"); + if (!val) return; + var map = {name: "autoCloseTags"}; + if (typeof val != "object" || val.whenClosing) + map["'/'"] = function(cm) { return autoCloseSlash(cm); }; + if (typeof val != "object" || val.whenOpening) + map["'>'"] = function(cm) { return autoCloseGT(cm); }; + cm.addKeyMap(map); + }); + + var htmlDontClose = ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", + "source", "track", "wbr"]; + var htmlIndent = ["applet", "blockquote", "body", "button", "div", "dl", "fieldset", "form", "frameset", "h1", "h2", "h3", "h4", + "h5", "h6", "head", "html", "iframe", "layer", "legend", "object", "ol", "p", "select", "table", "ul"]; + + function autoCloseGT(cm) { + var pos = cm.getCursor(), tok = cm.getTokenAt(pos); + var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; + if (inner.mode.name != "xml" || !state.tagName || cm.getOption("disableInput")) return CodeMirror.Pass; + + var opt = cm.getOption("autoCloseTags"), html = inner.mode.configuration == "html"; + var dontCloseTags = (typeof opt == "object" && opt.dontCloseTags) || (html && htmlDontClose); + var indentTags = (typeof opt == "object" && opt.indentTags) || (html && htmlIndent); + + var tagName = state.tagName; + if (tok.end > pos.ch) tagName = tagName.slice(0, tagName.length - tok.end + pos.ch); + var lowerTagName = tagName.toLowerCase(); + // Don't process the '>' at the end of an end-tag or self-closing tag + if (!tagName || + tok.type == "string" && (tok.end != pos.ch || !/[\"\']/.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1) || + tok.type == "tag" && state.type == "closeTag" || + tok.string.indexOf("/") == (tok.string.length - 1) || // match something like + dontCloseTags && indexOf(dontCloseTags, lowerTagName) > -1 || + CodeMirror.scanForClosingTag && CodeMirror.scanForClosingTag(cm, pos, tagName, + Math.min(cm.lastLine() + 1, pos.line + 50))) + return CodeMirror.Pass; + + var doIndent = indentTags && indexOf(indentTags, lowerTagName) > -1; + var curPos = doIndent ? CodeMirror.Pos(pos.line + 1, 0) : CodeMirror.Pos(pos.line, pos.ch + 1); + cm.replaceSelection(">" + (doIndent ? "\n\n" : "") + "", + {head: curPos, anchor: curPos}); + if (doIndent) { + cm.indentLine(pos.line + 1, null, true); + cm.indentLine(pos.line + 2, null); + } + } + + function autoCloseSlash(cm) { + var pos = cm.getCursor(), tok = cm.getTokenAt(pos); + var inner = CodeMirror.innerMode(cm.getMode(), tok.state), state = inner.state; + if (tok.type == "string" || tok.string.charAt(0) != "<" || + tok.start != pos.ch - 1 || inner.mode.name != "xml" || + cm.getOption("disableInput")) + return CodeMirror.Pass; + + var tagName = state.context && state.context.tagName; + if (tagName) cm.replaceSelection("/" + tagName + ">", "end"); + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } +})(); diff --git a/fhem/www/codemirror/addon/edit/continuelist.js b/fhem/www/codemirror/addon/edit/continuelist.js new file mode 100644 index 000000000..190b41dcb --- /dev/null +++ b/fhem/www/codemirror/addon/edit/continuelist.js @@ -0,0 +1,27 @@ +(function() { + 'use strict'; + + var listRE = /^(\s*)([*+-]|(\d+)\.)(\s*)/, + unorderedBullets = '*+-'; + + CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) { + if (cm.getOption("disableInput")) return CodeMirror.Pass; + + var pos = cm.getCursor(), + inList = cm.getStateAfter(pos.line).list !== false, + match; + + if (!inList || !(match = cm.getLine(pos.line).match(listRE))) { + cm.execCommand('newlineAndIndent'); + return; + } + + var indent = match[1], after = match[4]; + var bullet = unorderedBullets.indexOf(match[2]) >= 0 + ? match[2] + : (parseInt(match[3], 10) + 1) + '.'; + + cm.replaceSelection('\n' + indent + bullet + after, 'end'); + }; + +}()); diff --git a/fhem/www/codemirror/addon/edit/matchbrackets.js b/fhem/www/codemirror/addon/edit/matchbrackets.js new file mode 100644 index 000000000..465b6ccaa --- /dev/null +++ b/fhem/www/codemirror/addon/edit/matchbrackets.js @@ -0,0 +1,87 @@ +(function() { + var ie_lt8 = /MSIE \d/.test(navigator.userAgent) && + (document.documentMode == null || document.documentMode < 8); + + var Pos = CodeMirror.Pos; + + var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"}; + function findMatchingBracket(cm, where, strict) { + var state = cm.state.matchBrackets; + var maxScanLen = (state && state.maxScanLineLength) || 10000; + var maxScanLines = (state && state.maxScanLines) || 100; + + var cur = where || cm.getCursor(), line = cm.getLineHandle(cur.line), pos = cur.ch - 1; + var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)]; + if (!match) return null; + var forward = match.charAt(1) == ">", d = forward ? 1 : -1; + if (strict && forward != (pos == cur.ch)) return null; + var style = cm.getTokenTypeAt(Pos(cur.line, pos + 1)); + + var stack = [line.text.charAt(pos)], re = /[(){}[\]]/; + function scan(line, lineNo, start) { + if (!line.text) return; + var pos = forward ? 0 : line.text.length - 1, end = forward ? line.text.length : -1; + if (line.text.length > maxScanLen) return null; + if (start != null) pos = start + d; + for (; pos != end; pos += d) { + var ch = line.text.charAt(pos); + if (re.test(ch) && cm.getTokenTypeAt(Pos(lineNo, pos + 1)) == style) { + var match = matching[ch]; + if (match.charAt(1) == ">" == forward) stack.push(ch); + else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false}; + else if (!stack.length) return {pos: pos, match: true}; + } + } + } + for (var i = cur.line, found, e = forward ? Math.min(i + maxScanLines, cm.lineCount()) : Math.max(-1, i - maxScanLines); i != e; i+=d) { + if (i == cur.line) found = scan(line, i, pos); + else found = scan(cm.getLineHandle(i), i); + if (found) break; + } + return {from: Pos(cur.line, pos), to: found && Pos(i, found.pos), + match: found && found.match, forward: forward}; + } + + function matchBrackets(cm, autoclear) { + // Disable brace matching in long lines, since it'll cause hugely slow updates + var maxHighlightLen = cm.state.matchBrackets.maxHighlightLineLength || 1000; + var found = findMatchingBracket(cm); + if (!found || cm.getLine(found.from.line).length > maxHighlightLen || + found.to && cm.getLine(found.to.line).length > maxHighlightLen) + return; + + var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket"; + var one = cm.markText(found.from, Pos(found.from.line, found.from.ch + 1), {className: style}); + var two = found.to && cm.markText(found.to, Pos(found.to.line, found.to.ch + 1), {className: style}); + // Kludge to work around the IE bug from issue #1193, where text + // input stops going to the textarea whenever this fires. + if (ie_lt8 && cm.state.focused) cm.display.input.focus(); + var clear = function() { + cm.operation(function() { one.clear(); two && two.clear(); }); + }; + if (autoclear) setTimeout(clear, 800); + else return clear; + } + + var currentlyHighlighted = null; + function doMatchBrackets(cm) { + cm.operation(function() { + if (currentlyHighlighted) {currentlyHighlighted(); currentlyHighlighted = null;} + if (!cm.somethingSelected()) currentlyHighlighted = matchBrackets(cm, false); + }); + } + + CodeMirror.defineOption("matchBrackets", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) + cm.off("cursorActivity", doMatchBrackets); + if (val) { + cm.state.matchBrackets = typeof val == "object" ? val : {}; + cm.on("cursorActivity", doMatchBrackets); + } + }); + + CodeMirror.defineExtension("matchBrackets", function() {matchBrackets(this, true);}); + CodeMirror.defineExtension("findMatchingBracket", function(pos, strict){ + return findMatchingBracket(this, pos, strict); + }); +})(); diff --git a/fhem/www/codemirror/addon/edit/matchtags.js b/fhem/www/codemirror/addon/edit/matchtags.js new file mode 100644 index 000000000..f189c1f8e --- /dev/null +++ b/fhem/www/codemirror/addon/edit/matchtags.js @@ -0,0 +1,56 @@ +(function() { + "use strict"; + + CodeMirror.defineOption("matchTags", false, function(cm, val, old) { + if (old && old != CodeMirror.Init) { + cm.off("cursorActivity", doMatchTags); + cm.off("viewportChange", maybeUpdateMatch); + clear(cm); + } + if (val) { + cm.state.matchBothTags = typeof val == "object" && val.bothTags; + cm.on("cursorActivity", doMatchTags); + cm.on("viewportChange", maybeUpdateMatch); + doMatchTags(cm); + } + }); + + function clear(cm) { + if (cm.state.tagHit) cm.state.tagHit.clear(); + if (cm.state.tagOther) cm.state.tagOther.clear(); + cm.state.tagHit = cm.state.tagOther = null; + } + + function doMatchTags(cm) { + cm.state.failedTagMatch = false; + cm.operation(function() { + clear(cm); + if (cm.somethingSelected()) return; + var cur = cm.getCursor(), range = cm.getViewport(); + range.from = Math.min(range.from, cur.line); range.to = Math.max(cur.line + 1, range.to); + var match = CodeMirror.findMatchingTag(cm, cur, range); + if (!match) return; + if (cm.state.matchBothTags) { + var hit = match.at == "open" ? match.open : match.close; + if (hit) cm.state.tagHit = cm.markText(hit.from, hit.to, {className: "CodeMirror-matchingtag"}); + } + var other = match.at == "close" ? match.open : match.close; + if (other) + cm.state.tagOther = cm.markText(other.from, other.to, {className: "CodeMirror-matchingtag"}); + else + cm.state.failedTagMatch = true; + }); + } + + function maybeUpdateMatch(cm) { + if (cm.state.failedTagMatch) doMatchTags(cm); + } + + CodeMirror.commands.toMatchingTag = function(cm) { + var found = CodeMirror.findMatchingTag(cm, cm.getCursor()); + if (found) { + var other = found.at == "close" ? found.open : found.close; + if (other) cm.setSelection(other.to, other.from); + } + }; +})(); diff --git a/fhem/www/codemirror/addon/edit/trailingspace.js b/fhem/www/codemirror/addon/edit/trailingspace.js new file mode 100644 index 000000000..f6bb02645 --- /dev/null +++ b/fhem/www/codemirror/addon/edit/trailingspace.js @@ -0,0 +1,15 @@ +CodeMirror.defineOption("showTrailingSpace", false, function(cm, val, prev) { + if (prev == CodeMirror.Init) prev = false; + if (prev && !val) + cm.removeOverlay("trailingspace"); + else if (!prev && val) + cm.addOverlay({ + token: function(stream) { + for (var l = stream.string.length, i = l; i && /\s/.test(stream.string.charAt(i - 1)); --i) {} + if (i > stream.pos) { stream.pos = i; return null; } + stream.pos = l; + return "trailingspace"; + }, + name: "trailingspace" + }); +}); diff --git a/fhem/www/codemirror/addon/hint/anyword-hint.js b/fhem/www/codemirror/addon/hint/anyword-hint.js new file mode 100644 index 000000000..a144768c8 --- /dev/null +++ b/fhem/www/codemirror/addon/hint/anyword-hint.js @@ -0,0 +1,32 @@ +(function() { + "use strict"; + + var WORD = /[\w$]+/, RANGE = 500; + + CodeMirror.registerHelper("hint", "anyword", function(editor, options) { + var word = options && options.word || WORD; + var range = options && options.range || RANGE; + var cur = editor.getCursor(), curLine = editor.getLine(cur.line); + var start = cur.ch, end = start; + while (end < curLine.length && word.test(curLine.charAt(end))) ++end; + while (start && word.test(curLine.charAt(start - 1))) --start; + var curWord = start != end && curLine.slice(start, end); + + var list = [], seen = {}; + var re = new RegExp(word.source, "g"); + for (var dir = -1; dir <= 1; dir += 2) { + var line = cur.line, end = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir; + for (; line != end; line += dir) { + var text = editor.getLine(line), m; + while (m = re.exec(text)) { + if (line == cur.line && m[0] === curWord) continue; + if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) { + seen[m[0]] = true; + list.push(m[0]); + } + } + } + } + return {list: list, from: CodeMirror.Pos(cur.line, start), to: CodeMirror.Pos(cur.line, end)}; + }); +})(); diff --git a/fhem/www/codemirror/addon/hint/css-hint.js b/fhem/www/codemirror/addon/hint/css-hint.js new file mode 100644 index 000000000..6789c458b --- /dev/null +++ b/fhem/www/codemirror/addon/hint/css-hint.js @@ -0,0 +1,46 @@ +(function () { + "use strict"; + + var pseudoClasses = {link: 1, visited: 1, active: 1, hover: 1, focus: 1, + "first-letter": 1, "first-line": 1, "first-child": 1, + before: 1, after: 1, lang: 1}; + + CodeMirror.registerHelper("hint", "css", function(cm) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + var inner = CodeMirror.innerMode(cm.getMode(), token.state); + if (inner.mode.name != "css") return; + + var word = token.string, start = token.start, end = token.end; + if (/[^\w$_-]/.test(word)) { + word = ""; start = end = cur.ch; + } + + var spec = CodeMirror.resolveMode("text/css"); + + var result = []; + function add(keywords) { + for (var name in keywords) + if (!word || name.lastIndexOf(word, 0) == 0) + result.push(name); + } + + var st = token.state.state; + if (st == "pseudo" || token.type == "variable-3") { + add(pseudoClasses); + } else if (st == "block" || st == "maybeprop") { + add(spec.propertyKeywords); + } else if (st == "prop" || st == "parens" || st == "at" || st == "params") { + add(spec.valueKeywords); + add(spec.colorKeywords); + } else if (st == "media" || st == "media_parens") { + add(spec.mediaTypes); + add(spec.mediaFeatures); + } + + if (result.length) return { + list: result, + from: CodeMirror.Pos(cur.line, start), + to: CodeMirror.Pos(cur.line, end) + }; + }); +})(); diff --git a/fhem/www/codemirror/addon/hint/html-hint.js b/fhem/www/codemirror/addon/hint/html-hint.js new file mode 100755 index 000000000..cf256851e --- /dev/null +++ b/fhem/www/codemirror/addon/hint/html-hint.js @@ -0,0 +1,337 @@ +(function () { + var langs = "ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" "); + var targets = ["_blank", "_self", "_top", "_parent"]; + var charsets = ["ascii", "utf-8", "utf-16", "latin1", "latin1"]; + var methods = ["get", "post", "put", "delete"]; + var encs = ["application/x-www-form-urlencoded", "multipart/form-data", "text/plain"]; + var media = ["all", "screen", "print", "embossed", "braille", "handheld", "print", "projection", "screen", "tty", "tv", "speech", + "3d-glasses", "resolution [>][<][=] [X]", "device-aspect-ratio: X/Y", "orientation:portrait", + "orientation:landscape", "device-height: [X]", "device-width: [X]"]; + var s = { attrs: {} }; // Simple tag, reused for a whole lot of tags + + var data = { + a: { + attrs: { + href: null, ping: null, type: null, + media: media, + target: targets, + hreflang: langs + } + }, + abbr: s, + acronym: s, + address: s, + applet: s, + area: { + attrs: { + alt: null, coords: null, href: null, target: null, ping: null, + media: media, hreflang: langs, type: null, + shape: ["default", "rect", "circle", "poly"] + } + }, + article: s, + aside: s, + audio: { + attrs: { + src: null, mediagroup: null, + crossorigin: ["anonymous", "use-credentials"], + preload: ["none", "metadata", "auto"], + autoplay: ["", "autoplay"], + loop: ["", "loop"], + controls: ["", "controls"] + } + }, + b: s, + base: { attrs: { href: null, target: targets } }, + basefont: s, + bdi: s, + bdo: s, + big: s, + blockquote: { attrs: { cite: null } }, + body: s, + br: s, + button: { + attrs: { + form: null, formaction: null, name: null, value: null, + autofocus: ["", "autofocus"], + disabled: ["", "autofocus"], + formenctype: encs, + formmethod: methods, + formnovalidate: ["", "novalidate"], + formtarget: targets, + type: ["submit", "reset", "button"] + } + }, + canvas: { attrs: { width: null, height: null } }, + caption: s, + center: s, + cite: s, + code: s, + col: { attrs: { span: null } }, + colgroup: { attrs: { span: null } }, + command: { + attrs: { + type: ["command", "checkbox", "radio"], + label: null, icon: null, radiogroup: null, command: null, title: null, + disabled: ["", "disabled"], + checked: ["", "checked"] + } + }, + data: { attrs: { value: null } }, + datagrid: { attrs: { disabled: ["", "disabled"], multiple: ["", "multiple"] } }, + datalist: { attrs: { data: null } }, + dd: s, + del: { attrs: { cite: null, datetime: null } }, + details: { attrs: { open: ["", "open"] } }, + dfn: s, + dir: s, + div: s, + dl: s, + dt: s, + em: s, + embed: { attrs: { src: null, type: null, width: null, height: null } }, + eventsource: { attrs: { src: null } }, + fieldset: { attrs: { disabled: ["", "disabled"], form: null, name: null } }, + figcaption: s, + figure: s, + font: s, + footer: s, + form: { + attrs: { + action: null, name: null, + "accept-charset": charsets, + autocomplete: ["on", "off"], + enctype: encs, + method: methods, + novalidate: ["", "novalidate"], + target: targets + } + }, + frame: s, + frameset: s, + h1: s, h2: s, h3: s, h4: s, h5: s, h6: s, + head: { + attrs: {}, + children: ["title", "base", "link", "style", "meta", "script", "noscript", "command"] + }, + header: s, + hgroup: s, + hr: s, + html: { + attrs: { manifest: null }, + children: ["head", "body"] + }, + i: s, + iframe: { + attrs: { + src: null, srcdoc: null, name: null, width: null, height: null, + sandbox: ["allow-top-navigation", "allow-same-origin", "allow-forms", "allow-scripts"], + seamless: ["", "seamless"] + } + }, + img: { + attrs: { + alt: null, src: null, ismap: null, usemap: null, width: null, height: null, + crossorigin: ["anonymous", "use-credentials"] + } + }, + input: { + attrs: { + alt: null, dirname: null, form: null, formaction: null, + height: null, list: null, max: null, maxlength: null, min: null, + name: null, pattern: null, placeholder: null, size: null, src: null, + step: null, value: null, width: null, + accept: ["audio/*", "video/*", "image/*"], + autocomplete: ["on", "off"], + autofocus: ["", "autofocus"], + checked: ["", "checked"], + disabled: ["", "disabled"], + formenctype: encs, + formmethod: methods, + formnovalidate: ["", "novalidate"], + formtarget: targets, + multiple: ["", "multiple"], + readonly: ["", "readonly"], + required: ["", "required"], + type: ["hidden", "text", "search", "tel", "url", "email", "password", "datetime", "date", "month", + "week", "time", "datetime-local", "number", "range", "color", "checkbox", "radio", + "file", "submit", "image", "reset", "button"] + } + }, + ins: { attrs: { cite: null, datetime: null } }, + kbd: s, + keygen: { + attrs: { + challenge: null, form: null, name: null, + autofocus: ["", "autofocus"], + disabled: ["", "disabled"], + keytype: ["RSA"] + } + }, + label: { attrs: { "for": null, form: null } }, + legend: s, + li: { attrs: { value: null } }, + link: { + attrs: { + href: null, type: null, + hreflang: langs, + media: media, + sizes: ["all", "16x16", "16x16 32x32", "16x16 32x32 64x64"] + } + }, + map: { attrs: { name: null } }, + mark: s, + menu: { attrs: { label: null, type: ["list", "context", "toolbar"] } }, + meta: { + attrs: { + content: null, + charset: charsets, + name: ["viewport", "application-name", "author", "description", "generator", "keywords"], + "http-equiv": ["content-language", "content-type", "default-style", "refresh"] + } + }, + meter: { attrs: { value: null, min: null, low: null, high: null, max: null, optimum: null } }, + nav: s, + noframes: s, + noscript: s, + object: { + attrs: { + data: null, type: null, name: null, usemap: null, form: null, width: null, height: null, + typemustmatch: ["", "typemustmatch"] + } + }, + ol: { attrs: { reversed: ["", "reversed"], start: null, type: ["1", "a", "A", "i", "I"] } }, + optgroup: { attrs: { disabled: ["", "disabled"], label: null } }, + option: { attrs: { disabled: ["", "disabled"], label: null, selected: ["", "selected"], value: null } }, + output: { attrs: { "for": null, form: null, name: null } }, + p: s, + param: { attrs: { name: null, value: null } }, + pre: s, + progress: { attrs: { value: null, max: null } }, + q: { attrs: { cite: null } }, + rp: s, + rt: s, + ruby: s, + s: s, + samp: s, + script: { + attrs: { + type: ["text/javascript"], + src: null, + async: ["", "async"], + defer: ["", "defer"], + charset: charsets + } + }, + section: s, + select: { + attrs: { + form: null, name: null, size: null, + autofocus: ["", "autofocus"], + disabled: ["", "disabled"], + multiple: ["", "multiple"] + } + }, + small: s, + source: { attrs: { src: null, type: null, media: null } }, + span: s, + strike: s, + strong: s, + style: { + attrs: { + type: ["text/css"], + media: media, + scoped: null + } + }, + sub: s, + summary: s, + sup: s, + table: s, + tbody: s, + td: { attrs: { colspan: null, rowspan: null, headers: null } }, + textarea: { + attrs: { + dirname: null, form: null, maxlength: null, name: null, placeholder: null, + rows: null, cols: null, + autofocus: ["", "autofocus"], + disabled: ["", "disabled"], + readonly: ["", "readonly"], + required: ["", "required"], + wrap: ["soft", "hard"] + } + }, + tfoot: s, + th: { attrs: { colspan: null, rowspan: null, headers: null, scope: ["row", "col", "rowgroup", "colgroup"] } }, + thead: s, + time: { attrs: { datetime: null } }, + title: s, + tr: s, + track: { + attrs: { + src: null, label: null, "default": null, + kind: ["subtitles", "captions", "descriptions", "chapters", "metadata"], + srclang: langs + } + }, + tt: s, + u: s, + ul: s, + "var": s, + video: { + attrs: { + src: null, poster: null, width: null, height: null, + crossorigin: ["anonymous", "use-credentials"], + preload: ["auto", "metadata", "none"], + autoplay: ["", "autoplay"], + mediagroup: ["movie"], + muted: ["", "muted"], + controls: ["", "controls"] + } + }, + wbr: s + }; + + var globalAttrs = { + accesskey: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"], + "class": null, + contenteditable: ["true", "false"], + contextmenu: null, + dir: ["ltr", "rtl", "auto"], + draggable: ["true", "false", "auto"], + dropzone: ["copy", "move", "link", "string:", "file:"], + hidden: ["hidden"], + id: null, + inert: ["inert"], + itemid: null, + itemprop: null, + itemref: null, + itemscope: ["itemscope"], + itemtype: null, + lang: ["en", "es"], + spellcheck: ["true", "false"], + style: null, + tabindex: ["1", "2", "3", "4", "5", "6", "7", "8", "9"], + title: null, + translate: ["yes", "no"], + onclick: null, + rel: ["stylesheet", "alternate", "author", "bookmark", "help", "license", "next", "nofollow", "noreferrer", "prefetch", "prev", "search", "tag"] + }; + function populate(obj) { + for (var attr in globalAttrs) if (globalAttrs.hasOwnProperty(attr)) + obj.attrs[attr] = globalAttrs[attr]; + } + + populate(s); + for (var tag in data) if (data.hasOwnProperty(tag) && data[tag] != s) + populate(data[tag]); + + CodeMirror.htmlSchema = data; + function htmlHint(cm, options) { + var local = {schemaInfo: data}; + if (options) for (var opt in options) local[opt] = options[opt]; + return CodeMirror.hint.xml(cm, local); + } + CodeMirror.htmlHint = htmlHint; // deprecated + CodeMirror.registerHelper("hint", "html", htmlHint); +})(); diff --git a/fhem/www/codemirror/addon/hint/javascript-hint.js b/fhem/www/codemirror/addon/hint/javascript-hint.js new file mode 100644 index 000000000..c347c206e --- /dev/null +++ b/fhem/www/codemirror/addon/hint/javascript-hint.js @@ -0,0 +1,131 @@ +(function () { + var Pos = CodeMirror.Pos; + + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + function scriptHint(editor, keywords, getToken, options) { + // Find the token at the cursor + var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; + if (/\b(?:string|comment)\b/.test(token.type)) return; + token.state = CodeMirror.innerMode(editor.getMode(), token.state).state; + + // If it's not a 'word-style' token, ignore the token. + if (!/^[\w$_]*$/.test(token.string)) { + token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, + type: token.string == "." ? "property" : null}; + } + // If it is a property, find out what it is a property of. + while (tprop.type == "property") { + tprop = getToken(editor, Pos(cur.line, tprop.start)); + if (tprop.string != ".") return; + tprop = getToken(editor, Pos(cur.line, tprop.start)); + if (!context) var context = []; + context.push(tprop); + } + return {list: getCompletions(token, context, keywords, options), + from: Pos(cur.line, token.start), + to: Pos(cur.line, token.end)}; + } + + function javascriptHint(editor, options) { + return scriptHint(editor, javascriptKeywords, + function (e, cur) {return e.getTokenAt(cur);}, + options); + }; + CodeMirror.javascriptHint = javascriptHint; // deprecated + CodeMirror.registerHelper("hint", "javascript", javascriptHint); + + function getCoffeeScriptToken(editor, cur) { + // This getToken, it is for coffeescript, imitates the behavior of + // getTokenAt method in javascript.js, that is, returning "property" + // type and treat "." as indepenent token. + var token = editor.getTokenAt(cur); + if (cur.ch == token.start + 1 && token.string.charAt(0) == '.') { + token.end = token.start; + token.string = '.'; + token.type = "property"; + } + else if (/^\.[\w$_]*$/.test(token.string)) { + token.type = "property"; + token.start++; + token.string = token.string.replace(/\./, ''); + } + return token; + } + + function coffeescriptHint(editor, options) { + return scriptHint(editor, coffeescriptKeywords, getCoffeeScriptToken, options); + } + CodeMirror.coffeescriptHint = coffeescriptHint; // deprecated + CodeMirror.registerHelper("hint", "coffeescript", coffeescriptHint); + + var stringProps = ("charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight " + + "toUpperCase toLowerCase split concat match replace search").split(" "); + var arrayProps = ("length concat join splice push pop shift unshift slice reverse sort indexOf " + + "lastIndexOf every some filter forEach map reduce reduceRight ").split(" "); + var funcProps = "prototype apply call bind".split(" "); + var javascriptKeywords = ("break case catch continue debugger default delete do else false finally for function " + + "if in instanceof new null return switch throw true try typeof var void while with").split(" "); + var coffeescriptKeywords = ("and break catch class continue delete do else extends false finally for " + + "if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes").split(" "); + + function getCompletions(token, context, keywords, options) { + var found = [], start = token.string; + function maybeAdd(str) { + if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str); + } + function gatherCompletions(obj) { + if (typeof obj == "string") forEach(stringProps, maybeAdd); + else if (obj instanceof Array) forEach(arrayProps, maybeAdd); + else if (obj instanceof Function) forEach(funcProps, maybeAdd); + for (var name in obj) maybeAdd(name); + } + + if (context && context.length) { + // If this is a property, see if it belongs to some object we can + // find in the current environment. + var obj = context.pop(), base; + if (obj.type && obj.type.indexOf("variable") === 0) { + if (options && options.additionalContext) + base = options.additionalContext[obj.string]; + base = base || window[obj.string]; + } else if (obj.type == "string") { + base = ""; + } else if (obj.type == "atom") { + base = 1; + } else if (obj.type == "function") { + if (window.jQuery != null && (obj.string == '$' || obj.string == 'jQuery') && + (typeof window.jQuery == 'function')) + base = window.jQuery(); + else if (window._ != null && (obj.string == '_') && (typeof window._ == 'function')) + base = window._(); + } + while (base != null && context.length) + base = base[context.pop().string]; + if (base != null) gatherCompletions(base); + } else { + // If not, just look in the window object and any local scope + // (reading into JS mode internals to get at the local and global variables) + for (var v = token.state.localVars; v; v = v.next) maybeAdd(v.name); + for (var v = token.state.globalVars; v; v = v.next) maybeAdd(v.name); + gatherCompletions(window); + forEach(keywords, maybeAdd); + } + return found; + } +})(); diff --git a/fhem/www/codemirror/addon/hint/pig-hint.js b/fhem/www/codemirror/addon/hint/pig-hint.js new file mode 100644 index 000000000..6c0c52377 --- /dev/null +++ b/fhem/www/codemirror/addon/hint/pig-hint.js @@ -0,0 +1,121 @@ +(function () { + "use strict"; + + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + function scriptHint(editor, _keywords, getToken) { + // Find the token at the cursor + var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; + // If it's not a 'word-style' token, ignore the token. + + if (!/^[\w$_]*$/.test(token.string)) { + token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, + className: token.string == ":" ? "pig-type" : null}; + } + + if (!context) var context = []; + context.push(tprop); + + var completionList = getCompletions(token, context); + completionList = completionList.sort(); + //prevent autocomplete for last word, instead show dropdown with one word + if(completionList.length == 1) { + completionList.push(" "); + } + + return {list: completionList, + from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end)}; + } + + function pigHint(editor) { + return scriptHint(editor, pigKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); + } + CodeMirror.pigHint = pigHint; // deprecated + CodeMirror.registerHelper("hint", "pig", pigHint); + + var pigKeywords = "VOID IMPORT RETURNS DEFINE LOAD FILTER FOREACH ORDER CUBE DISTINCT COGROUP " + + "JOIN CROSS UNION SPLIT INTO IF OTHERWISE ALL AS BY USING INNER OUTER ONSCHEMA PARALLEL " + + "PARTITION GROUP AND OR NOT GENERATE FLATTEN ASC DESC IS STREAM THROUGH STORE MAPREDUCE " + + "SHIP CACHE INPUT OUTPUT STDERROR STDIN STDOUT LIMIT SAMPLE LEFT RIGHT FULL EQ GT LT GTE LTE " + + "NEQ MATCHES TRUE FALSE"; + var pigKeywordsU = pigKeywords.split(" "); + var pigKeywordsL = pigKeywords.toLowerCase().split(" "); + + var pigTypes = "BOOLEAN INT LONG FLOAT DOUBLE CHARARRAY BYTEARRAY BAG TUPLE MAP"; + var pigTypesU = pigTypes.split(" "); + var pigTypesL = pigTypes.toLowerCase().split(" "); + + var pigBuiltins = "ABS ACOS ARITY ASIN ATAN AVG BAGSIZE BINSTORAGE BLOOM BUILDBLOOM CBRT CEIL " + + "CONCAT COR COS COSH COUNT COUNT_STAR COV CONSTANTSIZE CUBEDIMENSIONS DIFF DISTINCT DOUBLEABS " + + "DOUBLEAVG DOUBLEBASE DOUBLEMAX DOUBLEMIN DOUBLEROUND DOUBLESUM EXP FLOOR FLOATABS FLOATAVG " + + "FLOATMAX FLOATMIN FLOATROUND FLOATSUM GENERICINVOKER INDEXOF INTABS INTAVG INTMAX INTMIN " + + "INTSUM INVOKEFORDOUBLE INVOKEFORFLOAT INVOKEFORINT INVOKEFORLONG INVOKEFORSTRING INVOKER " + + "ISEMPTY JSONLOADER JSONMETADATA JSONSTORAGE LAST_INDEX_OF LCFIRST LOG LOG10 LOWER LONGABS " + + "LONGAVG LONGMAX LONGMIN LONGSUM MAX MIN MAPSIZE MONITOREDUDF NONDETERMINISTIC OUTPUTSCHEMA " + + "PIGSTORAGE PIGSTREAMING RANDOM REGEX_EXTRACT REGEX_EXTRACT_ALL REPLACE ROUND SIN SINH SIZE " + + "SQRT STRSPLIT SUBSTRING SUM STRINGCONCAT STRINGMAX STRINGMIN STRINGSIZE TAN TANH TOBAG " + + "TOKENIZE TOMAP TOP TOTUPLE TRIM TEXTLOADER TUPLESIZE UCFIRST UPPER UTF8STORAGECONVERTER"; + var pigBuiltinsU = pigBuiltins.split(" ").join("() ").split(" "); + var pigBuiltinsL = pigBuiltins.toLowerCase().split(" ").join("() ").split(" "); + var pigBuiltinsC = ("BagSize BinStorage Bloom BuildBloom ConstantSize CubeDimensions DoubleAbs " + + "DoubleAvg DoubleBase DoubleMax DoubleMin DoubleRound DoubleSum FloatAbs FloatAvg FloatMax " + + "FloatMin FloatRound FloatSum GenericInvoker IntAbs IntAvg IntMax IntMin IntSum " + + "InvokeForDouble InvokeForFloat InvokeForInt InvokeForLong InvokeForString Invoker " + + "IsEmpty JsonLoader JsonMetadata JsonStorage LongAbs LongAvg LongMax LongMin LongSum MapSize " + + "MonitoredUDF Nondeterministic OutputSchema PigStorage PigStreaming StringConcat StringMax " + + "StringMin StringSize TextLoader TupleSize Utf8StorageConverter").split(" ").join("() ").split(" "); + + function getCompletions(token, context) { + var found = [], start = token.string; + function maybeAdd(str) { + if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str); + } + + function gatherCompletions(obj) { + if(obj == ":") { + forEach(pigTypesL, maybeAdd); + } + else { + forEach(pigBuiltinsU, maybeAdd); + forEach(pigBuiltinsL, maybeAdd); + forEach(pigBuiltinsC, maybeAdd); + forEach(pigTypesU, maybeAdd); + forEach(pigTypesL, maybeAdd); + forEach(pigKeywordsU, maybeAdd); + forEach(pigKeywordsL, maybeAdd); + } + } + + if (context) { + // If this is a property, see if it belongs to some object we can + // find in the current environment. + var obj = context.pop(), base; + + if (obj.type == "variable") + base = obj.string; + else if(obj.type == "variable-3") + base = ":" + obj.string; + + while (base != null && context.length) + base = base[context.pop().string]; + if (base != null) gatherCompletions(base); + } + return found; + } +})(); diff --git a/fhem/www/codemirror/addon/hint/python-hint.js b/fhem/www/codemirror/addon/hint/python-hint.js new file mode 100644 index 000000000..248284e8c --- /dev/null +++ b/fhem/www/codemirror/addon/hint/python-hint.js @@ -0,0 +1,91 @@ +(function () { + function forEach(arr, f) { + for (var i = 0, e = arr.length; i < e; ++i) f(arr[i]); + } + + function arrayContains(arr, item) { + if (!Array.prototype.indexOf) { + var i = arr.length; + while (i--) { + if (arr[i] === item) { + return true; + } + } + return false; + } + return arr.indexOf(item) != -1; + } + + function scriptHint(editor, _keywords, getToken) { + // Find the token at the cursor + var cur = editor.getCursor(), token = getToken(editor, cur), tprop = token; + // If it's not a 'word-style' token, ignore the token. + + if (!/^[\w$_]*$/.test(token.string)) { + token = tprop = {start: cur.ch, end: cur.ch, string: "", state: token.state, + className: token.string == ":" ? "python-type" : null}; + } + + if (!context) var context = []; + context.push(tprop); + + var completionList = getCompletions(token, context); + completionList = completionList.sort(); + + return {list: completionList, + from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end)}; + } + + function pythonHint(editor) { + return scriptHint(editor, pythonKeywordsU, function (e, cur) {return e.getTokenAt(cur);}); + } + CodeMirror.pythonHint = pythonHint; // deprecated + CodeMirror.registerHelper("hint", "python", pythonHint); + + var pythonKeywords = "and del from not while as elif global or with assert else if pass yield" ++ "break except import print class exec in raise continue finally is return def for lambda try"; + var pythonKeywordsL = pythonKeywords.split(" "); + var pythonKeywordsU = pythonKeywords.toUpperCase().split(" "); + + var pythonBuiltins = "abs divmod input open staticmethod all enumerate int ord str " ++ "any eval isinstance pow sum basestring execfile issubclass print super" ++ "bin file iter property tuple bool filter len range type" ++ "bytearray float list raw_input unichr callable format locals reduce unicode" ++ "chr frozenset long reload vars classmethod getattr map repr xrange" ++ "cmp globals max reversed zip compile hasattr memoryview round __import__" ++ "complex hash min set apply delattr help next setattr buffer" ++ "dict hex object slice coerce dir id oct sorted intern "; + var pythonBuiltinsL = pythonBuiltins.split(" ").join("() ").split(" "); + var pythonBuiltinsU = pythonBuiltins.toUpperCase().split(" ").join("() ").split(" "); + + function getCompletions(token, context) { + var found = [], start = token.string; + function maybeAdd(str) { + if (str.lastIndexOf(start, 0) == 0 && !arrayContains(found, str)) found.push(str); + } + + function gatherCompletions(_obj) { + forEach(pythonBuiltinsL, maybeAdd); + forEach(pythonBuiltinsU, maybeAdd); + forEach(pythonKeywordsL, maybeAdd); + forEach(pythonKeywordsU, maybeAdd); + } + + if (context) { + // If this is a property, see if it belongs to some object we can + // find in the current environment. + var obj = context.pop(), base; + + if (obj.type == "variable") + base = obj.string; + else if(obj.type == "variable-3") + base = ":" + obj.string; + + while (base != null && context.length) + base = base[context.pop().string]; + if (base != null) gatherCompletions(base); + } + return found; + } +})(); diff --git a/fhem/www/codemirror/addon/hint/show-hint.css b/fhem/www/codemirror/addon/hint/show-hint.css new file mode 100644 index 000000000..8a4ff052e --- /dev/null +++ b/fhem/www/codemirror/addon/hint/show-hint.css @@ -0,0 +1,38 @@ +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + + margin: 0; + padding: 2px; + + -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); + box-shadow: 2px 3px 5px rgba(0,0,0,.2); + border-radius: 3px; + border: 1px solid silver; + + background: white; + font-size: 90%; + font-family: monospace; + + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + max-width: 19em; + overflow: hidden; + white-space: pre; + color: black; + cursor: pointer; +} + +.CodeMirror-hint-active { + background: #08f; + color: white; +} diff --git a/fhem/www/codemirror/addon/hint/show-hint.js b/fhem/www/codemirror/addon/hint/show-hint.js new file mode 100644 index 000000000..7e03d1139 --- /dev/null +++ b/fhem/www/codemirror/addon/hint/show-hint.js @@ -0,0 +1,335 @@ +(function() { + "use strict"; + + var HINT_ELEMENT_CLASS = "CodeMirror-hint"; + var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + + CodeMirror.showHint = function(cm, getHints, options) { + // We want a single cursor position. + if (cm.somethingSelected()) return; + if (getHints == null) { + if (options && options.async) return; + else getHints = CodeMirror.hint.auto; + } + + if (cm.state.completionActive) cm.state.completionActive.close(); + + var completion = cm.state.completionActive = new Completion(cm, getHints, options || {}); + CodeMirror.signal(cm, "startCompletion", cm); + if (completion.options.async) + getHints(cm, function(hints) { completion.showHints(hints); }, completion.options); + else + return completion.showHints(getHints(cm, completion.options)); + }; + + function Completion(cm, getHints, options) { + this.cm = cm; + this.getHints = getHints; + this.options = options; + this.widget = this.onClose = null; + } + + Completion.prototype = { + close: function() { + if (!this.active()) return; + this.cm.state.completionActive = null; + + if (this.widget) this.widget.close(); + if (this.onClose) this.onClose(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function() { + return this.cm.state.completionActive == this; + }, + + pick: function(data, i) { + var completion = data.list[i]; + if (completion.hint) completion.hint(this.cm, data, completion); + else this.cm.replaceRange(getText(completion), data.from, data.to); + CodeMirror.signal(data, "pick", completion); + this.close(); + }, + + showHints: function(data) { + if (!data || !data.list.length || !this.active()) return this.close(); + + if (this.options.completeSingle != false && data.list.length == 1) + this.pick(data, 0); + else + this.showWidget(data); + }, + + showWidget: function(data) { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + + var debounce = 0, completion = this, finished; + var closeOn = this.options.closeCharacters || /[\s()\[\]{};:>,]/; + var startPos = this.cm.getCursor(), startLen = this.cm.getLine(startPos.line).length; + + var requestAnimationFrame = window.requestAnimationFrame || function(fn) { + return setTimeout(fn, 1000/60); + }; + var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + + function done() { + if (finished) return; + finished = true; + completion.close(); + completion.cm.off("cursorActivity", activity); + if (data) CodeMirror.signal(data, "close"); + } + + function update() { + if (finished) return; + CodeMirror.signal(data, "update"); + if (completion.options.async) + completion.getHints(completion.cm, finishUpdate, completion.options); + else + finishUpdate(completion.getHints(completion.cm, completion.options)); + } + function finishUpdate(data_) { + data = data_; + if (finished) return; + if (!data || !data.list.length) return done(); + completion.widget = new Widget(completion, data); + } + + function clearDebounce() { + if (debounce) { + cancelAnimationFrame(debounce); + debounce = 0; + } + } + + function activity() { + clearDebounce(); + var pos = completion.cm.getCursor(), line = completion.cm.getLine(pos.line); + if (pos.line != startPos.line || line.length - pos.ch != startLen - startPos.ch || + pos.ch < startPos.ch || completion.cm.somethingSelected() || + (pos.ch && closeOn.test(line.charAt(pos.ch - 1)))) { + completion.close(); + } else { + debounce = requestAnimationFrame(update); + if (completion.widget) completion.widget.close(); + } + } + this.cm.on("cursorActivity", activity); + this.onClose = done; + } + }; + + function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; + } + + function buildKeyMap(options, handle) { + var baseMap = { + Up: function() {handle.moveFocus(-1);}, + Down: function() {handle.moveFocus(1);}, + PageUp: function() {handle.moveFocus(-handle.menuSize() + 1, true);}, + PageDown: function() {handle.moveFocus(handle.menuSize() - 1, true);}, + Home: function() {handle.setFocus(0);}, + End: function() {handle.setFocus(handle.length - 1);}, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + var ourMap = options.customKeys ? {} : baseMap; + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function(cm) { return val(cm, handle); }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (options.customKeys) + for (var key in options.customKeys) if (options.customKeys.hasOwnProperty(key)) + addBinding(key, options.customKeys[key]); + if (options.extraKeys) + for (var key in options.extraKeys) if (options.extraKeys.hasOwnProperty(key)) + addBinding(key, options.extraKeys[key]); + return ourMap; + } + + function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } + } + + function Widget(completion, data) { + this.completion = completion; + this.data = data; + var widget = this, cm = completion.cm, options = completion.options; + + var hints = this.hints = document.createElement("ul"); + hints.className = "CodeMirror-hints"; + this.selectedHint = options.getDefaultSelection ? options.getDefaultSelection(cm,options,data) : 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(document.createElement("li")), cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var pos = cm.cursorCoords(options.alignWithWord !== false ? data.from : null); + var left = pos.left, top = pos.bottom, below = true; + hints.style.left = left + "px"; + hints.style.top = top + "px"; + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + (options.container || document.body).appendChild(hints); + var box = hints.getBoundingClientRect(); + var overlapX = box.right - winW, overlapY = box.bottom - winH; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX) + "px"; + } + if (overlapY > 0) { + var height = box.bottom - box.top; + if (box.top - (pos.bottom - pos.top) - height > 0) { + overlapY = height + (pos.bottom - pos.top); + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + overlapY -= height - winH; + } + hints.style.top = (top = pos.bottom - overlapY) + "px"; + } + + cm.addKeyMap(this.keyMap = buildKeyMap(options, { + moveFocus: function(n, avoidWrap) { widget.changeActive(widget.selectedHint + n, avoidWrap); }, + setFocus: function(n) { widget.changeActive(n); }, + menuSize: function() { return widget.screenAmount(); }, + length: completions.length, + close: function() { completion.close(); }, + pick: function() { widget.pick(); } + })); + + if (options.closeOnUnfocus !== false) { + var closingOnBlur; + cm.on("blur", this.onBlur = function() { closingOnBlur = setTimeout(function() { completion.close(); }, 100); }); + cm.on("focus", this.onFocus = function() { clearTimeout(closingOnBlur); }); + } + + var startScroll = cm.getScrollInfo(); + cm.on("scroll", this.onScroll = function() { + var curScroll = cm.getScrollInfo(), editor = cm.getWrapperElement().getBoundingClientRect(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) {widget.changeActive(t.hintId); widget.pick();} + }); + + CodeMirror.on(hints, "click", function(e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function() { + setTimeout(function(){cm.focus();}, 20); + }); + + CodeMirror.signal(data, "select", completions[0], hints.firstChild); + return true; + } + + Widget.prototype = { + close: function() { + if (this.completion.widget != this) return; + this.completion.widget = null; + this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus !== false) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + pick: function() { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function(i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + if (node.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node.offsetTop - 3; + else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + screenAmount: function() { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + } + }; + + CodeMirror.registerHelper("hint", "auto", function(cm, options) { + var helpers = cm.getHelpers(cm.getCursor(), "hint"); + if (helpers.length) { + for (var i = 0; i < helpers.length; i++) { + var cur = helpers[i](cm, options); + if (cur && cur.list.length) return cur; + } + } else { + var words = cm.getHelper(cm.getCursor(), "hintWords"); + if (words) return CodeMirror.hint.fromList(cm, {words: words}); + } + }); + + CodeMirror.registerHelper("hint", "fromList", function(cm, options) { + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, token.string.length) == token.string) + found.push(word); + } + + if (found.length) return { + list: found, + from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end) + }; + }); + + CodeMirror.commands.autocomplete = CodeMirror.showHint; +})(); diff --git a/fhem/www/codemirror/addon/hint/sql-hint.js b/fhem/www/codemirror/addon/hint/sql-hint.js new file mode 100644 index 000000000..d00781cff --- /dev/null +++ b/fhem/www/codemirror/addon/hint/sql-hint.js @@ -0,0 +1,146 @@ +(function () { + "use strict"; + + var tables; + var keywords; + var CONS = { + QUERY_DIV: ";", + ALIAS_KEYWORD: "AS" + }; + + function getKeywords(editor) { + var mode = editor.doc.modeOption; + if(mode === "sql") mode = "text/x-sql"; + return CodeMirror.resolveMode(mode).keywords; + } + + function match(string, word) { + var len = string.length; + var sub = word.substr(0, len); + return string.toUpperCase() === sub.toUpperCase(); + } + + function addMatches(result, search, wordlist, formatter) { + for(var word in wordlist) { + if(!wordlist.hasOwnProperty(word)) continue; + if(Array.isArray(wordlist)) { + word = wordlist[word]; + } + if(match(search, word)) { + result.push(formatter(word)); + } + } + } + + function columnCompletion(result, editor) { + var cur = editor.getCursor(); + var token = editor.getTokenAt(cur); + var string = token.string.substr(1); + var prevCur = CodeMirror.Pos(cur.line, token.start); + var table = editor.getTokenAt(prevCur).string; + if( !tables.hasOwnProperty( table ) ){ + table = findTableByAlias(table, editor); + } + var columns = tables[table]; + if(!columns) { + return; + } + addMatches(result, string, columns, + function(w) {return "." + w;}); + } + + function eachWord(lineText, f) { + if( !lineText ){return;} + var excepted = /[,;]/g; + var words = lineText.split( " " ); + for( var i = 0; i < words.length; i++ ){ + f( words[i]?words[i].replace( excepted, '' ) : '' ); + } + } + + function convertCurToNumber( cur ){ + // max characters of a line is 999,999. + return cur.line + cur.ch / Math.pow( 10, 6 ); + } + + function convertNumberToCur( num ){ + return CodeMirror.Pos(Math.floor( num ), +num.toString().split( '.' ).pop()); + } + + function findTableByAlias(alias, editor) { + var doc = editor.doc; + var fullQuery = doc.getValue(); + var aliasUpperCase = alias.toUpperCase(); + var previousWord = ""; + var table = ""; + var separator = []; + var validRange = { + start: CodeMirror.Pos( 0, 0 ), + end: CodeMirror.Pos( editor.lastLine(), editor.getLineHandle( editor.lastLine() ).length ) + }; + + //add separator + var indexOfSeparator = fullQuery.indexOf( CONS.QUERY_DIV ); + while( indexOfSeparator != -1 ){ + separator.push( doc.posFromIndex(indexOfSeparator)); + indexOfSeparator = fullQuery.indexOf( CONS.QUERY_DIV, indexOfSeparator+1); + } + separator.unshift( CodeMirror.Pos( 0, 0 ) ); + separator.push( CodeMirror.Pos( editor.lastLine(), editor.getLineHandle( editor.lastLine() ).text.length ) ); + + //find valieRange + var prevItem = 0; + var current = convertCurToNumber( editor.getCursor() ); + for( var i=0; i< separator.length; i++){ + var _v = convertCurToNumber( separator[i] ); + if( current > prevItem && current <= _v ){ + validRange = { start: convertNumberToCur( prevItem ), end: convertNumberToCur( _v ) }; + break; + } + prevItem = _v; + } + + var query = doc.getRange(validRange.start, validRange.end, false); + + for(var i=0; i < query.length; i++){ + var lineText = query[i]; + eachWord( lineText, function( word ){ + var wordUpperCase = word.toUpperCase(); + if( wordUpperCase === aliasUpperCase && tables.hasOwnProperty( previousWord ) ){ + table = previousWord; + } + if( wordUpperCase !== CONS.ALIAS_KEYWORD ){ + previousWord = word; + } + }); + if( table ){ break; } + } + return table; + } + + function sqlHint(editor, options) { + tables = (options && options.tables) || {}; + keywords = keywords || getKeywords(editor); + var cur = editor.getCursor(); + var token = editor.getTokenAt(cur); + var result = []; + var search = token.string.trim(); + + addMatches(result, search, keywords, + function(w) {return w.toUpperCase();}); + + addMatches(result, search, tables, + function(w) {return w;}); + + if(search.lastIndexOf('.') === 0) { + columnCompletion(result, editor); + } + + return { + list: result, + from: CodeMirror.Pos(cur.line, token.start), + to: CodeMirror.Pos(cur.line, token.end) + }; + } + CodeMirror.registerHelper("hint", "sql", sqlHint); +})(); diff --git a/fhem/www/codemirror/addon/hint/xml-hint.js b/fhem/www/codemirror/addon/hint/xml-hint.js new file mode 100644 index 000000000..69f2b771f --- /dev/null +++ b/fhem/www/codemirror/addon/hint/xml-hint.js @@ -0,0 +1,69 @@ +(function() { + "use strict"; + + var Pos = CodeMirror.Pos; + + function getHints(cm, options) { + var tags = options && options.schemaInfo; + var quote = (options && options.quoteChar) || '"'; + if (!tags) return; + var cur = cm.getCursor(), token = cm.getTokenAt(cur); + var inner = CodeMirror.innerMode(cm.getMode(), token.state); + if (inner.mode.name != "xml") return; + var result = [], replaceToken = false, prefix; + var isTag = token.string.charAt(0) == "<"; + if (!inner.state.tagName || isTag) { // Tag completion + if (isTag) { + prefix = token.string.slice(1); + replaceToken = true; + } + var cx = inner.state.context, curTag = cx && tags[cx.tagName]; + var childList = cx ? curTag && curTag.children : tags["!top"]; + if (childList) { + for (var i = 0; i < childList.length; ++i) if (!prefix || childList[i].lastIndexOf(prefix, 0) == 0) + result.push("<" + childList[i]); + } else { + for (var name in tags) if (tags.hasOwnProperty(name) && name != "!top" && (!prefix || name.lastIndexOf(prefix, 0) == 0)) + result.push("<" + name); + } + if (cx && (!prefix || ("/" + cx.tagName).lastIndexOf(prefix, 0) == 0)) + result.push(""); + } else { + // Attribute completion + var curTag = tags[inner.state.tagName], attrs = curTag && curTag.attrs; + if (!attrs) return; + if (token.type == "string" || token.string == "=") { // A value + var before = cm.getRange(Pos(cur.line, Math.max(0, cur.ch - 60)), + Pos(cur.line, token.type == "string" ? token.start : token.end)); + var atName = before.match(/([^\s\u00a0=<>\"\']+)=$/), atValues; + if (!atName || !attrs.hasOwnProperty(atName[1]) || !(atValues = attrs[atName[1]])) return; + if (typeof atValues == 'function') atValues = atValues.call(this, cm); // Functions can be used to supply values for autocomplete widget + if (token.type == "string") { + prefix = token.string; + if (/['"]/.test(token.string.charAt(0))) { + quote = token.string.charAt(0); + prefix = token.string.slice(1); + } + replaceToken = true; + } + for (var i = 0; i < atValues.length; ++i) if (!prefix || atValues[i].lastIndexOf(prefix, 0) == 0) + result.push(quote + atValues[i] + quote); + } else { // An attribute name + if (token.type == "attribute") { + prefix = token.string; + replaceToken = true; + } + for (var attr in attrs) if (attrs.hasOwnProperty(attr) && (!prefix || attr.lastIndexOf(prefix, 0) == 0)) + result.push(attr); + } + } + return { + list: result, + from: replaceToken ? Pos(cur.line, token.start) : cur, + to: replaceToken ? Pos(cur.line, token.end) : cur + }; + } + + CodeMirror.xmlHint = getHints; // deprecated + CodeMirror.registerHelper("hint", "xml", getHints); +})(); diff --git a/fhem/www/codemirror/fhem.README b/fhem/www/codemirror/fhem.README new file mode 100644 index 000000000..325d87f21 --- /dev/null +++ b/fhem/www/codemirror/fhem.README @@ -0,0 +1,6 @@ +# $Id $ + +- Directory maintained for fhem by betateilchen@FHEM-FORUM +- Autocomplete ./mode/fhem/fhem.js maintained by papa@FHEM-FORUM + +See commandref -> 01_FHEMWEB.pm for correct use. diff --git a/fhem/www/codemirror/lib/codemirror.css b/fhem/www/codemirror/lib/codemirror.css new file mode 100644 index 000000000..23eaf74d4 --- /dev/null +++ b/fhem/www/codemirror/lib/codemirror.css @@ -0,0 +1,263 @@ +/* BASICS */ + +.CodeMirror { + /* Set height, width, borders, and global font properties here */ + font-family: monospace; + height: 300px; +} +.CodeMirror-scroll { + /* Set scrolling behaviour here */ + overflow: auto; +} + +/* PADDING */ + +.CodeMirror-lines { + padding: 4px 0; /* Vertical padding around content */ +} +.CodeMirror pre { + padding: 0 4px; /* Horizontal padding of content */ +} + +.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + background-color: white; /* The little square between H and V scrollbars */ +} + +/* GUTTER */ + +.CodeMirror-gutters { + border-right: 1px solid #ddd; + background-color: #f7f7f7; + white-space: nowrap; +} +.CodeMirror-linenumbers {} +.CodeMirror-linenumber { + padding: 0 3px 0 5px; + min-width: 20px; + text-align: right; + color: #999; +} + +/* CURSOR */ + +.CodeMirror div.CodeMirror-cursor { + border-left: 1px solid black; + z-index: 3; +} +/* Shown when moving in bi-directional text */ +.CodeMirror div.CodeMirror-secondarycursor { + border-left: 1px solid silver; +} +.CodeMirror.cm-keymap-fat-cursor div.CodeMirror-cursor { + width: auto; + border: 0; + background: #7e7; + z-index: 1; +} +/* Can style cursor different in overwrite (non-insert) mode */ +.CodeMirror div.CodeMirror-cursor.CodeMirror-overwrite {} + +.cm-tab { display: inline-block; } + +/* DEFAULT THEME */ + +.cm-s-default .cm-keyword {color: #708;} +.cm-s-default .cm-atom {color: #219;} +.cm-s-default .cm-number {color: #164;} +.cm-s-default .cm-def {color: #00f;} +.cm-s-default .cm-variable {color: black;} +.cm-s-default .cm-variable-2 {color: #05a;} +.cm-s-default .cm-variable-3 {color: #085;} +.cm-s-default .cm-property {color: black;} +.cm-s-default .cm-operator {color: black;} +.cm-s-default .cm-comment {color: #a50;} +.cm-s-default .cm-string {color: #a11;} +.cm-s-default .cm-string-2 {color: #f50;} +.cm-s-default .cm-meta {color: #555;} +.cm-s-default .cm-qualifier {color: #555;} +.cm-s-default .cm-builtin {color: #30a;} +.cm-s-default .cm-bracket {color: #997;} +.cm-s-default .cm-tag {color: #170;} +.cm-s-default .cm-attribute {color: #00c;} +.cm-s-default .cm-header {color: blue;} +.cm-s-default .cm-quote {color: #090;} +.cm-s-default .cm-hr {color: #999;} +.cm-s-default .cm-link {color: #00c;} + +.cm-negative {color: #d44;} +.cm-positive {color: #292;} +.cm-header, .cm-strong {font-weight: bold;} +.cm-em {font-style: italic;} +.cm-link {text-decoration: underline;} + +.cm-s-default .cm-error {color: #f00;} +.cm-invalidchar {color: #f00;} + +div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} +div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} +.CodeMirror-activeline-background {background: #e8f2ff;} + +/* STOP */ + +/* The rest of this file contains styles related to the mechanics of + the editor. You probably shouldn't touch them. */ + +.CodeMirror { + line-height: 1; + position: relative; + overflow: hidden; + background: white; + color: black; +} + +.CodeMirror-scroll { + /* 30px is the magic margin used to hide the element's real scrollbars */ + /* See overflow: hidden in .CodeMirror */ + margin-bottom: -30px; margin-right: -30px; + padding-bottom: 30px; padding-right: 30px; + height: 100%; + outline: none; /* Prevent dragging from highlighting the element */ + position: relative; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +.CodeMirror-sizer { + position: relative; +} + +/* The fake, visible scrollbars. Used to force redraw during scrolling + before actuall scrolling happens, thus preventing shaking and + flickering artifacts. */ +.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { + position: absolute; + z-index: 6; + display: none; +} +.CodeMirror-vscrollbar { + right: 0; top: 0; + overflow-x: hidden; + overflow-y: scroll; +} +.CodeMirror-hscrollbar { + bottom: 0; left: 0; + overflow-y: hidden; + overflow-x: scroll; +} +.CodeMirror-scrollbar-filler { + right: 0; bottom: 0; +} +.CodeMirror-gutter-filler { + left: 0; bottom: 0; +} + +.CodeMirror-gutters { + position: absolute; left: 0; top: 0; + padding-bottom: 30px; + z-index: 3; +} +.CodeMirror-gutter { + white-space: normal; + height: 100%; + -moz-box-sizing: content-box; + box-sizing: content-box; + padding-bottom: 30px; + margin-bottom: -32px; + display: inline-block; + /* Hack to make IE7 behave */ + *zoom:1; + *display:inline; +} +.CodeMirror-gutter-elt { + position: absolute; + cursor: default; + z-index: 4; +} + +.CodeMirror-lines { + cursor: text; +} +.CodeMirror pre { + /* Reset some styles that the rest of the page might have set */ + -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; + border-width: 0; + background: transparent; + font-family: inherit; + font-size: inherit; + margin: 0; + white-space: pre; + word-wrap: normal; + line-height: inherit; + color: inherit; + z-index: 2; + position: relative; + overflow: visible; +} +.CodeMirror-wrap pre { + word-wrap: break-word; + white-space: pre-wrap; + word-break: normal; +} +.CodeMirror-code pre { + border-right: 30px solid transparent; + width: -webkit-fit-content; + width: -moz-fit-content; + width: fit-content; +} +.CodeMirror-wrap .CodeMirror-code pre { + border-right: none; + width: auto; +} +.CodeMirror-linebackground { + position: absolute; + left: 0; right: 0; top: 0; bottom: 0; + z-index: 0; +} + +.CodeMirror-linewidget { + position: relative; + z-index: 2; + overflow: auto; +} + +.CodeMirror-widget {} + +.CodeMirror-wrap .CodeMirror-scroll { + overflow-x: hidden; +} + +.CodeMirror-measure { + position: absolute; + width: 100%; + height: 0; + overflow: hidden; + visibility: hidden; +} +.CodeMirror-measure pre { position: static; } + +.CodeMirror div.CodeMirror-cursor { + position: absolute; + visibility: hidden; + border-right: none; + width: 0; +} +.CodeMirror-focused div.CodeMirror-cursor { + visibility: visible; +} + +.CodeMirror-selected { background: #d9d9d9; } +.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } + +.cm-searching { + background: #ffa; + background: rgba(255, 255, 0, .4); +} + +/* IE7 hack to prevent it from returning funny offsetTops on the spans */ +.CodeMirror span { *vertical-align: text-bottom; } + +@media print { + /* Hide the cursor when printing */ + .CodeMirror div.CodeMirror-cursor { + visibility: hidden; + } +} diff --git a/fhem/www/codemirror/lib/codemirror.js b/fhem/www/codemirror/lib/codemirror.js new file mode 100644 index 000000000..d82bd143c --- /dev/null +++ b/fhem/www/codemirror/lib/codemirror.js @@ -0,0 +1,6049 @@ +// CodeMirror version 3.21 +// +// CodeMirror is the only global var we claim +window.CodeMirror = (function() { + "use strict"; + + // BROWSER SNIFFING + + // Crude, but necessary to handle a number of hard-to-feature-detect + // bugs and behavior differences. + var gecko = /gecko\/\d/i.test(navigator.userAgent); + // IE11 currently doesn't count as 'ie', since it has almost none of + // the same bugs as earlier versions. Use ie_gt10 to handle + // incompatibilities in that version. + var old_ie = /MSIE \d/.test(navigator.userAgent); + var ie_lt8 = old_ie && (document.documentMode == null || document.documentMode < 8); + var ie_lt9 = old_ie && (document.documentMode == null || document.documentMode < 9); + var ie_gt10 = /Trident\/([7-9]|\d{2,})\./.test(navigator.userAgent); + var ie = old_ie || ie_gt10; + var webkit = /WebKit\//.test(navigator.userAgent); + var qtwebkit = webkit && /Qt\/\d+\.\d+/.test(navigator.userAgent); + var chrome = /Chrome\//.test(navigator.userAgent); + var opera = /Opera\//.test(navigator.userAgent); + var safari = /Apple Computer/.test(navigator.vendor); + var khtml = /KHTML\//.test(navigator.userAgent); + var mac_geLion = /Mac OS X 1\d\D([7-9]|\d\d)\D/.test(navigator.userAgent); + var mac_geMountainLion = /Mac OS X 1\d\D([8-9]|\d\d)\D/.test(navigator.userAgent); + var phantom = /PhantomJS/.test(navigator.userAgent); + + var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent); + // This is woefully incomplete. Suggestions for alternative methods welcome. + var mobile = ios || /Android|webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(navigator.userAgent); + var mac = ios || /Mac/.test(navigator.platform); + var windows = /win/i.test(navigator.platform); + + var opera_version = opera && navigator.userAgent.match(/Version\/(\d*\.\d*)/); + if (opera_version) opera_version = Number(opera_version[1]); + if (opera_version && opera_version >= 15) { opera = false; webkit = true; } + // Some browsers use the wrong event properties to signal cmd/ctrl on OS X + var flipCtrlCmd = mac && (qtwebkit || opera && (opera_version == null || opera_version < 12.11)); + var captureMiddleClick = gecko || (old_ie && !ie_lt9); + + // Optimize some code when these features are not used + var sawReadOnlySpans = false, sawCollapsedSpans = false; + + // CONSTRUCTOR + + function CodeMirror(place, options) { + if (!(this instanceof CodeMirror)) return new CodeMirror(place, options); + + this.options = options = options || {}; + // Determine effective options based on given values and defaults. + for (var opt in defaults) if (!options.hasOwnProperty(opt) && defaults.hasOwnProperty(opt)) + options[opt] = defaults[opt]; + setGuttersForLineNumbers(options); + + var docStart = typeof options.value == "string" ? 0 : options.value.first; + var display = this.display = makeDisplay(place, docStart); + display.wrapper.CodeMirror = this; + updateGutters(this); + if (options.autofocus && !mobile) focusInput(this); + + this.state = {keyMaps: [], + overlays: [], + modeGen: 0, + overwrite: false, focused: false, + suppressEdits: false, + pasteIncoming: false, cutIncoming: false, + draggingText: false, + highlight: new Delayed()}; + + themeChanged(this); + if (options.lineWrapping) + this.display.wrapper.className += " CodeMirror-wrap"; + + var doc = options.value; + if (typeof doc == "string") doc = new Doc(options.value, options.mode); + operation(this, attachDoc)(this, doc); + + // Override magic textarea content restore that IE sometimes does + // on our hidden textarea on reload + if (old_ie) setTimeout(bind(resetInput, this, true), 20); + + registerEventHandlers(this); + // IE throws unspecified error in certain cases, when + // trying to access activeElement before onload + var hasFocus; try { hasFocus = (document.activeElement == display.input); } catch(e) { } + if (hasFocus || (options.autofocus && !mobile)) setTimeout(bind(onFocus, this), 20); + else onBlur(this); + + operation(this, function() { + for (var opt in optionHandlers) + if (optionHandlers.propertyIsEnumerable(opt)) + optionHandlers[opt](this, options[opt], Init); + for (var i = 0; i < initHooks.length; ++i) initHooks[i](this); + })(); + } + + // DISPLAY CONSTRUCTOR + + function makeDisplay(place, docStart) { + var d = {}; + + var input = d.input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em; outline: none; font-size: 4px;"); + if (webkit) input.style.width = "1000px"; + else input.setAttribute("wrap", "off"); + // if border: 0; -- iOS fails to open keyboard (issue #1287) + if (ios) input.style.border = "1px solid black"; + input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off"); input.setAttribute("spellcheck", "false"); + + // Wraps and hides input textarea + d.inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;"); + // The actual fake scrollbars. + d.scrollbarH = elt("div", [elt("div", null, null, "height: 1px")], "CodeMirror-hscrollbar"); + d.scrollbarV = elt("div", [elt("div", null, null, "width: 1px")], "CodeMirror-vscrollbar"); + d.scrollbarFiller = elt("div", null, "CodeMirror-scrollbar-filler"); + d.gutterFiller = elt("div", null, "CodeMirror-gutter-filler"); + // DIVs containing the selection and the actual code + d.lineDiv = elt("div", null, "CodeMirror-code"); + d.selectionDiv = elt("div", null, null, "position: relative; z-index: 1"); + // Blinky cursor, and element used to ensure cursor fits at the end of a line + d.cursor = elt("div", "\u00a0", "CodeMirror-cursor"); + // Secondary cursor, shown when on a 'jump' in bi-directional text + d.otherCursor = elt("div", "\u00a0", "CodeMirror-cursor CodeMirror-secondarycursor"); + // Used to measure text size + d.measure = elt("div", null, "CodeMirror-measure"); + // Wraps everything that needs to exist inside the vertically-padded coordinate system + d.lineSpace = elt("div", [d.measure, d.selectionDiv, d.lineDiv, d.cursor, d.otherCursor], + null, "position: relative; outline: none"); + // Moved around its parent to cover visible view + d.mover = elt("div", [elt("div", [d.lineSpace], "CodeMirror-lines")], null, "position: relative"); + // Set to the height of the text, causes scrolling + d.sizer = elt("div", [d.mover], "CodeMirror-sizer"); + // D is needed because behavior of elts with overflow: auto and padding is inconsistent across browsers + d.heightForcer = elt("div", null, null, "position: absolute; height: " + scrollerCutOff + "px; width: 1px;"); + // Will contain the gutters, if any + d.gutters = elt("div", null, "CodeMirror-gutters"); + d.lineGutter = null; + // Provides scrolling + d.scroller = elt("div", [d.sizer, d.heightForcer, d.gutters], "CodeMirror-scroll"); + d.scroller.setAttribute("tabIndex", "-1"); + // The element in which the editor lives. + d.wrapper = elt("div", [d.inputDiv, d.scrollbarH, d.scrollbarV, + d.scrollbarFiller, d.gutterFiller, d.scroller], "CodeMirror"); + // Work around IE7 z-index bug + if (ie_lt8) { d.gutters.style.zIndex = -1; d.scroller.style.paddingRight = 0; } + if (place.appendChild) place.appendChild(d.wrapper); else place(d.wrapper); + + // Needed to hide big blue blinking cursor on Mobile Safari + if (ios) input.style.width = "0px"; + if (!webkit) d.scroller.draggable = true; + // Needed to handle Tab key in KHTML + if (khtml) { d.inputDiv.style.height = "1px"; d.inputDiv.style.position = "absolute"; } + // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8). + else if (ie_lt8) d.scrollbarH.style.minWidth = d.scrollbarV.style.minWidth = "18px"; + + // Current visible range (may be bigger than the view window). + d.viewOffset = d.lastSizeC = 0; + d.showingFrom = d.showingTo = docStart; + + // Used to only resize the line number gutter when necessary (when + // the amount of lines crosses a boundary that makes its width change) + d.lineNumWidth = d.lineNumInnerWidth = d.lineNumChars = null; + // See readInput and resetInput + d.prevInput = ""; + // Set to true when a non-horizontal-scrolling widget is added. As + // an optimization, widget aligning is skipped when d is false. + d.alignWidgets = false; + // Flag that indicates whether we currently expect input to appear + // (after some event like 'keypress' or 'input') and are polling + // intensively. + d.pollingFast = false; + // Self-resetting timeout for the poller + d.poll = new Delayed(); + + d.cachedCharWidth = d.cachedTextHeight = null; + d.measureLineCache = []; + d.measureLineCachePos = 0; + + // Tracks when resetInput has punted to just putting a short + // string instead of the (large) selection. + d.inaccurateSelection = false; + + // Tracks the maximum line length so that the horizontal scrollbar + // can be kept static when scrolling. + d.maxLine = null; + d.maxLineLength = 0; + d.maxLineChanged = false; + + // Used for measuring wheel scrolling granularity + d.wheelDX = d.wheelDY = d.wheelStartX = d.wheelStartY = null; + + return d; + } + + // STATE UPDATES + + // Used to get the editor into a consistent state again when options change. + + function loadMode(cm) { + cm.doc.mode = CodeMirror.getMode(cm.options, cm.doc.modeOption); + resetModeState(cm); + } + + function resetModeState(cm) { + cm.doc.iter(function(line) { + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + }); + cm.doc.frontier = cm.doc.first; + startWorker(cm, 100); + cm.state.modeGen++; + if (cm.curOp) regChange(cm); + } + + function wrappingChanged(cm) { + if (cm.options.lineWrapping) { + cm.display.wrapper.className += " CodeMirror-wrap"; + cm.display.sizer.style.minWidth = ""; + } else { + cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-wrap", ""); + computeMaxLength(cm); + } + estimateLineHeights(cm); + regChange(cm); + clearCaches(cm); + setTimeout(function(){updateScrollbars(cm);}, 100); + } + + function estimateHeight(cm) { + var th = textHeight(cm.display), wrapping = cm.options.lineWrapping; + var perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3); + return function(line) { + if (lineIsHidden(cm.doc, line)) + return 0; + else if (wrapping) + return (Math.ceil(line.text.length / perLine) || 1) * th; + else + return th; + }; + } + + function estimateLineHeights(cm) { + var doc = cm.doc, est = estimateHeight(cm); + doc.iter(function(line) { + var estHeight = est(line); + if (estHeight != line.height) updateLineHeight(line, estHeight); + }); + } + + function keyMapChanged(cm) { + var map = keyMap[cm.options.keyMap], style = map.style; + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-keymap-\S+/g, "") + + (style ? " cm-keymap-" + style : ""); + } + + function themeChanged(cm) { + cm.display.wrapper.className = cm.display.wrapper.className.replace(/\s*cm-s-\S+/g, "") + + cm.options.theme.replace(/(^|\s)\s*/g, " cm-s-"); + clearCaches(cm); + } + + function guttersChanged(cm) { + updateGutters(cm); + regChange(cm); + setTimeout(function(){alignHorizontally(cm);}, 20); + } + + function updateGutters(cm) { + var gutters = cm.display.gutters, specs = cm.options.gutters; + removeChildren(gutters); + for (var i = 0; i < specs.length; ++i) { + var gutterClass = specs[i]; + var gElt = gutters.appendChild(elt("div", null, "CodeMirror-gutter " + gutterClass)); + if (gutterClass == "CodeMirror-linenumbers") { + cm.display.lineGutter = gElt; + gElt.style.width = (cm.display.lineNumWidth || 1) + "px"; + } + } + gutters.style.display = i ? "" : "none"; + } + + function lineLength(doc, line) { + if (line.height == 0) return 0; + var len = line.text.length, merged, cur = line; + while (merged = collapsedSpanAtStart(cur)) { + var found = merged.find(); + cur = getLine(doc, found.from.line); + len += found.from.ch - found.to.ch; + } + cur = line; + while (merged = collapsedSpanAtEnd(cur)) { + var found = merged.find(); + len -= cur.text.length - found.from.ch; + cur = getLine(doc, found.to.line); + len += cur.text.length - found.to.ch; + } + return len; + } + + function computeMaxLength(cm) { + var d = cm.display, doc = cm.doc; + d.maxLine = getLine(doc, doc.first); + d.maxLineLength = lineLength(doc, d.maxLine); + d.maxLineChanged = true; + doc.iter(function(line) { + var len = lineLength(doc, line); + if (len > d.maxLineLength) { + d.maxLineLength = len; + d.maxLine = line; + } + }); + } + + // Make sure the gutters options contains the element + // "CodeMirror-linenumbers" when the lineNumbers option is true. + function setGuttersForLineNumbers(options) { + var found = indexOf(options.gutters, "CodeMirror-linenumbers"); + if (found == -1 && options.lineNumbers) { + options.gutters = options.gutters.concat(["CodeMirror-linenumbers"]); + } else if (found > -1 && !options.lineNumbers) { + options.gutters = options.gutters.slice(0); + options.gutters.splice(found, 1); + } + } + + // SCROLLBARS + + // Re-synchronize the fake scrollbars with the actual size of the + // content. Optionally force a scrollTop. + function updateScrollbars(cm) { + var d = cm.display, docHeight = cm.doc.height; + var totalHeight = docHeight + paddingVert(d); + d.sizer.style.minHeight = d.heightForcer.style.top = totalHeight + "px"; + d.gutters.style.height = Math.max(totalHeight, d.scroller.clientHeight - scrollerCutOff) + "px"; + var scrollHeight = Math.max(totalHeight, d.scroller.scrollHeight); + var needsH = d.scroller.scrollWidth > (d.scroller.clientWidth + 1); + var needsV = scrollHeight > (d.scroller.clientHeight + 1); + if (needsV) { + d.scrollbarV.style.display = "block"; + d.scrollbarV.style.bottom = needsH ? scrollbarWidth(d.measure) + "px" : "0"; + d.scrollbarV.firstChild.style.height = + (scrollHeight - d.scroller.clientHeight + d.scrollbarV.clientHeight) + "px"; + } else { + d.scrollbarV.style.display = ""; + d.scrollbarV.firstChild.style.height = "0"; + } + if (needsH) { + d.scrollbarH.style.display = "block"; + d.scrollbarH.style.right = needsV ? scrollbarWidth(d.measure) + "px" : "0"; + d.scrollbarH.firstChild.style.width = + (d.scroller.scrollWidth - d.scroller.clientWidth + d.scrollbarH.clientWidth) + "px"; + } else { + d.scrollbarH.style.display = ""; + d.scrollbarH.firstChild.style.width = "0"; + } + if (needsH && needsV) { + d.scrollbarFiller.style.display = "block"; + d.scrollbarFiller.style.height = d.scrollbarFiller.style.width = scrollbarWidth(d.measure) + "px"; + } else d.scrollbarFiller.style.display = ""; + if (needsH && cm.options.coverGutterNextToScrollbar && cm.options.fixedGutter) { + d.gutterFiller.style.display = "block"; + d.gutterFiller.style.height = scrollbarWidth(d.measure) + "px"; + d.gutterFiller.style.width = d.gutters.offsetWidth + "px"; + } else d.gutterFiller.style.display = ""; + + if (mac_geLion && scrollbarWidth(d.measure) === 0) { + d.scrollbarV.style.minWidth = d.scrollbarH.style.minHeight = mac_geMountainLion ? "18px" : "12px"; + d.scrollbarV.style.pointerEvents = d.scrollbarH.style.pointerEvents = "none"; + } + } + + function visibleLines(display, doc, viewPort) { + var top = display.scroller.scrollTop, height = display.wrapper.clientHeight; + if (typeof viewPort == "number") top = viewPort; + else if (viewPort) {top = viewPort.top; height = viewPort.bottom - viewPort.top;} + top = Math.floor(top - paddingTop(display)); + var bottom = Math.ceil(top + height); + return {from: lineAtHeight(doc, top), to: lineAtHeight(doc, bottom)}; + } + + // LINE NUMBERS + + function alignHorizontally(cm) { + var display = cm.display; + if (!display.alignWidgets && (!display.gutters.firstChild || !cm.options.fixedGutter)) return; + var comp = compensateForHScroll(display) - display.scroller.scrollLeft + cm.doc.scrollLeft; + var gutterW = display.gutters.offsetWidth, l = comp + "px"; + for (var n = display.lineDiv.firstChild; n; n = n.nextSibling) if (n.alignable) { + for (var i = 0, a = n.alignable; i < a.length; ++i) a[i].style.left = l; + } + if (cm.options.fixedGutter) + display.gutters.style.left = (comp + gutterW) + "px"; + } + + function maybeUpdateLineNumberWidth(cm) { + if (!cm.options.lineNumbers) return false; + var doc = cm.doc, last = lineNumberFor(cm.options, doc.first + doc.size - 1), display = cm.display; + if (last.length != display.lineNumChars) { + var test = display.measure.appendChild(elt("div", [elt("div", last)], + "CodeMirror-linenumber CodeMirror-gutter-elt")); + var innerW = test.firstChild.offsetWidth, padding = test.offsetWidth - innerW; + display.lineGutter.style.width = ""; + display.lineNumInnerWidth = Math.max(innerW, display.lineGutter.offsetWidth - padding); + display.lineNumWidth = display.lineNumInnerWidth + padding; + display.lineNumChars = display.lineNumInnerWidth ? last.length : -1; + display.lineGutter.style.width = display.lineNumWidth + "px"; + return true; + } + return false; + } + + function lineNumberFor(options, i) { + return String(options.lineNumberFormatter(i + options.firstLineNumber)); + } + function compensateForHScroll(display) { + return getRect(display.scroller).left - getRect(display.sizer).left; + } + + // DISPLAY DRAWING + + function updateDisplay(cm, changes, viewPort, forced) { + var oldFrom = cm.display.showingFrom, oldTo = cm.display.showingTo, updated; + var visible = visibleLines(cm.display, cm.doc, viewPort); + for (var first = true;; first = false) { + var oldWidth = cm.display.scroller.clientWidth; + if (!updateDisplayInner(cm, changes, visible, forced)) break; + updated = true; + changes = []; + updateSelection(cm); + updateScrollbars(cm); + if (first && cm.options.lineWrapping && oldWidth != cm.display.scroller.clientWidth) { + forced = true; + continue; + } + forced = false; + + // Clip forced viewport to actual scrollable area + if (viewPort) + viewPort = Math.min(cm.display.scroller.scrollHeight - cm.display.scroller.clientHeight, + typeof viewPort == "number" ? viewPort : viewPort.top); + visible = visibleLines(cm.display, cm.doc, viewPort); + if (visible.from >= cm.display.showingFrom && visible.to <= cm.display.showingTo) + break; + } + + if (updated) { + signalLater(cm, "update", cm); + if (cm.display.showingFrom != oldFrom || cm.display.showingTo != oldTo) + signalLater(cm, "viewportChange", cm, cm.display.showingFrom, cm.display.showingTo); + } + return updated; + } + + // Uses a set of changes plus the current scroll position to + // determine which DOM updates have to be made, and makes the + // updates. + function updateDisplayInner(cm, changes, visible, forced) { + var display = cm.display, doc = cm.doc; + if (!display.wrapper.offsetWidth) { + display.showingFrom = display.showingTo = doc.first; + display.viewOffset = 0; + return; + } + + // Bail out if the visible area is already rendered and nothing changed. + if (!forced && changes.length == 0 && + visible.from > display.showingFrom && visible.to < display.showingTo) + return; + + if (maybeUpdateLineNumberWidth(cm)) + changes = [{from: doc.first, to: doc.first + doc.size}]; + var gutterW = display.sizer.style.marginLeft = display.gutters.offsetWidth + "px"; + display.scrollbarH.style.left = cm.options.fixedGutter ? gutterW : "0"; + + // Used to determine which lines need their line numbers updated + var positionsChangedFrom = Infinity; + if (cm.options.lineNumbers) + for (var i = 0; i < changes.length; ++i) + if (changes[i].diff && changes[i].from < positionsChangedFrom) { positionsChangedFrom = changes[i].from; } + + var end = doc.first + doc.size; + var from = Math.max(visible.from - cm.options.viewportMargin, doc.first); + var to = Math.min(end, visible.to + cm.options.viewportMargin); + if (display.showingFrom < from && from - display.showingFrom < 20) from = Math.max(doc.first, display.showingFrom); + if (display.showingTo > to && display.showingTo - to < 20) to = Math.min(end, display.showingTo); + if (sawCollapsedSpans) { + from = lineNo(visualLine(doc, getLine(doc, from))); + while (to < end && lineIsHidden(doc, getLine(doc, to))) ++to; + } + + // Create a range of theoretically intact lines, and punch holes + // in that using the change info. + var intact = [{from: Math.max(display.showingFrom, doc.first), + to: Math.min(display.showingTo, end)}]; + if (intact[0].from >= intact[0].to) intact = []; + else intact = computeIntact(intact, changes); + // When merged lines are present, we might have to reduce the + // intact ranges because changes in continued fragments of the + // intact lines do require the lines to be redrawn. + if (sawCollapsedSpans) + for (var i = 0; i < intact.length; ++i) { + var range = intact[i], merged; + while (merged = collapsedSpanAtEnd(getLine(doc, range.to - 1))) { + var newTo = merged.find().from.line; + if (newTo > range.from) range.to = newTo; + else { intact.splice(i--, 1); break; } + } + } + + // Clip off the parts that won't be visible + var intactLines = 0; + for (var i = 0; i < intact.length; ++i) { + var range = intact[i]; + if (range.from < from) range.from = from; + if (range.to > to) range.to = to; + if (range.from >= range.to) intact.splice(i--, 1); + else intactLines += range.to - range.from; + } + if (!forced && intactLines == to - from && from == display.showingFrom && to == display.showingTo) { + updateViewOffset(cm); + return; + } + intact.sort(function(a, b) {return a.from - b.from;}); + + // Avoid crashing on IE's "unspecified error" when in iframes + try { + var focused = document.activeElement; + } catch(e) {} + if (intactLines < (to - from) * .7) display.lineDiv.style.display = "none"; + patchDisplay(cm, from, to, intact, positionsChangedFrom); + display.lineDiv.style.display = ""; + if (focused && document.activeElement != focused && focused.offsetHeight) focused.focus(); + + var different = from != display.showingFrom || to != display.showingTo || + display.lastSizeC != display.wrapper.clientHeight; + // This is just a bogus formula that detects when the editor is + // resized or the font size changes. + if (different) { + display.lastSizeC = display.wrapper.clientHeight; + startWorker(cm, 400); + } + display.showingFrom = from; display.showingTo = to; + + display.gutters.style.height = ""; + updateHeightsInViewport(cm); + updateViewOffset(cm); + + return true; + } + + function updateHeightsInViewport(cm) { + var display = cm.display; + var prevBottom = display.lineDiv.offsetTop; + for (var node = display.lineDiv.firstChild, height; node; node = node.nextSibling) if (node.lineObj) { + if (ie_lt8) { + var bot = node.offsetTop + node.offsetHeight; + height = bot - prevBottom; + prevBottom = bot; + } else { + var box = getRect(node); + height = box.bottom - box.top; + } + var diff = node.lineObj.height - height; + if (height < 2) height = textHeight(display); + if (diff > .001 || diff < -.001) { + updateLineHeight(node.lineObj, height); + var widgets = node.lineObj.widgets; + if (widgets) for (var i = 0; i < widgets.length; ++i) + widgets[i].height = widgets[i].node.offsetHeight; + } + } + } + + function updateViewOffset(cm) { + var off = cm.display.viewOffset = heightAtLine(cm, getLine(cm.doc, cm.display.showingFrom)); + // Position the mover div to align with the current virtual scroll position + cm.display.mover.style.top = off + "px"; + } + + function computeIntact(intact, changes) { + for (var i = 0, l = changes.length || 0; i < l; ++i) { + var change = changes[i], intact2 = [], diff = change.diff || 0; + for (var j = 0, l2 = intact.length; j < l2; ++j) { + var range = intact[j]; + if (change.to <= range.from && change.diff) { + intact2.push({from: range.from + diff, to: range.to + diff}); + } else if (change.to <= range.from || change.from >= range.to) { + intact2.push(range); + } else { + if (change.from > range.from) + intact2.push({from: range.from, to: change.from}); + if (change.to < range.to) + intact2.push({from: change.to + diff, to: range.to + diff}); + } + } + intact = intact2; + } + return intact; + } + + function getDimensions(cm) { + var d = cm.display, left = {}, width = {}; + for (var n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { + left[cm.options.gutters[i]] = n.offsetLeft; + width[cm.options.gutters[i]] = n.offsetWidth; + } + return {fixedPos: compensateForHScroll(d), + gutterTotalWidth: d.gutters.offsetWidth, + gutterLeft: left, + gutterWidth: width, + wrapperWidth: d.wrapper.clientWidth}; + } + + function patchDisplay(cm, from, to, intact, updateNumbersFrom) { + var dims = getDimensions(cm); + var display = cm.display, lineNumbers = cm.options.lineNumbers; + if (!intact.length && (!webkit || !cm.display.currentWheelTarget)) + removeChildren(display.lineDiv); + var container = display.lineDiv, cur = container.firstChild; + + function rm(node) { + var next = node.nextSibling; + if (webkit && mac && cm.display.currentWheelTarget == node) { + node.style.display = "none"; + node.lineObj = null; + } else { + node.parentNode.removeChild(node); + } + return next; + } + + var nextIntact = intact.shift(), lineN = from; + cm.doc.iter(from, to, function(line) { + if (nextIntact && nextIntact.to == lineN) nextIntact = intact.shift(); + if (lineIsHidden(cm.doc, line)) { + if (line.height != 0) updateLineHeight(line, 0); + if (line.widgets && cur && cur.previousSibling) for (var i = 0; i < line.widgets.length; ++i) { + var w = line.widgets[i]; + if (w.showIfHidden) { + var prev = cur.previousSibling; + if (/pre/i.test(prev.nodeName)) { + var wrap = elt("div", null, null, "position: relative"); + prev.parentNode.replaceChild(wrap, prev); + wrap.appendChild(prev); + prev = wrap; + } + var wnode = prev.appendChild(elt("div", [w.node], "CodeMirror-linewidget")); + if (!w.handleMouseEvents) wnode.ignoreEvents = true; + positionLineWidget(w, wnode, prev, dims); + } + } + } else if (nextIntact && nextIntact.from <= lineN && nextIntact.to > lineN) { + // This line is intact. Skip to the actual node. Update its + // line number if needed. + while (cur.lineObj != line) cur = rm(cur); + if (lineNumbers && updateNumbersFrom <= lineN && cur.lineNumber) + setTextContent(cur.lineNumber, lineNumberFor(cm.options, lineN)); + cur = cur.nextSibling; + } else { + // For lines with widgets, make an attempt to find and reuse + // the existing element, so that widgets aren't needlessly + // removed and re-inserted into the dom + if (line.widgets) for (var j = 0, search = cur, reuse; search && j < 20; ++j, search = search.nextSibling) + if (search.lineObj == line && /div/i.test(search.nodeName)) { reuse = search; break; } + // This line needs to be generated. + var lineNode = buildLineElement(cm, line, lineN, dims, reuse); + if (lineNode != reuse) { + container.insertBefore(lineNode, cur); + } else { + while (cur != reuse) cur = rm(cur); + cur = cur.nextSibling; + } + + lineNode.lineObj = line; + } + ++lineN; + }); + while (cur) cur = rm(cur); + } + + function buildLineElement(cm, line, lineNo, dims, reuse) { + var built = buildLineContent(cm, line), lineElement = built.pre; + var markers = line.gutterMarkers, display = cm.display, wrap; + + var bgClass = built.bgClass ? built.bgClass + " " + (line.bgClass || "") : line.bgClass; + if (!cm.options.lineNumbers && !markers && !bgClass && !line.wrapClass && !line.widgets) + return lineElement; + + // Lines with gutter elements, widgets or a background class need + // to be wrapped again, and have the extra elements added to the + // wrapper div + + if (reuse) { + reuse.alignable = null; + var isOk = true, widgetsSeen = 0, insertBefore = null; + for (var n = reuse.firstChild, next; n; n = next) { + next = n.nextSibling; + if (!/\bCodeMirror-linewidget\b/.test(n.className)) { + reuse.removeChild(n); + } else { + for (var i = 0; i < line.widgets.length; ++i) { + var widget = line.widgets[i]; + if (widget.node == n.firstChild) { + if (!widget.above && !insertBefore) insertBefore = n; + positionLineWidget(widget, n, reuse, dims); + ++widgetsSeen; + break; + } + } + if (i == line.widgets.length) { isOk = false; break; } + } + } + reuse.insertBefore(lineElement, insertBefore); + if (isOk && widgetsSeen == line.widgets.length) { + wrap = reuse; + reuse.className = line.wrapClass || ""; + } + } + if (!wrap) { + wrap = elt("div", null, line.wrapClass, "position: relative"); + wrap.appendChild(lineElement); + } + // Kludge to make sure the styled element lies behind the selection (by z-index) + if (bgClass) + wrap.insertBefore(elt("div", null, bgClass + " CodeMirror-linebackground"), wrap.firstChild); + if (cm.options.lineNumbers || markers) { + var gutterWrap = wrap.insertBefore(elt("div", null, "CodeMirror-gutter-wrapper", "position: absolute; left: " + + (cm.options.fixedGutter ? dims.fixedPos : -dims.gutterTotalWidth) + "px"), + lineElement); + if (cm.options.fixedGutter) (wrap.alignable || (wrap.alignable = [])).push(gutterWrap); + if (cm.options.lineNumbers && (!markers || !markers["CodeMirror-linenumbers"])) + wrap.lineNumber = gutterWrap.appendChild( + elt("div", lineNumberFor(cm.options, lineNo), + "CodeMirror-linenumber CodeMirror-gutter-elt", + "left: " + dims.gutterLeft["CodeMirror-linenumbers"] + "px; width: " + + display.lineNumInnerWidth + "px")); + if (markers) + for (var k = 0; k < cm.options.gutters.length; ++k) { + var id = cm.options.gutters[k], found = markers.hasOwnProperty(id) && markers[id]; + if (found) + gutterWrap.appendChild(elt("div", [found], "CodeMirror-gutter-elt", "left: " + + dims.gutterLeft[id] + "px; width: " + dims.gutterWidth[id] + "px")); + } + } + if (ie_lt8) wrap.style.zIndex = 2; + if (line.widgets && wrap != reuse) for (var i = 0, ws = line.widgets; i < ws.length; ++i) { + var widget = ws[i], node = elt("div", [widget.node], "CodeMirror-linewidget"); + if (!widget.handleMouseEvents) node.ignoreEvents = true; + positionLineWidget(widget, node, wrap, dims); + if (widget.above) + wrap.insertBefore(node, cm.options.lineNumbers && line.height != 0 ? gutterWrap : lineElement); + else + wrap.appendChild(node); + signalLater(widget, "redraw"); + } + return wrap; + } + + function positionLineWidget(widget, node, wrap, dims) { + if (widget.noHScroll) { + (wrap.alignable || (wrap.alignable = [])).push(node); + var width = dims.wrapperWidth; + node.style.left = dims.fixedPos + "px"; + if (!widget.coverGutter) { + width -= dims.gutterTotalWidth; + node.style.paddingLeft = dims.gutterTotalWidth + "px"; + } + node.style.width = width + "px"; + } + if (widget.coverGutter) { + node.style.zIndex = 5; + node.style.position = "relative"; + if (!widget.noHScroll) node.style.marginLeft = -dims.gutterTotalWidth + "px"; + } + } + + // SELECTION / CURSOR + + function updateSelection(cm) { + var display = cm.display; + var collapsed = posEq(cm.doc.sel.from, cm.doc.sel.to); + if (collapsed || cm.options.showCursorWhenSelecting) + updateSelectionCursor(cm); + else + display.cursor.style.display = display.otherCursor.style.display = "none"; + if (!collapsed) + updateSelectionRange(cm); + else + display.selectionDiv.style.display = "none"; + + // Move the hidden textarea near the cursor to prevent scrolling artifacts + if (cm.options.moveInputWithCursor) { + var headPos = cursorCoords(cm, cm.doc.sel.head, "div"); + var wrapOff = getRect(display.wrapper), lineOff = getRect(display.lineDiv); + display.inputDiv.style.top = Math.max(0, Math.min(display.wrapper.clientHeight - 10, + headPos.top + lineOff.top - wrapOff.top)) + "px"; + display.inputDiv.style.left = Math.max(0, Math.min(display.wrapper.clientWidth - 10, + headPos.left + lineOff.left - wrapOff.left)) + "px"; + } + } + + // No selection, plain cursor + function updateSelectionCursor(cm) { + var display = cm.display, pos = cursorCoords(cm, cm.doc.sel.head, "div"); + display.cursor.style.left = pos.left + "px"; + display.cursor.style.top = pos.top + "px"; + display.cursor.style.height = Math.max(0, pos.bottom - pos.top) * cm.options.cursorHeight + "px"; + display.cursor.style.display = ""; + + if (pos.other) { + display.otherCursor.style.display = ""; + display.otherCursor.style.left = pos.other.left + "px"; + display.otherCursor.style.top = pos.other.top + "px"; + display.otherCursor.style.height = (pos.other.bottom - pos.other.top) * .85 + "px"; + } else { display.otherCursor.style.display = "none"; } + } + + // Highlight selection + function updateSelectionRange(cm) { + var display = cm.display, doc = cm.doc, sel = cm.doc.sel; + var fragment = document.createDocumentFragment(); + var clientWidth = display.lineSpace.offsetWidth, pl = paddingLeft(cm.display); + + function add(left, top, width, bottom) { + if (top < 0) top = 0; + fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left + + "px; top: " + top + "px; width: " + (width == null ? clientWidth - left : width) + + "px; height: " + (bottom - top) + "px")); + } + + function drawForLine(line, fromArg, toArg) { + var lineObj = getLine(doc, line); + var lineLen = lineObj.text.length; + var start, end; + function coords(ch, bias) { + return charCoords(cm, Pos(line, ch), "div", lineObj, bias); + } + + iterateBidiSections(getOrder(lineObj), fromArg || 0, toArg == null ? lineLen : toArg, function(from, to, dir) { + var leftPos = coords(from, "left"), rightPos, left, right; + if (from == to) { + rightPos = leftPos; + left = right = leftPos.left; + } else { + rightPos = coords(to - 1, "right"); + if (dir == "rtl") { var tmp = leftPos; leftPos = rightPos; rightPos = tmp; } + left = leftPos.left; + right = rightPos.right; + } + if (fromArg == null && from == 0) left = pl; + if (rightPos.top - leftPos.top > 3) { // Different lines, draw top part + add(left, leftPos.top, null, leftPos.bottom); + left = pl; + if (leftPos.bottom < rightPos.top) add(left, leftPos.bottom, null, rightPos.top); + } + if (toArg == null && to == lineLen) right = clientWidth; + if (!start || leftPos.top < start.top || leftPos.top == start.top && leftPos.left < start.left) + start = leftPos; + if (!end || rightPos.bottom > end.bottom || rightPos.bottom == end.bottom && rightPos.right > end.right) + end = rightPos; + if (left < pl + 1) left = pl; + add(left, rightPos.top, right - left, rightPos.bottom); + }); + return {start: start, end: end}; + } + + if (sel.from.line == sel.to.line) { + drawForLine(sel.from.line, sel.from.ch, sel.to.ch); + } else { + var fromLine = getLine(doc, sel.from.line), toLine = getLine(doc, sel.to.line); + var singleVLine = visualLine(doc, fromLine) == visualLine(doc, toLine); + var leftEnd = drawForLine(sel.from.line, sel.from.ch, singleVLine ? fromLine.text.length : null).end; + var rightStart = drawForLine(sel.to.line, singleVLine ? 0 : null, sel.to.ch).start; + if (singleVLine) { + if (leftEnd.top < rightStart.top - 2) { + add(leftEnd.right, leftEnd.top, null, leftEnd.bottom); + add(pl, rightStart.top, rightStart.left, rightStart.bottom); + } else { + add(leftEnd.right, leftEnd.top, rightStart.left - leftEnd.right, leftEnd.bottom); + } + } + if (leftEnd.bottom < rightStart.top) + add(pl, leftEnd.bottom, null, rightStart.top); + } + + removeChildrenAndAdd(display.selectionDiv, fragment); + display.selectionDiv.style.display = ""; + } + + // Cursor-blinking + function restartBlink(cm) { + if (!cm.state.focused) return; + var display = cm.display; + clearInterval(display.blinker); + var on = true; + display.cursor.style.visibility = display.otherCursor.style.visibility = ""; + if (cm.options.cursorBlinkRate > 0) + display.blinker = setInterval(function() { + display.cursor.style.visibility = display.otherCursor.style.visibility = (on = !on) ? "" : "hidden"; + }, cm.options.cursorBlinkRate); + } + + // HIGHLIGHT WORKER + + function startWorker(cm, time) { + if (cm.doc.mode.startState && cm.doc.frontier < cm.display.showingTo) + cm.state.highlight.set(time, bind(highlightWorker, cm)); + } + + function highlightWorker(cm) { + var doc = cm.doc; + if (doc.frontier < doc.first) doc.frontier = doc.first; + if (doc.frontier >= cm.display.showingTo) return; + var end = +new Date + cm.options.workTime; + var state = copyState(doc.mode, getStateBefore(cm, doc.frontier)); + var changed = [], prevChange; + doc.iter(doc.frontier, Math.min(doc.first + doc.size, cm.display.showingTo + 500), function(line) { + if (doc.frontier >= cm.display.showingFrom) { // Visible + var oldStyles = line.styles; + line.styles = highlightLine(cm, line, state, true); + var ischange = !oldStyles || oldStyles.length != line.styles.length; + for (var i = 0; !ischange && i < oldStyles.length; ++i) ischange = oldStyles[i] != line.styles[i]; + if (ischange) { + if (prevChange && prevChange.end == doc.frontier) prevChange.end++; + else changed.push(prevChange = {start: doc.frontier, end: doc.frontier + 1}); + } + line.stateAfter = copyState(doc.mode, state); + } else { + processLine(cm, line.text, state); + line.stateAfter = doc.frontier % 5 == 0 ? copyState(doc.mode, state) : null; + } + ++doc.frontier; + if (+new Date > end) { + startWorker(cm, cm.options.workDelay); + return true; + } + }); + if (changed.length) + operation(cm, function() { + for (var i = 0; i < changed.length; ++i) + regChange(this, changed[i].start, changed[i].end); + })(); + } + + // Finds the line to start with when starting a parse. Tries to + // find a line with a stateAfter, so that it can start with a + // valid state. If that fails, it returns the line with the + // smallest indentation, which tends to need the least context to + // parse correctly. + function findStartLine(cm, n, precise) { + var minindent, minline, doc = cm.doc; + var lim = precise ? -1 : n - (cm.doc.mode.innerMode ? 1000 : 100); + for (var search = n; search > lim; --search) { + if (search <= doc.first) return doc.first; + var line = getLine(doc, search - 1); + if (line.stateAfter && (!precise || search <= doc.frontier)) return search; + var indented = countColumn(line.text, null, cm.options.tabSize); + if (minline == null || minindent > indented) { + minline = search - 1; + minindent = indented; + } + } + return minline; + } + + function getStateBefore(cm, n, precise) { + var doc = cm.doc, display = cm.display; + if (!doc.mode.startState) return true; + var pos = findStartLine(cm, n, precise), state = pos > doc.first && getLine(doc, pos-1).stateAfter; + if (!state) state = startState(doc.mode); + else state = copyState(doc.mode, state); + doc.iter(pos, n, function(line) { + processLine(cm, line.text, state); + var save = pos == n - 1 || pos % 5 == 0 || pos >= display.showingFrom && pos < display.showingTo; + line.stateAfter = save ? copyState(doc.mode, state) : null; + ++pos; + }); + if (precise) doc.frontier = pos; + return state; + } + + // POSITION MEASUREMENT + + function paddingTop(display) {return display.lineSpace.offsetTop;} + function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight;} + function paddingLeft(display) { + var e = removeChildrenAndAdd(display.measure, elt("pre", null, null, "text-align: left")).appendChild(elt("span", "x")); + return e.offsetLeft; + } + + function measureChar(cm, line, ch, data, bias) { + var dir = -1; + data = data || measureLine(cm, line); + if (data.crude) { + var left = data.left + ch * data.width; + return {left: left, right: left + data.width, top: data.top, bottom: data.bottom}; + } + + for (var pos = ch;; pos += dir) { + var r = data[pos]; + if (r) break; + if (dir < 0 && pos == 0) dir = 1; + } + bias = pos > ch ? "left" : pos < ch ? "right" : bias; + if (bias == "left" && r.leftSide) r = r.leftSide; + else if (bias == "right" && r.rightSide) r = r.rightSide; + return {left: pos < ch ? r.right : r.left, + right: pos > ch ? r.left : r.right, + top: r.top, + bottom: r.bottom}; + } + + function findCachedMeasurement(cm, line) { + var cache = cm.display.measureLineCache; + for (var i = 0; i < cache.length; ++i) { + var memo = cache[i]; + if (memo.text == line.text && memo.markedSpans == line.markedSpans && + cm.display.scroller.clientWidth == memo.width && + memo.classes == line.textClass + "|" + line.wrapClass) + return memo; + } + } + + function clearCachedMeasurement(cm, line) { + var exists = findCachedMeasurement(cm, line); + if (exists) exists.text = exists.measure = exists.markedSpans = null; + } + + function measureLine(cm, line) { + // First look in the cache + var cached = findCachedMeasurement(cm, line); + if (cached) return cached.measure; + + // Failing that, recompute and store result in cache + var measure = measureLineInner(cm, line); + var cache = cm.display.measureLineCache; + var memo = {text: line.text, width: cm.display.scroller.clientWidth, + markedSpans: line.markedSpans, measure: measure, + classes: line.textClass + "|" + line.wrapClass}; + if (cache.length == 16) cache[++cm.display.measureLineCachePos % 16] = memo; + else cache.push(memo); + return measure; + } + + function measureLineInner(cm, line) { + if (!cm.options.lineWrapping && line.text.length >= cm.options.crudeMeasuringFrom) + return crudelyMeasureLine(cm, line); + + var display = cm.display, measure = emptyArray(line.text.length); + var pre = buildLineContent(cm, line, measure, true).pre; + + // IE does not cache element positions of inline elements between + // calls to getBoundingClientRect. This makes the loop below, + // which gathers the positions of all the characters on the line, + // do an amount of layout work quadratic to the number of + // characters. When line wrapping is off, we try to improve things + // by first subdividing the line into a bunch of inline blocks, so + // that IE can reuse most of the layout information from caches + // for those blocks. This does interfere with line wrapping, so it + // doesn't work when wrapping is on, but in that case the + // situation is slightly better, since IE does cache line-wrapping + // information and only recomputes per-line. + if (old_ie && !ie_lt8 && !cm.options.lineWrapping && pre.childNodes.length > 100) { + var fragment = document.createDocumentFragment(); + var chunk = 10, n = pre.childNodes.length; + for (var i = 0, chunks = Math.ceil(n / chunk); i < chunks; ++i) { + var wrap = elt("div", null, null, "display: inline-block"); + for (var j = 0; j < chunk && n; ++j) { + wrap.appendChild(pre.firstChild); + --n; + } + fragment.appendChild(wrap); + } + pre.appendChild(fragment); + } + + removeChildrenAndAdd(display.measure, pre); + + var outer = getRect(display.lineDiv); + var vranges = [], data = emptyArray(line.text.length), maxBot = pre.offsetHeight; + // Work around an IE7/8 bug where it will sometimes have randomly + // replaced our pre with a clone at this point. + if (ie_lt9 && display.measure.first != pre) + removeChildrenAndAdd(display.measure, pre); + + function measureRect(rect) { + var top = rect.top - outer.top, bot = rect.bottom - outer.top; + if (bot > maxBot) bot = maxBot; + if (top < 0) top = 0; + for (var i = vranges.length - 2; i >= 0; i -= 2) { + var rtop = vranges[i], rbot = vranges[i+1]; + if (rtop > bot || rbot < top) continue; + if (rtop <= top && rbot >= bot || + top <= rtop && bot >= rbot || + Math.min(bot, rbot) - Math.max(top, rtop) >= (bot - top) >> 1) { + vranges[i] = Math.min(top, rtop); + vranges[i+1] = Math.max(bot, rbot); + break; + } + } + if (i < 0) { i = vranges.length; vranges.push(top, bot); } + return {left: rect.left - outer.left, + right: rect.right - outer.left, + top: i, bottom: null}; + } + function finishRect(rect) { + rect.bottom = vranges[rect.top+1]; + rect.top = vranges[rect.top]; + } + + for (var i = 0, cur; i < measure.length; ++i) if (cur = measure[i]) { + var node = cur, rect = null; + // A widget might wrap, needs special care + if (/\bCodeMirror-widget\b/.test(cur.className) && cur.getClientRects) { + if (cur.firstChild.nodeType == 1) node = cur.firstChild; + var rects = node.getClientRects(); + if (rects.length > 1) { + rect = data[i] = measureRect(rects[0]); + rect.rightSide = measureRect(rects[rects.length - 1]); + } + } + if (!rect) rect = data[i] = measureRect(getRect(node)); + if (cur.measureRight) rect.right = getRect(cur.measureRight).left - outer.left; + if (cur.leftSide) rect.leftSide = measureRect(getRect(cur.leftSide)); + } + removeChildren(cm.display.measure); + for (var i = 0, cur; i < data.length; ++i) if (cur = data[i]) { + finishRect(cur); + if (cur.leftSide) finishRect(cur.leftSide); + if (cur.rightSide) finishRect(cur.rightSide); + } + return data; + } + + function crudelyMeasureLine(cm, line) { + var copy = new Line(line.text.slice(0, 100), null); + if (line.textClass) copy.textClass = line.textClass; + var measure = measureLineInner(cm, copy); + var left = measureChar(cm, copy, 0, measure, "left"); + var right = measureChar(cm, copy, 99, measure, "right"); + return {crude: true, top: left.top, left: left.left, bottom: left.bottom, width: (right.right - left.left) / 100}; + } + + function measureLineWidth(cm, line) { + var hasBadSpan = false; + if (line.markedSpans) for (var i = 0; i < line.markedSpans; ++i) { + var sp = line.markedSpans[i]; + if (sp.collapsed && (sp.to == null || sp.to == line.text.length)) hasBadSpan = true; + } + var cached = !hasBadSpan && findCachedMeasurement(cm, line); + if (cached || line.text.length >= cm.options.crudeMeasuringFrom) + return measureChar(cm, line, line.text.length, cached && cached.measure, "right").right; + + var pre = buildLineContent(cm, line, null, true).pre; + var end = pre.appendChild(zeroWidthElement(cm.display.measure)); + removeChildrenAndAdd(cm.display.measure, pre); + return getRect(end).right - getRect(cm.display.lineDiv).left; + } + + function clearCaches(cm) { + cm.display.measureLineCache.length = cm.display.measureLineCachePos = 0; + cm.display.cachedCharWidth = cm.display.cachedTextHeight = null; + if (!cm.options.lineWrapping) cm.display.maxLineChanged = true; + cm.display.lineNumChars = null; + } + + function pageScrollX() { return window.pageXOffset || (document.documentElement || document.body).scrollLeft; } + function pageScrollY() { return window.pageYOffset || (document.documentElement || document.body).scrollTop; } + + // Context is one of "line", "div" (display.lineDiv), "local"/null (editor), or "page" + function intoCoordSystem(cm, lineObj, rect, context) { + if (lineObj.widgets) for (var i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) { + var size = widgetHeight(lineObj.widgets[i]); + rect.top += size; rect.bottom += size; + } + if (context == "line") return rect; + if (!context) context = "local"; + var yOff = heightAtLine(cm, lineObj); + if (context == "local") yOff += paddingTop(cm.display); + else yOff -= cm.display.viewOffset; + if (context == "page" || context == "window") { + var lOff = getRect(cm.display.lineSpace); + yOff += lOff.top + (context == "window" ? 0 : pageScrollY()); + var xOff = lOff.left + (context == "window" ? 0 : pageScrollX()); + rect.left += xOff; rect.right += xOff; + } + rect.top += yOff; rect.bottom += yOff; + return rect; + } + + // Context may be "window", "page", "div", or "local"/null + // Result is in "div" coords + function fromCoordSystem(cm, coords, context) { + if (context == "div") return coords; + var left = coords.left, top = coords.top; + // First move into "page" coordinate system + if (context == "page") { + left -= pageScrollX(); + top -= pageScrollY(); + } else if (context == "local" || !context) { + var localBox = getRect(cm.display.sizer); + left += localBox.left; + top += localBox.top; + } + + var lineSpaceBox = getRect(cm.display.lineSpace); + return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top}; + } + + function charCoords(cm, pos, context, lineObj, bias) { + if (!lineObj) lineObj = getLine(cm.doc, pos.line); + return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, null, bias), context); + } + + function cursorCoords(cm, pos, context, lineObj, measurement) { + lineObj = lineObj || getLine(cm.doc, pos.line); + if (!measurement) measurement = measureLine(cm, lineObj); + function get(ch, right) { + var m = measureChar(cm, lineObj, ch, measurement, right ? "right" : "left"); + if (right) m.left = m.right; else m.right = m.left; + return intoCoordSystem(cm, lineObj, m, context); + } + function getBidi(ch, partPos) { + var part = order[partPos], right = part.level % 2; + if (ch == bidiLeft(part) && partPos && part.level < order[partPos - 1].level) { + part = order[--partPos]; + ch = bidiRight(part) - (part.level % 2 ? 0 : 1); + right = true; + } else if (ch == bidiRight(part) && partPos < order.length - 1 && part.level < order[partPos + 1].level) { + part = order[++partPos]; + ch = bidiLeft(part) - part.level % 2; + right = false; + } + if (right && ch == part.to && ch > part.from) return get(ch - 1); + return get(ch, right); + } + var order = getOrder(lineObj), ch = pos.ch; + if (!order) return get(ch); + var partPos = getBidiPartAt(order, ch); + var val = getBidi(ch, partPos); + if (bidiOther != null) val.other = getBidi(ch, bidiOther); + return val; + } + + function PosWithInfo(line, ch, outside, xRel) { + var pos = new Pos(line, ch); + pos.xRel = xRel; + if (outside) pos.outside = true; + return pos; + } + + // Coords must be lineSpace-local + function coordsChar(cm, x, y) { + var doc = cm.doc; + y += cm.display.viewOffset; + if (y < 0) return PosWithInfo(doc.first, 0, true, -1); + var lineNo = lineAtHeight(doc, y), last = doc.first + doc.size - 1; + if (lineNo > last) + return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, true, 1); + if (x < 0) x = 0; + + for (;;) { + var lineObj = getLine(doc, lineNo); + var found = coordsCharInner(cm, lineObj, lineNo, x, y); + var merged = collapsedSpanAtEnd(lineObj); + var mergedPos = merged && merged.find(); + if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) + lineNo = mergedPos.to.line; + else + return found; + } + } + + function coordsCharInner(cm, lineObj, lineNo, x, y) { + var innerOff = y - heightAtLine(cm, lineObj); + var wrongLine = false, adjust = 2 * cm.display.wrapper.clientWidth; + var measurement = measureLine(cm, lineObj); + + function getX(ch) { + var sp = cursorCoords(cm, Pos(lineNo, ch), "line", + lineObj, measurement); + wrongLine = true; + if (innerOff > sp.bottom) return sp.left - adjust; + else if (innerOff < sp.top) return sp.left + adjust; + else wrongLine = false; + return sp.left; + } + + var bidi = getOrder(lineObj), dist = lineObj.text.length; + var from = lineLeft(lineObj), to = lineRight(lineObj); + var fromX = getX(from), fromOutside = wrongLine, toX = getX(to), toOutside = wrongLine; + + if (x > toX) return PosWithInfo(lineNo, to, toOutside, 1); + // Do a binary search between these bounds. + for (;;) { + if (bidi ? to == from || to == moveVisually(lineObj, from, 1) : to - from <= 1) { + var ch = x < fromX || x - fromX <= toX - x ? from : to; + var xDiff = x - (ch == from ? fromX : toX); + while (isExtendingChar(lineObj.text.charAt(ch))) ++ch; + var pos = PosWithInfo(lineNo, ch, ch == from ? fromOutside : toOutside, + xDiff < 0 ? -1 : xDiff ? 1 : 0); + return pos; + } + var step = Math.ceil(dist / 2), middle = from + step; + if (bidi) { + middle = from; + for (var i = 0; i < step; ++i) middle = moveVisually(lineObj, middle, 1); + } + var middleX = getX(middle); + if (middleX > x) {to = middle; toX = middleX; if (toOutside = wrongLine) toX += 1000; dist = step;} + else {from = middle; fromX = middleX; fromOutside = wrongLine; dist -= step;} + } + } + + var measureText; + function textHeight(display) { + if (display.cachedTextHeight != null) return display.cachedTextHeight; + if (measureText == null) { + measureText = elt("pre"); + // Measure a bunch of lines, for browsers that compute + // fractional heights. + for (var i = 0; i < 49; ++i) { + measureText.appendChild(document.createTextNode("x")); + measureText.appendChild(elt("br")); + } + measureText.appendChild(document.createTextNode("x")); + } + removeChildrenAndAdd(display.measure, measureText); + var height = measureText.offsetHeight / 50; + if (height > 3) display.cachedTextHeight = height; + removeChildren(display.measure); + return height || 1; + } + + function charWidth(display) { + if (display.cachedCharWidth != null) return display.cachedCharWidth; + var anchor = elt("span", "x"); + var pre = elt("pre", [anchor]); + removeChildrenAndAdd(display.measure, pre); + var width = anchor.offsetWidth; + if (width > 2) display.cachedCharWidth = width; + return width || 10; + } + + // OPERATIONS + + // Operations are used to wrap changes in such a way that each + // change won't have to update the cursor and display (which would + // be awkward, slow, and error-prone), but instead updates are + // batched and then all combined and executed at once. + + var nextOpId = 0; + function startOperation(cm) { + cm.curOp = { + // An array of ranges of lines that have to be updated. See + // updateDisplay. + changes: [], + forceUpdate: false, + updateInput: null, + userSelChange: null, + textChanged: null, + selectionChanged: false, + cursorActivity: false, + updateMaxLine: false, + updateScrollPos: false, + id: ++nextOpId + }; + if (!delayedCallbackDepth++) delayedCallbacks = []; + } + + function endOperation(cm) { + var op = cm.curOp, doc = cm.doc, display = cm.display; + cm.curOp = null; + + if (op.updateMaxLine) computeMaxLength(cm); + if (display.maxLineChanged && !cm.options.lineWrapping && display.maxLine) { + var width = measureLineWidth(cm, display.maxLine); + display.sizer.style.minWidth = Math.max(0, width + 3 + scrollerCutOff) + "px"; + display.maxLineChanged = false; + var maxScrollLeft = Math.max(0, display.sizer.offsetLeft + display.sizer.offsetWidth - display.scroller.clientWidth); + if (maxScrollLeft < doc.scrollLeft && !op.updateScrollPos) + setScrollLeft(cm, Math.min(display.scroller.scrollLeft, maxScrollLeft), true); + } + var newScrollPos, updated; + if (op.updateScrollPos) { + newScrollPos = op.updateScrollPos; + } else if (op.selectionChanged && display.scroller.clientHeight) { // don't rescroll if not visible + var coords = cursorCoords(cm, doc.sel.head); + newScrollPos = calculateScrollPos(cm, coords.left, coords.top, coords.left, coords.bottom); + } + if (op.changes.length || op.forceUpdate || newScrollPos && newScrollPos.scrollTop != null) { + updated = updateDisplay(cm, op.changes, newScrollPos && newScrollPos.scrollTop, op.forceUpdate); + if (cm.display.scroller.offsetHeight) cm.doc.scrollTop = cm.display.scroller.scrollTop; + } + if (!updated && op.selectionChanged) updateSelection(cm); + if (op.updateScrollPos) { + var top = Math.max(0, Math.min(display.scroller.scrollHeight - display.scroller.clientHeight, newScrollPos.scrollTop)); + var left = Math.max(0, Math.min(display.scroller.scrollWidth - display.scroller.clientWidth, newScrollPos.scrollLeft)); + display.scroller.scrollTop = display.scrollbarV.scrollTop = doc.scrollTop = top; + display.scroller.scrollLeft = display.scrollbarH.scrollLeft = doc.scrollLeft = left; + alignHorizontally(cm); + if (op.scrollToPos) + scrollPosIntoView(cm, clipPos(cm.doc, op.scrollToPos.from), + clipPos(cm.doc, op.scrollToPos.to), op.scrollToPos.margin); + } else if (newScrollPos) { + scrollCursorIntoView(cm); + } + if (op.selectionChanged) restartBlink(cm); + + if (cm.state.focused && op.updateInput) + resetInput(cm, op.userSelChange); + + var hidden = op.maybeHiddenMarkers, unhidden = op.maybeUnhiddenMarkers; + if (hidden) for (var i = 0; i < hidden.length; ++i) + if (!hidden[i].lines.length) signal(hidden[i], "hide"); + if (unhidden) for (var i = 0; i < unhidden.length; ++i) + if (unhidden[i].lines.length) signal(unhidden[i], "unhide"); + + var delayed; + if (!--delayedCallbackDepth) { + delayed = delayedCallbacks; + delayedCallbacks = null; + } + if (op.textChanged) + signal(cm, "change", cm, op.textChanged); + if (op.cursorActivity) signal(cm, "cursorActivity", cm); + if (delayed) for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + // Wraps a function in an operation. Returns the wrapped function. + function operation(cm1, f) { + return function() { + var cm = cm1 || this, withOp = !cm.curOp; + if (withOp) startOperation(cm); + try { var result = f.apply(cm, arguments); } + finally { if (withOp) endOperation(cm); } + return result; + }; + } + function docOperation(f) { + return function() { + var withOp = this.cm && !this.cm.curOp, result; + if (withOp) startOperation(this.cm); + try { result = f.apply(this, arguments); } + finally { if (withOp) endOperation(this.cm); } + return result; + }; + } + function runInOp(cm, f) { + var withOp = !cm.curOp, result; + if (withOp) startOperation(cm); + try { result = f(); } + finally { if (withOp) endOperation(cm); } + return result; + } + + function regChange(cm, from, to, lendiff) { + if (from == null) from = cm.doc.first; + if (to == null) to = cm.doc.first + cm.doc.size; + cm.curOp.changes.push({from: from, to: to, diff: lendiff}); + } + + // INPUT HANDLING + + function slowPoll(cm) { + if (cm.display.pollingFast) return; + cm.display.poll.set(cm.options.pollInterval, function() { + readInput(cm); + if (cm.state.focused) slowPoll(cm); + }); + } + + function fastPoll(cm) { + var missed = false; + cm.display.pollingFast = true; + function p() { + var changed = readInput(cm); + if (!changed && !missed) {missed = true; cm.display.poll.set(60, p);} + else {cm.display.pollingFast = false; slowPoll(cm);} + } + cm.display.poll.set(20, p); + } + + // prevInput is a hack to work with IME. If we reset the textarea + // on every change, that breaks IME. So we look for changes + // compared to the previous content instead. (Modern browsers have + // events that indicate IME taking place, but these are not widely + // supported or compatible enough yet to rely on.) + function readInput(cm) { + var input = cm.display.input, prevInput = cm.display.prevInput, doc = cm.doc, sel = doc.sel; + if (!cm.state.focused || hasSelection(input) || isReadOnly(cm) || cm.options.disableInput) return false; + if (cm.state.pasteIncoming && cm.state.fakedLastChar) { + input.value = input.value.substring(0, input.value.length - 1); + cm.state.fakedLastChar = false; + } + var text = input.value; + if (text == prevInput && posEq(sel.from, sel.to)) return false; + if (ie && !ie_lt9 && cm.display.inputHasSelection === text) { + resetInput(cm, true); + return false; + } + + var withOp = !cm.curOp; + if (withOp) startOperation(cm); + sel.shift = false; + var same = 0, l = Math.min(prevInput.length, text.length); + while (same < l && prevInput.charCodeAt(same) == text.charCodeAt(same)) ++same; + var from = sel.from, to = sel.to; + var inserted = text.slice(same); + if (same < prevInput.length) + from = Pos(from.line, from.ch - (prevInput.length - same)); + else if (cm.state.overwrite && posEq(from, to) && !cm.state.pasteIncoming) + to = Pos(to.line, Math.min(getLine(doc, to.line).text.length, to.ch + inserted.length)); + + var updateInput = cm.curOp.updateInput; + var changeEvent = {from: from, to: to, text: splitLines(inserted), + origin: cm.state.pasteIncoming ? "paste" : cm.state.cutIncoming ? "cut" : "+input"}; + makeChange(cm.doc, changeEvent, "end"); + cm.curOp.updateInput = updateInput; + signalLater(cm, "inputRead", cm, changeEvent); + if (inserted && !cm.state.pasteIncoming && cm.options.electricChars && + cm.options.smartIndent && sel.head.ch < 100) { + var electric = cm.getModeAt(sel.head).electricChars; + if (electric) for (var i = 0; i < electric.length; i++) + if (inserted.indexOf(electric.charAt(i)) > -1) { + indentLine(cm, sel.head.line, "smart"); + break; + } + } + + if (text.length > 1000 || text.indexOf("\n") > -1) input.value = cm.display.prevInput = ""; + else cm.display.prevInput = text; + if (withOp) endOperation(cm); + cm.state.pasteIncoming = cm.state.cutIncoming = false; + return true; + } + + function resetInput(cm, user) { + var minimal, selected, doc = cm.doc; + if (!posEq(doc.sel.from, doc.sel.to)) { + cm.display.prevInput = ""; + minimal = hasCopyEvent && + (doc.sel.to.line - doc.sel.from.line > 100 || (selected = cm.getSelection()).length > 1000); + var content = minimal ? "-" : selected || cm.getSelection(); + cm.display.input.value = content; + if (cm.state.focused) selectInput(cm.display.input); + if (ie && !ie_lt9) cm.display.inputHasSelection = content; + } else if (user) { + cm.display.prevInput = cm.display.input.value = ""; + if (ie && !ie_lt9) cm.display.inputHasSelection = null; + } + cm.display.inaccurateSelection = minimal; + } + + function focusInput(cm) { + if (cm.options.readOnly != "nocursor" && (!mobile || document.activeElement != cm.display.input)) + cm.display.input.focus(); + } + + function isReadOnly(cm) { + return cm.options.readOnly || cm.doc.cantEdit; + } + + // EVENT HANDLERS + + function registerEventHandlers(cm) { + var d = cm.display; + on(d.scroller, "mousedown", operation(cm, onMouseDown)); + if (old_ie) + on(d.scroller, "dblclick", operation(cm, function(e) { + if (signalDOMEvent(cm, e)) return; + var pos = posFromMouse(cm, e); + if (!pos || clickInGutter(cm, e) || eventInWidget(cm.display, e)) return; + e_preventDefault(e); + var word = findWordAt(getLine(cm.doc, pos.line).text, pos); + extendSelection(cm.doc, word.from, word.to); + })); + else + on(d.scroller, "dblclick", function(e) { signalDOMEvent(cm, e) || e_preventDefault(e); }); + on(d.lineSpace, "selectstart", function(e) { + if (!eventInWidget(d, e)) e_preventDefault(e); + }); + // Gecko browsers fire contextmenu *after* opening the menu, at + // which point we can't mess with it anymore. Context menu is + // handled in onMouseDown for Gecko. + if (!captureMiddleClick) on(d.scroller, "contextmenu", function(e) {onContextMenu(cm, e);}); + + on(d.scroller, "scroll", function() { + if (d.scroller.clientHeight) { + setScrollTop(cm, d.scroller.scrollTop); + setScrollLeft(cm, d.scroller.scrollLeft, true); + signal(cm, "scroll", cm); + } + }); + on(d.scrollbarV, "scroll", function() { + if (d.scroller.clientHeight) setScrollTop(cm, d.scrollbarV.scrollTop); + }); + on(d.scrollbarH, "scroll", function() { + if (d.scroller.clientHeight) setScrollLeft(cm, d.scrollbarH.scrollLeft); + }); + + on(d.scroller, "mousewheel", function(e){onScrollWheel(cm, e);}); + on(d.scroller, "DOMMouseScroll", function(e){onScrollWheel(cm, e);}); + + function reFocus() { if (cm.state.focused) setTimeout(bind(focusInput, cm), 0); } + on(d.scrollbarH, "mousedown", reFocus); + on(d.scrollbarV, "mousedown", reFocus); + // Prevent wrapper from ever scrolling + on(d.wrapper, "scroll", function() { d.wrapper.scrollTop = d.wrapper.scrollLeft = 0; }); + + var resizeTimer; + function onResize() { + if (resizeTimer == null) resizeTimer = setTimeout(function() { + resizeTimer = null; + // Might be a text scaling operation, clear size caches. + d.cachedCharWidth = d.cachedTextHeight = knownScrollbarWidth = null; + clearCaches(cm); + runInOp(cm, bind(regChange, cm)); + }, 100); + } + on(window, "resize", onResize); + // Above handler holds on to the editor and its data structures. + // Here we poll to unregister it when the editor is no longer in + // the document, so that it can be garbage-collected. + function unregister() { + for (var p = d.wrapper.parentNode; p && p != document.body; p = p.parentNode) {} + if (p) setTimeout(unregister, 5000); + else off(window, "resize", onResize); + } + setTimeout(unregister, 5000); + + on(d.input, "keyup", operation(cm, function(e) { + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (e.keyCode == 16) cm.doc.sel.shift = false; + })); + on(d.input, "input", function() { + if (ie && !ie_lt9 && cm.display.inputHasSelection) cm.display.inputHasSelection = null; + fastPoll(cm); + }); + on(d.input, "keydown", operation(cm, onKeyDown)); + on(d.input, "keypress", operation(cm, onKeyPress)); + on(d.input, "focus", bind(onFocus, cm)); + on(d.input, "blur", bind(onBlur, cm)); + + function drag_(e) { + if (signalDOMEvent(cm, e) || cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e))) return; + e_stop(e); + } + if (cm.options.dragDrop) { + on(d.scroller, "dragstart", function(e){onDragStart(cm, e);}); + on(d.scroller, "dragenter", drag_); + on(d.scroller, "dragover", drag_); + on(d.scroller, "drop", operation(cm, onDrop)); + } + on(d.scroller, "paste", function(e) { + if (eventInWidget(d, e)) return; + focusInput(cm); + fastPoll(cm); + }); + on(d.input, "paste", function() { + // Workaround for webkit bug https://bugs.webkit.org/show_bug.cgi?id=90206 + // Add a char to the end of textarea before paste occur so that + // selection doesn't span to the end of textarea. + if (webkit && !cm.state.fakedLastChar && !(new Date - cm.state.lastMiddleDown < 200)) { + var start = d.input.selectionStart, end = d.input.selectionEnd; + d.input.value += "$"; + d.input.selectionStart = start; + d.input.selectionEnd = end; + cm.state.fakedLastChar = true; + } + cm.state.pasteIncoming = true; + fastPoll(cm); + }); + + function prepareCopy(e) { + if (d.inaccurateSelection) { + d.prevInput = ""; + d.inaccurateSelection = false; + d.input.value = cm.getSelection(); + selectInput(d.input); + } + if (e.type == "cut") cm.state.cutIncoming = true; + } + on(d.input, "cut", prepareCopy); + on(d.input, "copy", prepareCopy); + + // Needed to handle Tab key in KHTML + if (khtml) on(d.sizer, "mouseup", function() { + if (document.activeElement == d.input) d.input.blur(); + focusInput(cm); + }); + } + + function eventInWidget(display, e) { + for (var n = e_target(e); n != display.wrapper; n = n.parentNode) { + if (!n || n.ignoreEvents || n.parentNode == display.sizer && n != display.mover) return true; + } + } + + function posFromMouse(cm, e, liberal) { + var display = cm.display; + if (!liberal) { + var target = e_target(e); + if (target == display.scrollbarH || target == display.scrollbarH.firstChild || + target == display.scrollbarV || target == display.scrollbarV.firstChild || + target == display.scrollbarFiller || target == display.gutterFiller) return null; + } + var x, y, space = getRect(display.lineSpace); + // Fails unpredictably on IE[67] when mouse is dragged around quickly. + try { x = e.clientX; y = e.clientY; } catch (e) { return null; } + return coordsChar(cm, x - space.left, y - space.top); + } + + var lastClick, lastDoubleClick; + function onMouseDown(e) { + if (signalDOMEvent(this, e)) return; + var cm = this, display = cm.display, doc = cm.doc, sel = doc.sel; + sel.shift = e.shiftKey; + + if (eventInWidget(display, e)) { + if (!webkit) { + display.scroller.draggable = false; + setTimeout(function(){display.scroller.draggable = true;}, 100); + } + return; + } + if (clickInGutter(cm, e)) return; + var start = posFromMouse(cm, e); + + switch (e_button(e)) { + case 3: + if (captureMiddleClick) onContextMenu.call(cm, cm, e); + return; + case 2: + if (webkit) cm.state.lastMiddleDown = +new Date; + if (start) extendSelection(cm.doc, start); + setTimeout(bind(focusInput, cm), 20); + e_preventDefault(e); + return; + } + // For button 1, if it was clicked inside the editor + // (posFromMouse returning non-null), we have to adjust the + // selection. + if (!start) {if (e_target(e) == display.scroller) e_preventDefault(e); return;} + + if (!cm.state.focused) onFocus(cm); + + var now = +new Date, type = "single"; + if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) { + type = "triple"; + e_preventDefault(e); + setTimeout(bind(focusInput, cm), 20); + selectLine(cm, start.line); + } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) { + type = "double"; + lastDoubleClick = {time: now, pos: start}; + e_preventDefault(e); + var word = findWordAt(getLine(doc, start.line).text, start); + extendSelection(cm.doc, word.from, word.to); + } else { lastClick = {time: now, pos: start}; } + + var last = start; + if (cm.options.dragDrop && dragAndDrop && !isReadOnly(cm) && !posEq(sel.from, sel.to) && + !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") { + var dragEnd = operation(cm, function(e2) { + if (webkit) display.scroller.draggable = false; + cm.state.draggingText = false; + off(document, "mouseup", dragEnd); + off(display.scroller, "drop", dragEnd); + if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) { + e_preventDefault(e2); + extendSelection(cm.doc, start); + focusInput(cm); + // Work around unexplainable focus problem in IE9 (#2127) + if (old_ie && !ie_lt9) + setTimeout(function() {document.body.focus(); focusInput(cm);}, 20); + } + }); + // Let the drag handler handle this. + if (webkit) display.scroller.draggable = true; + cm.state.draggingText = dragEnd; + // IE's approach to draggable + if (display.scroller.dragDrop) display.scroller.dragDrop(); + on(document, "mouseup", dragEnd); + on(display.scroller, "drop", dragEnd); + return; + } + e_preventDefault(e); + if (type == "single") extendSelection(cm.doc, clipPos(doc, start)); + + var startstart = sel.from, startend = sel.to, lastPos = start; + + function doSelect(cur) { + if (posEq(lastPos, cur)) return; + lastPos = cur; + + if (type == "single") { + extendSelection(cm.doc, clipPos(doc, start), cur); + return; + } + + startstart = clipPos(doc, startstart); + startend = clipPos(doc, startend); + if (type == "double") { + var word = findWordAt(getLine(doc, cur.line).text, cur); + if (posLess(cur, startstart)) extendSelection(cm.doc, word.from, startend); + else extendSelection(cm.doc, startstart, word.to); + } else if (type == "triple") { + if (posLess(cur, startstart)) extendSelection(cm.doc, startend, clipPos(doc, Pos(cur.line, 0))); + else extendSelection(cm.doc, startstart, clipPos(doc, Pos(cur.line + 1, 0))); + } + } + + var editorSize = getRect(display.wrapper); + // Used to ensure timeout re-tries don't fire when another extend + // happened in the meantime (clearTimeout isn't reliable -- at + // least on Chrome, the timeouts still happen even when cleared, + // if the clear happens after their scheduled firing time). + var counter = 0; + + function extend(e) { + var curCount = ++counter; + var cur = posFromMouse(cm, e, true); + if (!cur) return; + if (!posEq(cur, last)) { + if (!cm.state.focused) onFocus(cm); + last = cur; + doSelect(cur); + var visible = visibleLines(display, doc); + if (cur.line >= visible.to || cur.line < visible.from) + setTimeout(operation(cm, function(){if (counter == curCount) extend(e);}), 150); + } else { + var outside = e.clientY < editorSize.top ? -20 : e.clientY > editorSize.bottom ? 20 : 0; + if (outside) setTimeout(operation(cm, function() { + if (counter != curCount) return; + display.scroller.scrollTop += outside; + extend(e); + }), 50); + } + } + + function done(e) { + counter = Infinity; + e_preventDefault(e); + focusInput(cm); + off(document, "mousemove", move); + off(document, "mouseup", up); + } + + var move = operation(cm, function(e) { + if (!old_ie && !e_button(e)) done(e); + else extend(e); + }); + var up = operation(cm, done); + on(document, "mousemove", move); + on(document, "mouseup", up); + } + + function gutterEvent(cm, e, type, prevent, signalfn) { + try { var mX = e.clientX, mY = e.clientY; } + catch(e) { return false; } + if (mX >= Math.floor(getRect(cm.display.gutters).right)) return false; + if (prevent) e_preventDefault(e); + + var display = cm.display; + var lineBox = getRect(display.lineDiv); + + if (mY > lineBox.bottom || !hasHandler(cm, type)) return e_defaultPrevented(e); + mY -= lineBox.top - display.viewOffset; + + for (var i = 0; i < cm.options.gutters.length; ++i) { + var g = display.gutters.childNodes[i]; + if (g && getRect(g).right >= mX) { + var line = lineAtHeight(cm.doc, mY); + var gutter = cm.options.gutters[i]; + signalfn(cm, type, cm, line, gutter, e); + return e_defaultPrevented(e); + } + } + } + + function contextMenuInGutter(cm, e) { + if (!hasHandler(cm, "gutterContextMenu")) return false; + return gutterEvent(cm, e, "gutterContextMenu", false, signal); + } + + function clickInGutter(cm, e) { + return gutterEvent(cm, e, "gutterClick", true, signalLater); + } + + // Kludge to work around strange IE behavior where it'll sometimes + // re-fire a series of drag-related events right after the drop (#1551) + var lastDrop = 0; + + function onDrop(e) { + var cm = this; + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e) || (cm.options.onDragEvent && cm.options.onDragEvent(cm, addStop(e)))) + return; + e_preventDefault(e); + if (ie) lastDrop = +new Date; + var pos = posFromMouse(cm, e, true), files = e.dataTransfer.files; + if (!pos || isReadOnly(cm)) return; + if (files && files.length && window.FileReader && window.File) { + var n = files.length, text = Array(n), read = 0; + var loadFile = function(file, i) { + var reader = new FileReader; + reader.onload = function() { + text[i] = reader.result; + if (++read == n) { + pos = clipPos(cm.doc, pos); + makeChange(cm.doc, {from: pos, to: pos, text: splitLines(text.join("\n")), origin: "paste"}, "around"); + } + }; + reader.readAsText(file); + }; + for (var i = 0; i < n; ++i) loadFile(files[i], i); + } else { + // Don't do a replace if the drop happened inside of the selected text. + if (cm.state.draggingText && !(posLess(pos, cm.doc.sel.from) || posLess(cm.doc.sel.to, pos))) { + cm.state.draggingText(e); + // Ensure the editor is re-focused + setTimeout(bind(focusInput, cm), 20); + return; + } + try { + var text = e.dataTransfer.getData("Text"); + if (text) { + var curFrom = cm.doc.sel.from, curTo = cm.doc.sel.to; + setSelection(cm.doc, pos, pos); + if (cm.state.draggingText) replaceRange(cm.doc, "", curFrom, curTo, "paste"); + cm.replaceSelection(text, null, "paste"); + focusInput(cm); + } + } + catch(e){} + } + } + + function onDragStart(cm, e) { + if (ie && (!cm.state.draggingText || +new Date - lastDrop < 100)) { e_stop(e); return; } + if (signalDOMEvent(cm, e) || eventInWidget(cm.display, e)) return; + + var txt = cm.getSelection(); + e.dataTransfer.setData("Text", txt); + + // Use dummy image instead of default browsers image. + // Recent Safari (~6.0.2) have a tendency to segfault when this happens, so we don't do it there. + if (e.dataTransfer.setDragImage && !safari) { + var img = elt("img", null, null, "position: fixed; left: 0; top: 0;"); + img.src = "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="; + if (opera) { + img.width = img.height = 1; + cm.display.wrapper.appendChild(img); + // Force a relayout, or Opera won't use our image for some obscure reason + img._top = img.offsetTop; + } + e.dataTransfer.setDragImage(img, 0, 0); + if (opera) img.parentNode.removeChild(img); + } + } + + function setScrollTop(cm, val) { + if (Math.abs(cm.doc.scrollTop - val) < 2) return; + cm.doc.scrollTop = val; + if (!gecko) updateDisplay(cm, [], val); + if (cm.display.scroller.scrollTop != val) cm.display.scroller.scrollTop = val; + if (cm.display.scrollbarV.scrollTop != val) cm.display.scrollbarV.scrollTop = val; + if (gecko) updateDisplay(cm, []); + startWorker(cm, 100); + } + function setScrollLeft(cm, val, isScroller) { + if (isScroller ? val == cm.doc.scrollLeft : Math.abs(cm.doc.scrollLeft - val) < 2) return; + val = Math.min(val, cm.display.scroller.scrollWidth - cm.display.scroller.clientWidth); + cm.doc.scrollLeft = val; + alignHorizontally(cm); + if (cm.display.scroller.scrollLeft != val) cm.display.scroller.scrollLeft = val; + if (cm.display.scrollbarH.scrollLeft != val) cm.display.scrollbarH.scrollLeft = val; + } + + // Since the delta values reported on mouse wheel events are + // unstandardized between browsers and even browser versions, and + // generally horribly unpredictable, this code starts by measuring + // the scroll effect that the first few mouse wheel events have, + // and, from that, detects the way it can convert deltas to pixel + // offsets afterwards. + // + // The reason we want to know the amount a wheel event will scroll + // is that it gives us a chance to update the display before the + // actual scrolling happens, reducing flickering. + + var wheelSamples = 0, wheelPixelsPerUnit = null; + // Fill in a browser-detected starting value on browsers where we + // know one. These don't have to be accurate -- the result of them + // being wrong would just be a slight flicker on the first wheel + // scroll (if it is large enough). + if (old_ie) wheelPixelsPerUnit = -.53; + else if (gecko) wheelPixelsPerUnit = 15; + else if (chrome) wheelPixelsPerUnit = -.7; + else if (safari) wheelPixelsPerUnit = -1/3; + + function onScrollWheel(cm, e) { + var dx = e.wheelDeltaX, dy = e.wheelDeltaY; + if (dx == null && e.detail && e.axis == e.HORIZONTAL_AXIS) dx = e.detail; + if (dy == null && e.detail && e.axis == e.VERTICAL_AXIS) dy = e.detail; + else if (dy == null) dy = e.wheelDelta; + + var display = cm.display, scroll = display.scroller; + // Quit if there's nothing to scroll here + if (!(dx && scroll.scrollWidth > scroll.clientWidth || + dy && scroll.scrollHeight > scroll.clientHeight)) return; + + // Webkit browsers on OS X abort momentum scrolls when the target + // of the scroll event is removed from the scrollable element. + // This hack (see related code in patchDisplay) makes sure the + // element is kept around. + if (dy && mac && webkit) { + for (var cur = e.target; cur != scroll; cur = cur.parentNode) { + if (cur.lineObj) { + cm.display.currentWheelTarget = cur; + break; + } + } + } + + // On some browsers, horizontal scrolling will cause redraws to + // happen before the gutter has been realigned, causing it to + // wriggle around in a most unseemly way. When we have an + // estimated pixels/delta value, we just handle horizontal + // scrolling entirely here. It'll be slightly off from native, but + // better than glitching out. + if (dx && !gecko && !opera && wheelPixelsPerUnit != null) { + if (dy) + setScrollTop(cm, Math.max(0, Math.min(scroll.scrollTop + dy * wheelPixelsPerUnit, scroll.scrollHeight - scroll.clientHeight))); + setScrollLeft(cm, Math.max(0, Math.min(scroll.scrollLeft + dx * wheelPixelsPerUnit, scroll.scrollWidth - scroll.clientWidth))); + e_preventDefault(e); + display.wheelStartX = null; // Abort measurement, if in progress + return; + } + + if (dy && wheelPixelsPerUnit != null) { + var pixels = dy * wheelPixelsPerUnit; + var top = cm.doc.scrollTop, bot = top + display.wrapper.clientHeight; + if (pixels < 0) top = Math.max(0, top + pixels - 50); + else bot = Math.min(cm.doc.height, bot + pixels + 50); + updateDisplay(cm, [], {top: top, bottom: bot}); + } + + if (wheelSamples < 20) { + if (display.wheelStartX == null) { + display.wheelStartX = scroll.scrollLeft; display.wheelStartY = scroll.scrollTop; + display.wheelDX = dx; display.wheelDY = dy; + setTimeout(function() { + if (display.wheelStartX == null) return; + var movedX = scroll.scrollLeft - display.wheelStartX; + var movedY = scroll.scrollTop - display.wheelStartY; + var sample = (movedY && display.wheelDY && movedY / display.wheelDY) || + (movedX && display.wheelDX && movedX / display.wheelDX); + display.wheelStartX = display.wheelStartY = null; + if (!sample) return; + wheelPixelsPerUnit = (wheelPixelsPerUnit * wheelSamples + sample) / (wheelSamples + 1); + ++wheelSamples; + }, 200); + } else { + display.wheelDX += dx; display.wheelDY += dy; + } + } + } + + function doHandleBinding(cm, bound, dropShift) { + if (typeof bound == "string") { + bound = commands[bound]; + if (!bound) return false; + } + // Ensure previous input has been read, so that the handler sees a + // consistent view of the document + if (cm.display.pollingFast && readInput(cm)) cm.display.pollingFast = false; + var doc = cm.doc, prevShift = doc.sel.shift, done = false; + try { + if (isReadOnly(cm)) cm.state.suppressEdits = true; + if (dropShift) doc.sel.shift = false; + done = bound(cm) != Pass; + } finally { + doc.sel.shift = prevShift; + cm.state.suppressEdits = false; + } + return done; + } + + function allKeyMaps(cm) { + var maps = cm.state.keyMaps.slice(0); + if (cm.options.extraKeys) maps.push(cm.options.extraKeys); + maps.push(cm.options.keyMap); + return maps; + } + + var maybeTransition; + function handleKeyBinding(cm, e) { + // Handle auto keymap transitions + var startMap = getKeyMap(cm.options.keyMap), next = startMap.auto; + clearTimeout(maybeTransition); + if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() { + if (getKeyMap(cm.options.keyMap) == startMap) { + cm.options.keyMap = (next.call ? next.call(null, cm) : next); + keyMapChanged(cm); + } + }, 50); + + var name = keyName(e, true), handled = false; + if (!name) return false; + var keymaps = allKeyMaps(cm); + + if (e.shiftKey) { + // First try to resolve full name (including 'Shift-'). Failing + // that, see if there is a cursor-motion command (starting with + // 'go') bound to the keyname without 'Shift-'. + handled = lookupKey("Shift-" + name, keymaps, function(b) {return doHandleBinding(cm, b, true);}) + || lookupKey(name, keymaps, function(b) { + if (typeof b == "string" ? /^go[A-Z]/.test(b) : b.motion) + return doHandleBinding(cm, b); + }); + } else { + handled = lookupKey(name, keymaps, function(b) { return doHandleBinding(cm, b); }); + } + + if (handled) { + e_preventDefault(e); + restartBlink(cm); + if (ie_lt9) { e.oldKeyCode = e.keyCode; e.keyCode = 0; } + signalLater(cm, "keyHandled", cm, name, e); + } + return handled; + } + + function handleCharBinding(cm, e, ch) { + var handled = lookupKey("'" + ch + "'", allKeyMaps(cm), + function(b) { return doHandleBinding(cm, b, true); }); + if (handled) { + e_preventDefault(e); + restartBlink(cm); + signalLater(cm, "keyHandled", cm, "'" + ch + "'", e); + } + return handled; + } + + var lastStoppedKey = null; + function onKeyDown(e) { + var cm = this; + if (!cm.state.focused) onFocus(cm); + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + if (old_ie && e.keyCode == 27) e.returnValue = false; + var code = e.keyCode; + // IE does strange things with escape. + cm.doc.sel.shift = code == 16 || e.shiftKey; + // First give onKeyEvent option a chance to handle this. + var handled = handleKeyBinding(cm, e); + if (opera) { + lastStoppedKey = handled ? code : null; + // Opera has no cut event... we try to at least catch the key combo + if (!handled && code == 88 && !hasCopyEvent && (mac ? e.metaKey : e.ctrlKey)) + cm.replaceSelection(""); + } + } + + function onKeyPress(e) { + var cm = this; + if (signalDOMEvent(cm, e) || cm.options.onKeyEvent && cm.options.onKeyEvent(cm, addStop(e))) return; + var keyCode = e.keyCode, charCode = e.charCode; + if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;} + if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(cm, e)) return; + var ch = String.fromCharCode(charCode == null ? keyCode : charCode); + if (handleCharBinding(cm, e, ch)) return; + if (ie && !ie_lt9) cm.display.inputHasSelection = null; + fastPoll(cm); + } + + function onFocus(cm) { + if (cm.options.readOnly == "nocursor") return; + if (!cm.state.focused) { + signal(cm, "focus", cm); + cm.state.focused = true; + if (cm.display.wrapper.className.search(/\bCodeMirror-focused\b/) == -1) + cm.display.wrapper.className += " CodeMirror-focused"; + if (!cm.curOp) { + resetInput(cm, true); + if (webkit) setTimeout(bind(resetInput, cm, true), 0); // Issue #1730 + } + } + slowPoll(cm); + restartBlink(cm); + } + function onBlur(cm) { + if (cm.state.focused) { + signal(cm, "blur", cm); + cm.state.focused = false; + cm.display.wrapper.className = cm.display.wrapper.className.replace(" CodeMirror-focused", ""); + } + clearInterval(cm.display.blinker); + setTimeout(function() {if (!cm.state.focused) cm.doc.sel.shift = false;}, 150); + } + + var detectingSelectAll; + function onContextMenu(cm, e) { + if (signalDOMEvent(cm, e, "contextmenu")) return; + var display = cm.display, sel = cm.doc.sel; + if (eventInWidget(display, e) || contextMenuInGutter(cm, e)) return; + + var pos = posFromMouse(cm, e), scrollPos = display.scroller.scrollTop; + if (!pos || opera) return; // Opera is difficult. + + // Reset the current text selection only if the click is done outside of the selection + // and 'resetSelectionOnContextMenu' option is true. + var reset = cm.options.resetSelectionOnContextMenu; + if (reset && (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))) + operation(cm, setSelection)(cm.doc, pos, pos); + + var oldCSS = display.input.style.cssText; + display.inputDiv.style.position = "absolute"; + display.input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) + + "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: transparent; outline: none;" + + "border-width: 0; outline: none; overflow: hidden; opacity: .05; -ms-opacity: .05; filter: alpha(opacity=5);"; + focusInput(cm); + resetInput(cm, true); + // Adds "Select all" to context menu in FF + if (posEq(sel.from, sel.to)) display.input.value = display.prevInput = " "; + + function prepareSelectAllHack() { + if (display.input.selectionStart != null) { + var extval = display.input.value = "\u200b" + (posEq(sel.from, sel.to) ? "" : display.input.value); + display.prevInput = "\u200b"; + display.input.selectionStart = 1; display.input.selectionEnd = extval.length; + } + } + function rehide() { + display.inputDiv.style.position = "relative"; + display.input.style.cssText = oldCSS; + if (ie_lt9) display.scrollbarV.scrollTop = display.scroller.scrollTop = scrollPos; + slowPoll(cm); + + // Try to detect the user choosing select-all + if (display.input.selectionStart != null) { + if (!old_ie || ie_lt9) prepareSelectAllHack(); + clearTimeout(detectingSelectAll); + var i = 0, poll = function(){ + if (display.prevInput == "\u200b" && display.input.selectionStart == 0) + operation(cm, commands.selectAll)(cm); + else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500); + else resetInput(cm); + }; + detectingSelectAll = setTimeout(poll, 200); + } + } + + if (old_ie && !ie_lt9) prepareSelectAllHack(); + if (captureMiddleClick) { + e_stop(e); + var mouseup = function() { + off(window, "mouseup", mouseup); + setTimeout(rehide, 20); + }; + on(window, "mouseup", mouseup); + } else { + setTimeout(rehide, 50); + } + } + + // UPDATING + + var changeEnd = CodeMirror.changeEnd = function(change) { + if (!change.text) return change.to; + return Pos(change.from.line + change.text.length - 1, + lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0)); + }; + + // Make sure a position will be valid after the given change. + function clipPostChange(doc, change, pos) { + if (!posLess(change.from, pos)) return clipPos(doc, pos); + var diff = (change.text.length - 1) - (change.to.line - change.from.line); + if (pos.line > change.to.line + diff) { + var preLine = pos.line - diff, lastLine = doc.first + doc.size - 1; + if (preLine > lastLine) return Pos(lastLine, getLine(doc, lastLine).text.length); + return clipToLen(pos, getLine(doc, preLine).text.length); + } + if (pos.line == change.to.line + diff) + return clipToLen(pos, lst(change.text).length + (change.text.length == 1 ? change.from.ch : 0) + + getLine(doc, change.to.line).text.length - change.to.ch); + var inside = pos.line - change.from.line; + return clipToLen(pos, change.text[inside].length + (inside ? 0 : change.from.ch)); + } + + // Hint can be null|"end"|"start"|"around"|{anchor,head} + function computeSelAfterChange(doc, change, hint) { + if (hint && typeof hint == "object") // Assumed to be {anchor, head} object + return {anchor: clipPostChange(doc, change, hint.anchor), + head: clipPostChange(doc, change, hint.head)}; + + if (hint == "start") return {anchor: change.from, head: change.from}; + + var end = changeEnd(change); + if (hint == "around") return {anchor: change.from, head: end}; + if (hint == "end") return {anchor: end, head: end}; + + // hint is null, leave the selection alone as much as possible + var adjustPos = function(pos) { + if (posLess(pos, change.from)) return pos; + if (!posLess(change.to, pos)) return end; + + var line = pos.line + change.text.length - (change.to.line - change.from.line) - 1, ch = pos.ch; + if (pos.line == change.to.line) ch += end.ch - change.to.ch; + return Pos(line, ch); + }; + return {anchor: adjustPos(doc.sel.anchor), head: adjustPos(doc.sel.head)}; + } + + function filterChange(doc, change, update) { + var obj = { + canceled: false, + from: change.from, + to: change.to, + text: change.text, + origin: change.origin, + cancel: function() { this.canceled = true; } + }; + if (update) obj.update = function(from, to, text, origin) { + if (from) this.from = clipPos(doc, from); + if (to) this.to = clipPos(doc, to); + if (text) this.text = text; + if (origin !== undefined) this.origin = origin; + }; + signal(doc, "beforeChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeChange", doc.cm, obj); + + if (obj.canceled) return null; + return {from: obj.from, to: obj.to, text: obj.text, origin: obj.origin}; + } + + // Replace the range from from to to by the strings in replacement. + // change is a {from, to, text [, origin]} object + function makeChange(doc, change, selUpdate, ignoreReadOnly) { + if (doc.cm) { + if (!doc.cm.curOp) return operation(doc.cm, makeChange)(doc, change, selUpdate, ignoreReadOnly); + if (doc.cm.state.suppressEdits) return; + } + + if (hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange")) { + change = filterChange(doc, change, true); + if (!change) return; + } + + // Possibly split or suppress the update based on the presence + // of read-only spans in its range. + var split = sawReadOnlySpans && !ignoreReadOnly && removeReadOnlyRanges(doc, change.from, change.to); + if (split) { + for (var i = split.length - 1; i >= 1; --i) + makeChangeNoReadonly(doc, {from: split[i].from, to: split[i].to, text: [""]}); + if (split.length) + makeChangeNoReadonly(doc, {from: split[0].from, to: split[0].to, text: change.text}, selUpdate); + } else { + makeChangeNoReadonly(doc, change, selUpdate); + } + } + + function makeChangeNoReadonly(doc, change, selUpdate) { + if (change.text.length == 1 && change.text[0] == "" && posEq(change.from, change.to)) return; + var selAfter = computeSelAfterChange(doc, change, selUpdate); + addToHistory(doc, change, selAfter, doc.cm ? doc.cm.curOp.id : NaN); + + makeChangeSingleDoc(doc, change, selAfter, stretchSpansOverChange(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, stretchSpansOverChange(doc, change)); + }); + } + + function makeChangeFromHistory(doc, type) { + if (doc.cm && doc.cm.state.suppressEdits) return; + + var hist = doc.history; + var event = (type == "undo" ? hist.done : hist.undone).pop(); + if (!event) return; + + var anti = {changes: [], anchorBefore: event.anchorAfter, headBefore: event.headAfter, + anchorAfter: event.anchorBefore, headAfter: event.headBefore, + generation: hist.generation}; + (type == "undo" ? hist.undone : hist.done).push(anti); + hist.generation = event.generation || ++hist.maxGeneration; + + var filter = hasHandler(doc, "beforeChange") || doc.cm && hasHandler(doc.cm, "beforeChange"); + + for (var i = event.changes.length - 1; i >= 0; --i) { + var change = event.changes[i]; + change.origin = type; + if (filter && !filterChange(doc, change, false)) { + (type == "undo" ? hist.done : hist.undone).length = 0; + return; + } + + anti.changes.push(historyChangeFromChange(doc, change)); + + var after = i ? computeSelAfterChange(doc, change, null) + : {anchor: event.anchorBefore, head: event.headBefore}; + makeChangeSingleDoc(doc, change, after, mergeOldSpans(doc, change)); + var rebased = []; + + linkedDocs(doc, function(doc, sharedHist) { + if (!sharedHist && indexOf(rebased, doc.history) == -1) { + rebaseHist(doc.history, change); + rebased.push(doc.history); + } + makeChangeSingleDoc(doc, change, null, mergeOldSpans(doc, change)); + }); + } + } + + function shiftDoc(doc, distance) { + function shiftPos(pos) {return Pos(pos.line + distance, pos.ch);} + doc.first += distance; + if (doc.cm) regChange(doc.cm, doc.first, doc.first, distance); + doc.sel.head = shiftPos(doc.sel.head); doc.sel.anchor = shiftPos(doc.sel.anchor); + doc.sel.from = shiftPos(doc.sel.from); doc.sel.to = shiftPos(doc.sel.to); + } + + function makeChangeSingleDoc(doc, change, selAfter, spans) { + if (doc.cm && !doc.cm.curOp) + return operation(doc.cm, makeChangeSingleDoc)(doc, change, selAfter, spans); + + if (change.to.line < doc.first) { + shiftDoc(doc, change.text.length - 1 - (change.to.line - change.from.line)); + return; + } + if (change.from.line > doc.lastLine()) return; + + // Clip the change to the size of this doc + if (change.from.line < doc.first) { + var shift = change.text.length - 1 - (doc.first - change.from.line); + shiftDoc(doc, shift); + change = {from: Pos(doc.first, 0), to: Pos(change.to.line + shift, change.to.ch), + text: [lst(change.text)], origin: change.origin}; + } + var last = doc.lastLine(); + if (change.to.line > last) { + change = {from: change.from, to: Pos(last, getLine(doc, last).text.length), + text: [change.text[0]], origin: change.origin}; + } + + change.removed = getBetween(doc, change.from, change.to); + + if (!selAfter) selAfter = computeSelAfterChange(doc, change, null); + if (doc.cm) makeChangeSingleDocInEditor(doc.cm, change, spans, selAfter); + else updateDoc(doc, change, spans, selAfter); + } + + function makeChangeSingleDocInEditor(cm, change, spans, selAfter) { + var doc = cm.doc, display = cm.display, from = change.from, to = change.to; + + var recomputeMaxLength = false, checkWidthStart = from.line; + if (!cm.options.lineWrapping) { + checkWidthStart = lineNo(visualLine(doc, getLine(doc, from.line))); + doc.iter(checkWidthStart, to.line + 1, function(line) { + if (line == display.maxLine) { + recomputeMaxLength = true; + return true; + } + }); + } + + if (!posLess(doc.sel.head, change.from) && !posLess(change.to, doc.sel.head)) + cm.curOp.cursorActivity = true; + + updateDoc(doc, change, spans, selAfter, estimateHeight(cm)); + + if (!cm.options.lineWrapping) { + doc.iter(checkWidthStart, from.line + change.text.length, function(line) { + var len = lineLength(doc, line); + if (len > display.maxLineLength) { + display.maxLine = line; + display.maxLineLength = len; + display.maxLineChanged = true; + recomputeMaxLength = false; + } + }); + if (recomputeMaxLength) cm.curOp.updateMaxLine = true; + } + + // Adjust frontier, schedule worker + doc.frontier = Math.min(doc.frontier, from.line); + startWorker(cm, 400); + + var lendiff = change.text.length - (to.line - from.line) - 1; + // Remember that these lines changed, for updating the display + regChange(cm, from.line, to.line + 1, lendiff); + + if (hasHandler(cm, "change")) { + var changeObj = {from: from, to: to, + text: change.text, + removed: change.removed, + origin: change.origin}; + if (cm.curOp.textChanged) { + for (var cur = cm.curOp.textChanged; cur.next; cur = cur.next) {} + cur.next = changeObj; + } else cm.curOp.textChanged = changeObj; + } + } + + function replaceRange(doc, code, from, to, origin) { + if (!to) to = from; + if (posLess(to, from)) { var tmp = to; to = from; from = tmp; } + if (typeof code == "string") code = splitLines(code); + makeChange(doc, {from: from, to: to, text: code, origin: origin}, null); + } + + // POSITION OBJECT + + function Pos(line, ch) { + if (!(this instanceof Pos)) return new Pos(line, ch); + this.line = line; this.ch = ch; + } + CodeMirror.Pos = Pos; + + function posEq(a, b) {return a.line == b.line && a.ch == b.ch;} + function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);} + function cmp(a, b) {return a.line - b.line || a.ch - b.ch;} + function copyPos(x) {return Pos(x.line, x.ch);} + + // SELECTION + + function clipLine(doc, n) {return Math.max(doc.first, Math.min(n, doc.first + doc.size - 1));} + function clipPos(doc, pos) { + if (pos.line < doc.first) return Pos(doc.first, 0); + var last = doc.first + doc.size - 1; + if (pos.line > last) return Pos(last, getLine(doc, last).text.length); + return clipToLen(pos, getLine(doc, pos.line).text.length); + } + function clipToLen(pos, linelen) { + var ch = pos.ch; + if (ch == null || ch > linelen) return Pos(pos.line, linelen); + else if (ch < 0) return Pos(pos.line, 0); + else return pos; + } + function isLine(doc, l) {return l >= doc.first && l < doc.first + doc.size;} + + // If shift is held, this will move the selection anchor. Otherwise, + // it'll set the whole selection. + function extendSelection(doc, pos, other, bias) { + if (doc.sel.shift || doc.sel.extend) { + var anchor = doc.sel.anchor; + if (other) { + var posBefore = posLess(pos, anchor); + if (posBefore != posLess(other, anchor)) { + anchor = pos; + pos = other; + } else if (posBefore != posLess(pos, other)) { + pos = other; + } + } + setSelection(doc, anchor, pos, bias); + } else { + setSelection(doc, pos, other || pos, bias); + } + if (doc.cm) doc.cm.curOp.userSelChange = true; + } + + function filterSelectionChange(doc, anchor, head) { + var obj = {anchor: anchor, head: head}; + signal(doc, "beforeSelectionChange", doc, obj); + if (doc.cm) signal(doc.cm, "beforeSelectionChange", doc.cm, obj); + obj.anchor = clipPos(doc, obj.anchor); obj.head = clipPos(doc, obj.head); + return obj; + } + + // Update the selection. Last two args are only used by + // updateDoc, since they have to be expressed in the line + // numbers before the update. + function setSelection(doc, anchor, head, bias, checkAtomic) { + if (!checkAtomic && hasHandler(doc, "beforeSelectionChange") || doc.cm && hasHandler(doc.cm, "beforeSelectionChange")) { + var filtered = filterSelectionChange(doc, anchor, head); + head = filtered.head; + anchor = filtered.anchor; + } + + var sel = doc.sel; + sel.goalColumn = null; + if (bias == null) bias = posLess(head, sel.head) ? -1 : 1; + // Skip over atomic spans. + if (checkAtomic || !posEq(anchor, sel.anchor)) + anchor = skipAtomic(doc, anchor, bias, checkAtomic != "push"); + if (checkAtomic || !posEq(head, sel.head)) + head = skipAtomic(doc, head, bias, checkAtomic != "push"); + + if (posEq(sel.anchor, anchor) && posEq(sel.head, head)) return; + + sel.anchor = anchor; sel.head = head; + var inv = posLess(head, anchor); + sel.from = inv ? head : anchor; + sel.to = inv ? anchor : head; + + if (doc.cm) + doc.cm.curOp.updateInput = doc.cm.curOp.selectionChanged = + doc.cm.curOp.cursorActivity = true; + + signalLater(doc, "cursorActivity", doc); + } + + function reCheckSelection(cm) { + setSelection(cm.doc, cm.doc.sel.from, cm.doc.sel.to, null, "push"); + } + + function skipAtomic(doc, pos, bias, mayClear) { + var flipped = false, curPos = pos; + var dir = bias || 1; + doc.cantEdit = false; + search: for (;;) { + var line = getLine(doc, curPos.line); + if (line.markedSpans) { + for (var i = 0; i < line.markedSpans.length; ++i) { + var sp = line.markedSpans[i], m = sp.marker; + if ((sp.from == null || (m.inclusiveLeft ? sp.from <= curPos.ch : sp.from < curPos.ch)) && + (sp.to == null || (m.inclusiveRight ? sp.to >= curPos.ch : sp.to > curPos.ch))) { + if (mayClear) { + signal(m, "beforeCursorEnter"); + if (m.explicitlyCleared) { + if (!line.markedSpans) break; + else {--i; continue;} + } + } + if (!m.atomic) continue; + var newPos = m.find()[dir < 0 ? "from" : "to"]; + if (posEq(newPos, curPos)) { + newPos.ch += dir; + if (newPos.ch < 0) { + if (newPos.line > doc.first) newPos = clipPos(doc, Pos(newPos.line - 1)); + else newPos = null; + } else if (newPos.ch > line.text.length) { + if (newPos.line < doc.first + doc.size - 1) newPos = Pos(newPos.line + 1, 0); + else newPos = null; + } + if (!newPos) { + if (flipped) { + // Driven in a corner -- no valid cursor position found at all + // -- try again *with* clearing, if we didn't already + if (!mayClear) return skipAtomic(doc, pos, bias, true); + // Otherwise, turn off editing until further notice, and return the start of the doc + doc.cantEdit = true; + return Pos(doc.first, 0); + } + flipped = true; newPos = pos; dir = -dir; + } + } + curPos = newPos; + continue search; + } + } + } + return curPos; + } + } + + // SCROLLING + + function scrollCursorIntoView(cm) { + var coords = scrollPosIntoView(cm, cm.doc.sel.head, null, cm.options.cursorScrollMargin); + if (!cm.state.focused) return; + var display = cm.display, box = getRect(display.sizer), doScroll = null; + if (coords.top + box.top < 0) doScroll = true; + else if (coords.bottom + box.top > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false; + if (doScroll != null && !phantom) { + var scrollNode = elt("div", "\u200b", null, "position: absolute; top: " + + (coords.top - display.viewOffset) + "px; height: " + + (coords.bottom - coords.top + scrollerCutOff) + "px; left: " + + coords.left + "px; width: 2px;"); + cm.display.lineSpace.appendChild(scrollNode); + scrollNode.scrollIntoView(doScroll); + cm.display.lineSpace.removeChild(scrollNode); + } + } + + function scrollPosIntoView(cm, pos, end, margin) { + if (margin == null) margin = 0; + for (;;) { + var changed = false, coords = cursorCoords(cm, pos); + var endCoords = !end || end == pos ? coords : cursorCoords(cm, end); + var scrollPos = calculateScrollPos(cm, Math.min(coords.left, endCoords.left), + Math.min(coords.top, endCoords.top) - margin, + Math.max(coords.left, endCoords.left), + Math.max(coords.bottom, endCoords.bottom) + margin); + var startTop = cm.doc.scrollTop, startLeft = cm.doc.scrollLeft; + if (scrollPos.scrollTop != null) { + setScrollTop(cm, scrollPos.scrollTop); + if (Math.abs(cm.doc.scrollTop - startTop) > 1) changed = true; + } + if (scrollPos.scrollLeft != null) { + setScrollLeft(cm, scrollPos.scrollLeft); + if (Math.abs(cm.doc.scrollLeft - startLeft) > 1) changed = true; + } + if (!changed) return coords; + } + } + + function scrollIntoView(cm, x1, y1, x2, y2) { + var scrollPos = calculateScrollPos(cm, x1, y1, x2, y2); + if (scrollPos.scrollTop != null) setScrollTop(cm, scrollPos.scrollTop); + if (scrollPos.scrollLeft != null) setScrollLeft(cm, scrollPos.scrollLeft); + } + + function calculateScrollPos(cm, x1, y1, x2, y2) { + var display = cm.display, snapMargin = textHeight(cm.display); + if (y1 < 0) y1 = 0; + var screen = display.scroller.clientHeight - scrollerCutOff, screentop = display.scroller.scrollTop, result = {}; + var docBottom = cm.doc.height + paddingVert(display); + var atTop = y1 < snapMargin, atBottom = y2 > docBottom - snapMargin; + if (y1 < screentop) { + result.scrollTop = atTop ? 0 : y1; + } else if (y2 > screentop + screen) { + var newTop = Math.min(y1, (atBottom ? docBottom : y2) - screen); + if (newTop != screentop) result.scrollTop = newTop; + } + + var screenw = display.scroller.clientWidth - scrollerCutOff, screenleft = display.scroller.scrollLeft; + x1 += display.gutters.offsetWidth; x2 += display.gutters.offsetWidth; + var gutterw = display.gutters.offsetWidth; + var atLeft = x1 < gutterw + 10; + if (x1 < screenleft + gutterw || atLeft) { + if (atLeft) x1 = 0; + result.scrollLeft = Math.max(0, x1 - 10 - gutterw); + } else if (x2 > screenw + screenleft - 3) { + result.scrollLeft = x2 + 10 - screenw; + } + return result; + } + + function updateScrollPos(cm, left, top) { + cm.curOp.updateScrollPos = {scrollLeft: left == null ? cm.doc.scrollLeft : left, + scrollTop: top == null ? cm.doc.scrollTop : top}; + } + + function addToScrollPos(cm, left, top) { + var pos = cm.curOp.updateScrollPos || (cm.curOp.updateScrollPos = {scrollLeft: cm.doc.scrollLeft, scrollTop: cm.doc.scrollTop}); + var scroll = cm.display.scroller; + pos.scrollTop = Math.max(0, Math.min(scroll.scrollHeight - scroll.clientHeight, pos.scrollTop + top)); + pos.scrollLeft = Math.max(0, Math.min(scroll.scrollWidth - scroll.clientWidth, pos.scrollLeft + left)); + } + + // API UTILITIES + + function indentLine(cm, n, how, aggressive) { + var doc = cm.doc; + if (how == null) how = "add"; + if (how == "smart") { + if (!cm.doc.mode.indent) how = "prev"; + else var state = getStateBefore(cm, n); + } + + var tabSize = cm.options.tabSize; + var line = getLine(doc, n), curSpace = countColumn(line.text, null, tabSize); + var curSpaceString = line.text.match(/^\s*/)[0], indentation; + if (!aggressive && !/\S/.test(line.text)) { + indentation = 0; + how = "not"; + } else if (how == "smart") { + indentation = cm.doc.mode.indent(state, line.text.slice(curSpaceString.length), line.text); + if (indentation == Pass) { + if (!aggressive) return; + how = "prev"; + } + } + if (how == "prev") { + if (n > doc.first) indentation = countColumn(getLine(doc, n-1).text, null, tabSize); + else indentation = 0; + } else if (how == "add") { + indentation = curSpace + cm.options.indentUnit; + } else if (how == "subtract") { + indentation = curSpace - cm.options.indentUnit; + } else if (typeof how == "number") { + indentation = curSpace + how; + } + indentation = Math.max(0, indentation); + + var indentString = "", pos = 0; + if (cm.options.indentWithTabs) + for (var i = Math.floor(indentation / tabSize); i; --i) {pos += tabSize; indentString += "\t";} + if (pos < indentation) indentString += spaceStr(indentation - pos); + + if (indentString != curSpaceString) + replaceRange(cm.doc, indentString, Pos(n, 0), Pos(n, curSpaceString.length), "+input"); + else if (doc.sel.head.line == n && doc.sel.head.ch < curSpaceString.length) + setSelection(doc, Pos(n, curSpaceString.length), Pos(n, curSpaceString.length), 1); + line.stateAfter = null; + } + + function changeLine(cm, handle, op) { + var no = handle, line = handle, doc = cm.doc; + if (typeof handle == "number") line = getLine(doc, clipLine(doc, handle)); + else no = lineNo(handle); + if (no == null) return null; + if (op(line, no)) regChange(cm, no, no + 1); + else return null; + return line; + } + + function findPosH(doc, pos, dir, unit, visually) { + var line = pos.line, ch = pos.ch, origDir = dir; + var lineObj = getLine(doc, line); + var possible = true; + function findNextLine() { + var l = line + dir; + if (l < doc.first || l >= doc.first + doc.size) return (possible = false); + line = l; + return lineObj = getLine(doc, l); + } + function moveOnce(boundToLine) { + var next = (visually ? moveVisually : moveLogically)(lineObj, ch, dir, true); + if (next == null) { + if (!boundToLine && findNextLine()) { + if (visually) ch = (dir < 0 ? lineRight : lineLeft)(lineObj); + else ch = dir < 0 ? lineObj.text.length : 0; + } else return (possible = false); + } else ch = next; + return true; + } + + if (unit == "char") moveOnce(); + else if (unit == "column") moveOnce(true); + else if (unit == "word" || unit == "group") { + var sawType = null, group = unit == "group"; + for (var first = true;; first = false) { + if (dir < 0 && !moveOnce(!first)) break; + var cur = lineObj.text.charAt(ch) || "\n"; + var type = isWordChar(cur) ? "w" + : !group ? null + : /\s/.test(cur) ? null + : "p"; + if (sawType && sawType != type) { + if (dir < 0) {dir = 1; moveOnce();} + break; + } + if (type) sawType = type; + if (dir > 0 && !moveOnce(!first)) break; + } + } + var result = skipAtomic(doc, Pos(line, ch), origDir, true); + if (!possible) result.hitSide = true; + return result; + } + + function findPosV(cm, pos, dir, unit) { + var doc = cm.doc, x = pos.left, y; + if (unit == "page") { + var pageSize = Math.min(cm.display.wrapper.clientHeight, window.innerHeight || document.documentElement.clientHeight); + y = pos.top + dir * (pageSize - (dir < 0 ? 1.5 : .5) * textHeight(cm.display)); + } else if (unit == "line") { + y = dir > 0 ? pos.bottom + 3 : pos.top - 3; + } + for (;;) { + var target = coordsChar(cm, x, y); + if (!target.outside) break; + if (dir < 0 ? y <= 0 : y >= doc.height) { target.hitSide = true; break; } + y += dir * 5; + } + return target; + } + + function findWordAt(line, pos) { + var start = pos.ch, end = pos.ch; + if (line) { + if ((pos.xRel < 0 || end == line.length) && start) --start; else ++end; + var startChar = line.charAt(start); + var check = isWordChar(startChar) ? isWordChar + : /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} + : function(ch) {return !/\s/.test(ch) && !isWordChar(ch);}; + while (start > 0 && check(line.charAt(start - 1))) --start; + while (end < line.length && check(line.charAt(end))) ++end; + } + return {from: Pos(pos.line, start), to: Pos(pos.line, end)}; + } + + function selectLine(cm, line) { + extendSelection(cm.doc, Pos(line, 0), clipPos(cm.doc, Pos(line + 1, 0))); + } + + // PROTOTYPE + + // The publicly visible API. Note that operation(null, f) means + // 'wrap f in an operation, performed on its `this` parameter' + + CodeMirror.prototype = { + constructor: CodeMirror, + focus: function(){window.focus(); focusInput(this); fastPoll(this);}, + + setOption: function(option, value) { + var options = this.options, old = options[option]; + if (options[option] == value && option != "mode") return; + options[option] = value; + if (optionHandlers.hasOwnProperty(option)) + operation(this, optionHandlers[option])(this, value, old); + }, + + getOption: function(option) {return this.options[option];}, + getDoc: function() {return this.doc;}, + + addKeyMap: function(map, bottom) { + this.state.keyMaps[bottom ? "push" : "unshift"](map); + }, + removeKeyMap: function(map) { + var maps = this.state.keyMaps; + for (var i = 0; i < maps.length; ++i) + if (maps[i] == map || (typeof maps[i] != "string" && maps[i].name == map)) { + maps.splice(i, 1); + return true; + } + }, + + addOverlay: operation(null, function(spec, options) { + var mode = spec.token ? spec : CodeMirror.getMode(this.options, spec); + if (mode.startState) throw new Error("Overlays may not be stateful."); + this.state.overlays.push({mode: mode, modeSpec: spec, opaque: options && options.opaque}); + this.state.modeGen++; + regChange(this); + }), + removeOverlay: operation(null, function(spec) { + var overlays = this.state.overlays; + for (var i = 0; i < overlays.length; ++i) { + var cur = overlays[i].modeSpec; + if (cur == spec || typeof spec == "string" && cur.name == spec) { + overlays.splice(i, 1); + this.state.modeGen++; + regChange(this); + return; + } + } + }), + + indentLine: operation(null, function(n, dir, aggressive) { + if (typeof dir != "string" && typeof dir != "number") { + if (dir == null) dir = this.options.smartIndent ? "smart" : "prev"; + else dir = dir ? "add" : "subtract"; + } + if (isLine(this.doc, n)) indentLine(this, n, dir, aggressive); + }), + indentSelection: operation(null, function(how) { + var sel = this.doc.sel; + if (posEq(sel.from, sel.to)) return indentLine(this, sel.from.line, how, true); + var e = sel.to.line - (sel.to.ch ? 0 : 1); + for (var i = sel.from.line; i <= e; ++i) indentLine(this, i, how); + }), + + // Fetch the parser token for a given character. Useful for hacks + // that want to inspect the mode state (say, for completion). + getTokenAt: function(pos, precise) { + var doc = this.doc; + pos = clipPos(doc, pos); + var state = getStateBefore(this, pos.line, precise), mode = this.doc.mode; + var line = getLine(doc, pos.line); + var stream = new StringStream(line.text, this.options.tabSize); + while (stream.pos < pos.ch && !stream.eol()) { + stream.start = stream.pos; + var style = mode.token(stream, state); + } + return {start: stream.start, + end: stream.pos, + string: stream.current(), + className: style || null, // Deprecated, use 'type' instead + type: style || null, + state: state}; + }, + + getTokenTypeAt: function(pos) { + pos = clipPos(this.doc, pos); + var styles = getLineStyles(this, getLine(this.doc, pos.line)); + var before = 0, after = (styles.length - 1) / 2, ch = pos.ch; + if (ch == 0) return styles[2]; + for (;;) { + var mid = (before + after) >> 1; + if ((mid ? styles[mid * 2 - 1] : 0) >= ch) after = mid; + else if (styles[mid * 2 + 1] < ch) before = mid + 1; + else return styles[mid * 2 + 2]; + } + }, + + getModeAt: function(pos) { + var mode = this.doc.mode; + if (!mode.innerMode) return mode; + return CodeMirror.innerMode(mode, this.getTokenAt(pos).state).mode; + }, + + getHelper: function(pos, type) { + return this.getHelpers(pos, type)[0]; + }, + + getHelpers: function(pos, type) { + var found = []; + if (!helpers.hasOwnProperty(type)) return helpers; + var help = helpers[type], mode = this.getModeAt(pos); + if (typeof mode[type] == "string") { + if (help[mode[type]]) found.push(help[mode[type]]); + } else if (mode[type]) { + for (var i = 0; i < mode[type].length; i++) { + var val = help[mode[type][i]]; + if (val) found.push(val); + } + } else if (mode.helperType && help[mode.helperType]) { + found.push(help[mode.helperType]); + } else if (help[mode.name]) { + found.push(help[mode.name]); + } + for (var i = 0; i < help._global.length; i++) { + var cur = help._global[i]; + if (cur.pred(mode, this) && indexOf(found, cur.val) == -1) + found.push(cur.val); + } + return found; + }, + + getStateAfter: function(line, precise) { + var doc = this.doc; + line = clipLine(doc, line == null ? doc.first + doc.size - 1: line); + return getStateBefore(this, line + 1, precise); + }, + + cursorCoords: function(start, mode) { + var pos, sel = this.doc.sel; + if (start == null) pos = sel.head; + else if (typeof start == "object") pos = clipPos(this.doc, start); + else pos = start ? sel.from : sel.to; + return cursorCoords(this, pos, mode || "page"); + }, + + charCoords: function(pos, mode) { + return charCoords(this, clipPos(this.doc, pos), mode || "page"); + }, + + coordsChar: function(coords, mode) { + coords = fromCoordSystem(this, coords, mode || "page"); + return coordsChar(this, coords.left, coords.top); + }, + + lineAtHeight: function(height, mode) { + height = fromCoordSystem(this, {top: height, left: 0}, mode || "page").top; + return lineAtHeight(this.doc, height + this.display.viewOffset); + }, + heightAtLine: function(line, mode) { + var end = false, last = this.doc.first + this.doc.size - 1; + if (line < this.doc.first) line = this.doc.first; + else if (line > last) { line = last; end = true; } + var lineObj = getLine(this.doc, line); + return intoCoordSystem(this, getLine(this.doc, line), {top: 0, left: 0}, mode || "page").top + + (end ? lineObj.height : 0); + }, + + defaultTextHeight: function() { return textHeight(this.display); }, + defaultCharWidth: function() { return charWidth(this.display); }, + + setGutterMarker: operation(null, function(line, gutterID, value) { + return changeLine(this, line, function(line) { + var markers = line.gutterMarkers || (line.gutterMarkers = {}); + markers[gutterID] = value; + if (!value && isEmpty(markers)) line.gutterMarkers = null; + return true; + }); + }), + + clearGutter: operation(null, function(gutterID) { + var cm = this, doc = cm.doc, i = doc.first; + doc.iter(function(line) { + if (line.gutterMarkers && line.gutterMarkers[gutterID]) { + line.gutterMarkers[gutterID] = null; + regChange(cm, i, i + 1); + if (isEmpty(line.gutterMarkers)) line.gutterMarkers = null; + } + ++i; + }); + }), + + addLineClass: operation(null, function(handle, where, cls) { + return changeLine(this, handle, function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + if (!line[prop]) line[prop] = cls; + else if (new RegExp("(?:^|\\s)" + cls + "(?:$|\\s)").test(line[prop])) return false; + else line[prop] += " " + cls; + return true; + }); + }), + + removeLineClass: operation(null, function(handle, where, cls) { + return changeLine(this, handle, function(line) { + var prop = where == "text" ? "textClass" : where == "background" ? "bgClass" : "wrapClass"; + var cur = line[prop]; + if (!cur) return false; + else if (cls == null) line[prop] = null; + else { + var found = cur.match(new RegExp("(?:^|\\s+)" + cls + "(?:$|\\s+)")); + if (!found) return false; + var end = found.index + found[0].length; + line[prop] = cur.slice(0, found.index) + (!found.index || end == cur.length ? "" : " ") + cur.slice(end) || null; + } + return true; + }); + }), + + addLineWidget: operation(null, function(handle, node, options) { + return addLineWidget(this, handle, node, options); + }), + + removeLineWidget: function(widget) { widget.clear(); }, + + lineInfo: function(line) { + if (typeof line == "number") { + if (!isLine(this.doc, line)) return null; + var n = line; + line = getLine(this.doc, line); + if (!line) return null; + } else { + var n = lineNo(line); + if (n == null) return null; + } + return {line: n, handle: line, text: line.text, gutterMarkers: line.gutterMarkers, + textClass: line.textClass, bgClass: line.bgClass, wrapClass: line.wrapClass, + widgets: line.widgets}; + }, + + getViewport: function() { return {from: this.display.showingFrom, to: this.display.showingTo};}, + + addWidget: function(pos, node, scroll, vert, horiz) { + var display = this.display; + pos = cursorCoords(this, clipPos(this.doc, pos)); + var top = pos.bottom, left = pos.left; + node.style.position = "absolute"; + display.sizer.appendChild(node); + if (vert == "over") { + top = pos.top; + } else if (vert == "above" || vert == "near") { + var vspace = Math.max(display.wrapper.clientHeight, this.doc.height), + hspace = Math.max(display.sizer.clientWidth, display.lineSpace.clientWidth); + // Default to positioning above (if specified and possible); otherwise default to positioning below + if ((vert == 'above' || pos.bottom + node.offsetHeight > vspace) && pos.top > node.offsetHeight) + top = pos.top - node.offsetHeight; + else if (pos.bottom + node.offsetHeight <= vspace) + top = pos.bottom; + if (left + node.offsetWidth > hspace) + left = hspace - node.offsetWidth; + } + node.style.top = top + "px"; + node.style.left = node.style.right = ""; + if (horiz == "right") { + left = display.sizer.clientWidth - node.offsetWidth; + node.style.right = "0px"; + } else { + if (horiz == "left") left = 0; + else if (horiz == "middle") left = (display.sizer.clientWidth - node.offsetWidth) / 2; + node.style.left = left + "px"; + } + if (scroll) + scrollIntoView(this, left, top, left + node.offsetWidth, top + node.offsetHeight); + }, + + triggerOnKeyDown: operation(null, onKeyDown), + + execCommand: function(cmd) { + if (commands.hasOwnProperty(cmd)) + return commands[cmd](this); + }, + + findPosH: function(from, amount, unit, visually) { + var dir = 1; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + cur = findPosH(this.doc, cur, dir, unit, visually); + if (cur.hitSide) break; + } + return cur; + }, + + moveH: operation(null, function(dir, unit) { + var sel = this.doc.sel, pos; + if (sel.shift || sel.extend || posEq(sel.from, sel.to)) + pos = findPosH(this.doc, sel.head, dir, unit, this.options.rtlMoveVisually); + else + pos = dir < 0 ? sel.from : sel.to; + extendSelection(this.doc, pos, pos, dir); + }), + + deleteH: operation(null, function(dir, unit) { + var sel = this.doc.sel; + if (!posEq(sel.from, sel.to)) replaceRange(this.doc, "", sel.from, sel.to, "+delete"); + else replaceRange(this.doc, "", sel.from, findPosH(this.doc, sel.head, dir, unit, false), "+delete"); + this.curOp.userSelChange = true; + }), + + findPosV: function(from, amount, unit, goalColumn) { + var dir = 1, x = goalColumn; + if (amount < 0) { dir = -1; amount = -amount; } + for (var i = 0, cur = clipPos(this.doc, from); i < amount; ++i) { + var coords = cursorCoords(this, cur, "div"); + if (x == null) x = coords.left; + else coords.left = x; + cur = findPosV(this, coords, dir, unit); + if (cur.hitSide) break; + } + return cur; + }, + + moveV: operation(null, function(dir, unit) { + var sel = this.doc.sel, target, goal; + if (sel.shift || sel.extend || posEq(sel.from, sel.to)) { + var pos = cursorCoords(this, sel.head, "div"); + if (sel.goalColumn != null) pos.left = sel.goalColumn; + target = findPosV(this, pos, dir, unit); + if (unit == "page") addToScrollPos(this, 0, charCoords(this, target, "div").top - pos.top); + goal = pos.left; + } else { + target = dir < 0 ? sel.from : sel.to; + } + extendSelection(this.doc, target, target, dir); + if (goal != null) sel.goalColumn = goal; + }), + + toggleOverwrite: function(value) { + if (value != null && value == this.state.overwrite) return; + if (this.state.overwrite = !this.state.overwrite) + this.display.cursor.className += " CodeMirror-overwrite"; + else + this.display.cursor.className = this.display.cursor.className.replace(" CodeMirror-overwrite", ""); + }, + hasFocus: function() { return this.state.focused; }, + + scrollTo: operation(null, function(x, y) { + updateScrollPos(this, x, y); + }), + getScrollInfo: function() { + var scroller = this.display.scroller, co = scrollerCutOff; + return {left: scroller.scrollLeft, top: scroller.scrollTop, + height: scroller.scrollHeight - co, width: scroller.scrollWidth - co, + clientHeight: scroller.clientHeight - co, clientWidth: scroller.clientWidth - co}; + }, + + scrollIntoView: operation(null, function(range, margin) { + if (range == null) range = {from: this.doc.sel.head, to: null}; + else if (typeof range == "number") range = {from: Pos(range, 0), to: null}; + else if (range.from == null) range = {from: range, to: null}; + if (!range.to) range.to = range.from; + if (!margin) margin = 0; + + var coords = range; + if (range.from.line != null) { + this.curOp.scrollToPos = {from: range.from, to: range.to, margin: margin}; + coords = {from: cursorCoords(this, range.from), + to: cursorCoords(this, range.to)}; + } + var sPos = calculateScrollPos(this, Math.min(coords.from.left, coords.to.left), + Math.min(coords.from.top, coords.to.top) - margin, + Math.max(coords.from.right, coords.to.right), + Math.max(coords.from.bottom, coords.to.bottom) + margin); + updateScrollPos(this, sPos.scrollLeft, sPos.scrollTop); + }), + + setSize: operation(null, function(width, height) { + function interpret(val) { + return typeof val == "number" || /^\d+$/.test(String(val)) ? val + "px" : val; + } + if (width != null) this.display.wrapper.style.width = interpret(width); + if (height != null) this.display.wrapper.style.height = interpret(height); + if (this.options.lineWrapping) + this.display.measureLineCache.length = this.display.measureLineCachePos = 0; + this.curOp.forceUpdate = true; + }), + + operation: function(f){return runInOp(this, f);}, + + refresh: operation(null, function() { + var badHeight = this.display.cachedTextHeight == null; + clearCaches(this); + updateScrollPos(this, this.doc.scrollLeft, this.doc.scrollTop); + regChange(this); + if (badHeight) estimateLineHeights(this); + }), + + swapDoc: operation(null, function(doc) { + var old = this.doc; + old.cm = null; + attachDoc(this, doc); + clearCaches(this); + resetInput(this, true); + updateScrollPos(this, doc.scrollLeft, doc.scrollTop); + signalLater(this, "swapDoc", this, old); + return old; + }), + + getInputField: function(){return this.display.input;}, + getWrapperElement: function(){return this.display.wrapper;}, + getScrollerElement: function(){return this.display.scroller;}, + getGutterElement: function(){return this.display.gutters;} + }; + eventMixin(CodeMirror); + + // OPTION DEFAULTS + + var optionHandlers = CodeMirror.optionHandlers = {}; + + // The default configuration options. + var defaults = CodeMirror.defaults = {}; + + function option(name, deflt, handle, notOnInit) { + CodeMirror.defaults[name] = deflt; + if (handle) optionHandlers[name] = + notOnInit ? function(cm, val, old) {if (old != Init) handle(cm, val, old);} : handle; + } + + var Init = CodeMirror.Init = {toString: function(){return "CodeMirror.Init";}}; + + // These two are, on init, called from the constructor because they + // have to be initialized before the editor can start at all. + option("value", "", function(cm, val) { + cm.setValue(val); + }, true); + option("mode", null, function(cm, val) { + cm.doc.modeOption = val; + loadMode(cm); + }, true); + + option("indentUnit", 2, loadMode, true); + option("indentWithTabs", false); + option("smartIndent", true); + option("tabSize", 4, function(cm) { + resetModeState(cm); + clearCaches(cm); + regChange(cm); + }, true); + option("specialChars", /[\t\u0000-\u0019\u00ad\u200b\u2028\u2029\ufeff]/g, function(cm, val) { + cm.options.specialChars = new RegExp(val.source + (val.test("\t") ? "" : "|\t"), "g"); + cm.refresh(); + }, true); + option("specialCharPlaceholder", defaultSpecialCharPlaceholder, function(cm) {cm.refresh();}, true); + option("electricChars", true); + option("rtlMoveVisually", !windows); + option("wholeLineUpdateBefore", true); + + option("theme", "default", function(cm) { + themeChanged(cm); + guttersChanged(cm); + }, true); + option("keyMap", "default", keyMapChanged); + option("extraKeys", null); + + option("onKeyEvent", null); + option("onDragEvent", null); + + option("lineWrapping", false, wrappingChanged, true); + option("gutters", [], function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("fixedGutter", true, function(cm, val) { + cm.display.gutters.style.left = val ? compensateForHScroll(cm.display) + "px" : "0"; + cm.refresh(); + }, true); + option("coverGutterNextToScrollbar", false, updateScrollbars, true); + option("lineNumbers", false, function(cm) { + setGuttersForLineNumbers(cm.options); + guttersChanged(cm); + }, true); + option("firstLineNumber", 1, guttersChanged, true); + option("lineNumberFormatter", function(integer) {return integer;}, guttersChanged, true); + option("showCursorWhenSelecting", false, updateSelection, true); + + option("resetSelectionOnContextMenu", true); + + option("readOnly", false, function(cm, val) { + if (val == "nocursor") { + onBlur(cm); + cm.display.input.blur(); + cm.display.disabled = true; + } else { + cm.display.disabled = false; + if (!val) resetInput(cm, true); + } + }); + option("disableInput", false, function(cm, val) {if (!val) resetInput(cm, true);}, true); + option("dragDrop", true); + + option("cursorBlinkRate", 530); + option("cursorScrollMargin", 0); + option("cursorHeight", 1); + option("workTime", 100); + option("workDelay", 100); + option("flattenSpans", true, resetModeState, true); + option("addModeClass", false, resetModeState, true); + option("pollInterval", 100); + option("undoDepth", 40, function(cm, val){cm.doc.history.undoDepth = val;}); + option("historyEventDelay", 500); + option("viewportMargin", 10, function(cm){cm.refresh();}, true); + option("maxHighlightLength", 10000, resetModeState, true); + option("crudeMeasuringFrom", 10000); + option("moveInputWithCursor", true, function(cm, val) { + if (!val) cm.display.inputDiv.style.top = cm.display.inputDiv.style.left = 0; + }); + + option("tabindex", null, function(cm, val) { + cm.display.input.tabIndex = val || ""; + }); + option("autofocus", null); + + // MODE DEFINITION AND QUERYING + + // Known modes, by name and by MIME + var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {}; + + CodeMirror.defineMode = function(name, mode) { + if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name; + if (arguments.length > 2) { + mode.dependencies = []; + for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]); + } + modes[name] = mode; + }; + + CodeMirror.defineMIME = function(mime, spec) { + mimeModes[mime] = spec; + }; + + CodeMirror.resolveMode = function(spec) { + if (typeof spec == "string" && mimeModes.hasOwnProperty(spec)) { + spec = mimeModes[spec]; + } else if (spec && typeof spec.name == "string" && mimeModes.hasOwnProperty(spec.name)) { + var found = mimeModes[spec.name]; + spec = createObj(found, spec); + spec.name = found.name; + } else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec)) { + return CodeMirror.resolveMode("application/xml"); + } + if (typeof spec == "string") return {name: spec}; + else return spec || {name: "null"}; + }; + + CodeMirror.getMode = function(options, spec) { + var spec = CodeMirror.resolveMode(spec); + var mfactory = modes[spec.name]; + if (!mfactory) return CodeMirror.getMode(options, "text/plain"); + var modeObj = mfactory(options, spec); + if (modeExtensions.hasOwnProperty(spec.name)) { + var exts = modeExtensions[spec.name]; + for (var prop in exts) { + if (!exts.hasOwnProperty(prop)) continue; + if (modeObj.hasOwnProperty(prop)) modeObj["_" + prop] = modeObj[prop]; + modeObj[prop] = exts[prop]; + } + } + modeObj.name = spec.name; + if (spec.helperType) modeObj.helperType = spec.helperType; + if (spec.modeProps) for (var prop in spec.modeProps) + modeObj[prop] = spec.modeProps[prop]; + + return modeObj; + }; + + CodeMirror.defineMode("null", function() { + return {token: function(stream) {stream.skipToEnd();}}; + }); + CodeMirror.defineMIME("text/plain", "null"); + + var modeExtensions = CodeMirror.modeExtensions = {}; + CodeMirror.extendMode = function(mode, properties) { + var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {}); + copyObj(properties, exts); + }; + + // EXTENSIONS + + CodeMirror.defineExtension = function(name, func) { + CodeMirror.prototype[name] = func; + }; + CodeMirror.defineDocExtension = function(name, func) { + Doc.prototype[name] = func; + }; + CodeMirror.defineOption = option; + + var initHooks = []; + CodeMirror.defineInitHook = function(f) {initHooks.push(f);}; + + var helpers = CodeMirror.helpers = {}; + CodeMirror.registerHelper = function(type, name, value) { + if (!helpers.hasOwnProperty(type)) helpers[type] = CodeMirror[type] = {_global: []}; + helpers[type][name] = value; + }; + CodeMirror.registerGlobalHelper = function(type, name, predicate, value) { + CodeMirror.registerHelper(type, name, value); + helpers[type]._global.push({pred: predicate, val: value}); + }; + + // UTILITIES + + CodeMirror.isWordChar = isWordChar; + + // MODE STATE HANDLING + + // Utility functions for working with state. Exported because modes + // sometimes need to do this. + function copyState(mode, state) { + if (state === true) return state; + if (mode.copyState) return mode.copyState(state); + var nstate = {}; + for (var n in state) { + var val = state[n]; + if (val instanceof Array) val = val.concat([]); + nstate[n] = val; + } + return nstate; + } + CodeMirror.copyState = copyState; + + function startState(mode, a1, a2) { + return mode.startState ? mode.startState(a1, a2) : true; + } + CodeMirror.startState = startState; + + CodeMirror.innerMode = function(mode, state) { + while (mode.innerMode) { + var info = mode.innerMode(state); + if (!info || info.mode == mode) break; + state = info.state; + mode = info.mode; + } + return info || {mode: mode, state: state}; + }; + + // STANDARD COMMANDS + + var commands = CodeMirror.commands = { + selectAll: function(cm) {cm.setSelection(Pos(cm.firstLine(), 0), Pos(cm.lastLine()));}, + killLine: function(cm) { + var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to); + if (!sel && cm.getLine(from.line).length == from.ch) + cm.replaceRange("", from, Pos(from.line + 1, 0), "+delete"); + else cm.replaceRange("", from, sel ? to : Pos(from.line), "+delete"); + }, + deleteLine: function(cm) { + var l = cm.getCursor().line; + cm.replaceRange("", Pos(l, 0), Pos(l), "+delete"); + }, + delLineLeft: function(cm) { + var cur = cm.getCursor(); + cm.replaceRange("", Pos(cur.line, 0), cur, "+delete"); + }, + undo: function(cm) {cm.undo();}, + redo: function(cm) {cm.redo();}, + goDocStart: function(cm) {cm.extendSelection(Pos(cm.firstLine(), 0));}, + goDocEnd: function(cm) {cm.extendSelection(Pos(cm.lastLine()));}, + goLineStart: function(cm) { + cm.extendSelection(lineStart(cm, cm.getCursor().line)); + }, + goLineStartSmart: function(cm) { + var cur = cm.getCursor(), start = lineStart(cm, cur.line); + var line = cm.getLineHandle(start.line); + var order = getOrder(line); + if (!order || order[0].level == 0) { + var firstNonWS = Math.max(0, line.text.search(/\S/)); + var inWS = cur.line == start.line && cur.ch <= firstNonWS && cur.ch; + cm.extendSelection(Pos(start.line, inWS ? 0 : firstNonWS)); + } else cm.extendSelection(start); + }, + goLineEnd: function(cm) { + cm.extendSelection(lineEnd(cm, cm.getCursor().line)); + }, + goLineRight: function(cm) { + var top = cm.charCoords(cm.getCursor(), "div").top + 5; + cm.extendSelection(cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div")); + }, + goLineLeft: function(cm) { + var top = cm.charCoords(cm.getCursor(), "div").top + 5; + cm.extendSelection(cm.coordsChar({left: 0, top: top}, "div")); + }, + goLineUp: function(cm) {cm.moveV(-1, "line");}, + goLineDown: function(cm) {cm.moveV(1, "line");}, + goPageUp: function(cm) {cm.moveV(-1, "page");}, + goPageDown: function(cm) {cm.moveV(1, "page");}, + goCharLeft: function(cm) {cm.moveH(-1, "char");}, + goCharRight: function(cm) {cm.moveH(1, "char");}, + goColumnLeft: function(cm) {cm.moveH(-1, "column");}, + goColumnRight: function(cm) {cm.moveH(1, "column");}, + goWordLeft: function(cm) {cm.moveH(-1, "word");}, + goGroupRight: function(cm) {cm.moveH(1, "group");}, + goGroupLeft: function(cm) {cm.moveH(-1, "group");}, + goWordRight: function(cm) {cm.moveH(1, "word");}, + delCharBefore: function(cm) {cm.deleteH(-1, "char");}, + delCharAfter: function(cm) {cm.deleteH(1, "char");}, + delWordBefore: function(cm) {cm.deleteH(-1, "word");}, + delWordAfter: function(cm) {cm.deleteH(1, "word");}, + delGroupBefore: function(cm) {cm.deleteH(-1, "group");}, + delGroupAfter: function(cm) {cm.deleteH(1, "group");}, + indentAuto: function(cm) {cm.indentSelection("smart");}, + indentMore: function(cm) {cm.indentSelection("add");}, + indentLess: function(cm) {cm.indentSelection("subtract");}, + insertTab: function(cm) { + cm.replaceSelection("\t", "end", "+input"); + }, + defaultTab: function(cm) { + if (cm.somethingSelected()) cm.indentSelection("add"); + else cm.replaceSelection("\t", "end", "+input"); + }, + transposeChars: function(cm) { + var cur = cm.getCursor(), line = cm.getLine(cur.line); + if (cur.ch > 0 && cur.ch < line.length - 1) + cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1), + Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1)); + }, + newlineAndIndent: function(cm) { + operation(cm, function() { + cm.replaceSelection("\n", "end", "+input"); + cm.indentLine(cm.getCursor().line, null, true); + })(); + }, + toggleOverwrite: function(cm) {cm.toggleOverwrite();} + }; + + // STANDARD KEYMAPS + + var keyMap = CodeMirror.keyMap = {}; + keyMap.basic = { + "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown", + "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown", + "Delete": "delCharAfter", "Backspace": "delCharBefore", "Shift-Backspace": "delCharBefore", + "Tab": "defaultTab", "Shift-Tab": "indentAuto", + "Enter": "newlineAndIndent", "Insert": "toggleOverwrite" + }; + // Note that the save and find-related commands aren't defined by + // default. Unknown commands are simply ignored. + keyMap.pcDefault = { + "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo", + "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd", + "Ctrl-Left": "goGroupLeft", "Ctrl-Right": "goGroupRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd", + "Ctrl-Backspace": "delGroupBefore", "Ctrl-Delete": "delGroupAfter", "Ctrl-S": "save", "Ctrl-F": "find", + "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll", + "Ctrl-[": "indentLess", "Ctrl-]": "indentMore", + fallthrough: "basic" + }; + keyMap.macDefault = { + "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo", + "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goGroupLeft", + "Alt-Right": "goGroupRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delGroupBefore", + "Ctrl-Alt-Backspace": "delGroupAfter", "Alt-Delete": "delGroupAfter", "Cmd-S": "save", "Cmd-F": "find", + "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll", + "Cmd-[": "indentLess", "Cmd-]": "indentMore", "Cmd-Backspace": "delLineLeft", + fallthrough: ["basic", "emacsy"] + }; + keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault; + keyMap.emacsy = { + "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown", + "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd", + "Ctrl-V": "goPageDown", "Shift-Ctrl-V": "goPageUp", "Ctrl-D": "delCharAfter", "Ctrl-H": "delCharBefore", + "Alt-D": "delWordAfter", "Alt-Backspace": "delWordBefore", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars" + }; + + // KEYMAP DISPATCH + + function getKeyMap(val) { + if (typeof val == "string") return keyMap[val]; + else return val; + } + + function lookupKey(name, maps, handle) { + function lookup(map) { + map = getKeyMap(map); + var found = map[name]; + if (found === false) return "stop"; + if (found != null && handle(found)) return true; + if (map.nofallthrough) return "stop"; + + var fallthrough = map.fallthrough; + if (fallthrough == null) return false; + if (Object.prototype.toString.call(fallthrough) != "[object Array]") + return lookup(fallthrough); + for (var i = 0, e = fallthrough.length; i < e; ++i) { + var done = lookup(fallthrough[i]); + if (done) return done; + } + return false; + } + + for (var i = 0; i < maps.length; ++i) { + var done = lookup(maps[i]); + if (done) return done != "stop"; + } + } + function isModifierKey(event) { + var name = keyNames[event.keyCode]; + return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod"; + } + function keyName(event, noShift) { + if (opera && event.keyCode == 34 && event["char"]) return false; + var name = keyNames[event.keyCode]; + if (name == null || event.altGraphKey) return false; + if (event.altKey) name = "Alt-" + name; + if (flipCtrlCmd ? event.metaKey : event.ctrlKey) name = "Ctrl-" + name; + if (flipCtrlCmd ? event.ctrlKey : event.metaKey) name = "Cmd-" + name; + if (!noShift && event.shiftKey) name = "Shift-" + name; + return name; + } + CodeMirror.lookupKey = lookupKey; + CodeMirror.isModifierKey = isModifierKey; + CodeMirror.keyName = keyName; + + // FROMTEXTAREA + + CodeMirror.fromTextArea = function(textarea, options) { + if (!options) options = {}; + options.value = textarea.value; + if (!options.tabindex && textarea.tabindex) + options.tabindex = textarea.tabindex; + if (!options.placeholder && textarea.placeholder) + options.placeholder = textarea.placeholder; + // Set autofocus to true if this textarea is focused, or if it has + // autofocus and no other element is focused. + if (options.autofocus == null) { + var hasFocus = document.body; + // doc.activeElement occasionally throws on IE + try { hasFocus = document.activeElement; } catch(e) {} + options.autofocus = hasFocus == textarea || + textarea.getAttribute("autofocus") != null && hasFocus == document.body; + } + + function save() {textarea.value = cm.getValue();} + if (textarea.form) { + on(textarea.form, "submit", save); + // Deplorable hack to make the submit method do the right thing. + if (!options.leaveSubmitMethodAlone) { + var form = textarea.form, realSubmit = form.submit; + try { + var wrappedSubmit = form.submit = function() { + save(); + form.submit = realSubmit; + form.submit(); + form.submit = wrappedSubmit; + }; + } catch(e) {} + } + } + + textarea.style.display = "none"; + var cm = CodeMirror(function(node) { + textarea.parentNode.insertBefore(node, textarea.nextSibling); + }, options); + cm.save = save; + cm.getTextArea = function() { return textarea; }; + cm.toTextArea = function() { + save(); + textarea.parentNode.removeChild(cm.getWrapperElement()); + textarea.style.display = ""; + if (textarea.form) { + off(textarea.form, "submit", save); + if (typeof textarea.form.submit == "function") + textarea.form.submit = realSubmit; + } + }; + return cm; + }; + + // STRING STREAM + + // Fed to the mode parsers, provides helper functions to make + // parsers more succinct. + + // The character stream used by a mode's parser. + function StringStream(string, tabSize) { + this.pos = this.start = 0; + this.string = string; + this.tabSize = tabSize || 8; + this.lastColumnPos = this.lastColumnValue = 0; + this.lineStart = 0; + } + + StringStream.prototype = { + eol: function() {return this.pos >= this.string.length;}, + sol: function() {return this.pos == this.lineStart;}, + peek: function() {return this.string.charAt(this.pos) || undefined;}, + next: function() { + if (this.pos < this.string.length) + return this.string.charAt(this.pos++); + }, + eat: function(match) { + var ch = this.string.charAt(this.pos); + if (typeof match == "string") var ok = ch == match; + else var ok = ch && (match.test ? match.test(ch) : match(ch)); + if (ok) {++this.pos; return ch;} + }, + eatWhile: function(match) { + var start = this.pos; + while (this.eat(match)){} + return this.pos > start; + }, + eatSpace: function() { + var start = this.pos; + while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos; + return this.pos > start; + }, + skipToEnd: function() {this.pos = this.string.length;}, + skipTo: function(ch) { + var found = this.string.indexOf(ch, this.pos); + if (found > -1) {this.pos = found; return true;} + }, + backUp: function(n) {this.pos -= n;}, + column: function() { + if (this.lastColumnPos < this.start) { + this.lastColumnValue = countColumn(this.string, this.start, this.tabSize, this.lastColumnPos, this.lastColumnValue); + this.lastColumnPos = this.start; + } + return this.lastColumnValue - (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + indentation: function() { + return countColumn(this.string, null, this.tabSize) - + (this.lineStart ? countColumn(this.string, this.lineStart, this.tabSize) : 0); + }, + match: function(pattern, consume, caseInsensitive) { + if (typeof pattern == "string") { + var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;}; + var substr = this.string.substr(this.pos, pattern.length); + if (cased(substr) == cased(pattern)) { + if (consume !== false) this.pos += pattern.length; + return true; + } + } else { + var match = this.string.slice(this.pos).match(pattern); + if (match && match.index > 0) return null; + if (match && consume !== false) this.pos += match[0].length; + return match; + } + }, + current: function(){return this.string.slice(this.start, this.pos);}, + hideFirstChars: function(n, inner) { + this.lineStart += n; + try { return inner(); } + finally { this.lineStart -= n; } + } + }; + CodeMirror.StringStream = StringStream; + + // TEXTMARKERS + + function TextMarker(doc, type) { + this.lines = []; + this.type = type; + this.doc = doc; + } + CodeMirror.TextMarker = TextMarker; + eventMixin(TextMarker); + + TextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + var cm = this.doc.cm, withOp = cm && !cm.curOp; + if (withOp) startOperation(cm); + if (hasHandler(this, "clear")) { + var found = this.find(); + if (found) signalLater(this, "clear", found.from, found.to); + } + var min = null, max = null; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.to != null) max = lineNo(line); + line.markedSpans = removeMarkedSpan(line.markedSpans, span); + if (span.from != null) + min = lineNo(line); + else if (this.collapsed && !lineIsHidden(this.doc, line) && cm) + updateLineHeight(line, textHeight(cm.display)); + } + if (cm && this.collapsed && !cm.options.lineWrapping) for (var i = 0; i < this.lines.length; ++i) { + var visual = visualLine(cm.doc, this.lines[i]), len = lineLength(cm.doc, visual); + if (len > cm.display.maxLineLength) { + cm.display.maxLine = visual; + cm.display.maxLineLength = len; + cm.display.maxLineChanged = true; + } + } + + if (min != null && cm) regChange(cm, min, max + 1); + this.lines.length = 0; + this.explicitlyCleared = true; + if (this.atomic && this.doc.cantEdit) { + this.doc.cantEdit = false; + if (cm) reCheckSelection(cm); + } + if (withOp) endOperation(cm); + }; + + TextMarker.prototype.find = function(bothSides) { + var from, to; + for (var i = 0; i < this.lines.length; ++i) { + var line = this.lines[i]; + var span = getMarkedSpanFor(line.markedSpans, this); + if (span.from != null || span.to != null) { + var found = lineNo(line); + if (span.from != null) from = Pos(found, span.from); + if (span.to != null) to = Pos(found, span.to); + } + } + if (this.type == "bookmark" && !bothSides) return from; + return from && {from: from, to: to}; + }; + + TextMarker.prototype.changed = function() { + var pos = this.find(), cm = this.doc.cm; + if (!pos || !cm) return; + if (this.type != "bookmark") pos = pos.from; + var line = getLine(this.doc, pos.line); + clearCachedMeasurement(cm, line); + if (pos.line >= cm.display.showingFrom && pos.line < cm.display.showingTo) { + for (var node = cm.display.lineDiv.firstChild; node; node = node.nextSibling) if (node.lineObj == line) { + if (node.offsetHeight != line.height) updateLineHeight(line, node.offsetHeight); + break; + } + runInOp(cm, function() { + cm.curOp.selectionChanged = cm.curOp.forceUpdate = cm.curOp.updateMaxLine = true; + }); + } + }; + + TextMarker.prototype.attachLine = function(line) { + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + if (!op.maybeHiddenMarkers || indexOf(op.maybeHiddenMarkers, this) == -1) + (op.maybeUnhiddenMarkers || (op.maybeUnhiddenMarkers = [])).push(this); + } + this.lines.push(line); + }; + TextMarker.prototype.detachLine = function(line) { + this.lines.splice(indexOf(this.lines, line), 1); + if (!this.lines.length && this.doc.cm) { + var op = this.doc.cm.curOp; + (op.maybeHiddenMarkers || (op.maybeHiddenMarkers = [])).push(this); + } + }; + + var nextMarkerId = 0; + + function markText(doc, from, to, options, type) { + if (options && options.shared) return markTextShared(doc, from, to, options, type); + if (doc.cm && !doc.cm.curOp) return operation(doc.cm, markText)(doc, from, to, options, type); + + var marker = new TextMarker(doc, type); + if (options) copyObj(options, marker); + if (posLess(to, from) || posEq(from, to) && marker.clearWhenEmpty !== false) + return marker; + if (marker.replacedWith) { + marker.collapsed = true; + marker.replacedWith = elt("span", [marker.replacedWith], "CodeMirror-widget"); + if (!options.handleMouseEvents) marker.replacedWith.ignoreEvents = true; + } + if (marker.collapsed) { + if (conflictingCollapsedRange(doc, from.line, from, to, marker) || + from.line != to.line && conflictingCollapsedRange(doc, to.line, from, to, marker)) + throw new Error("Inserting collapsed marker partially overlapping an existing one"); + sawCollapsedSpans = true; + } + + if (marker.addToHistory) + addToHistory(doc, {from: from, to: to, origin: "markText"}, + {head: doc.sel.head, anchor: doc.sel.anchor}, NaN); + + var curLine = from.line, cm = doc.cm, updateMaxLine; + doc.iter(curLine, to.line + 1, function(line) { + if (cm && marker.collapsed && !cm.options.lineWrapping && visualLine(doc, line) == cm.display.maxLine) + updateMaxLine = true; + var span = {from: null, to: null, marker: marker}; + if (curLine == from.line) span.from = from.ch; + if (curLine == to.line) span.to = to.ch; + if (marker.collapsed && curLine != from.line) updateLineHeight(line, 0); + addMarkedSpan(line, span); + ++curLine; + }); + if (marker.collapsed) doc.iter(from.line, to.line + 1, function(line) { + if (lineIsHidden(doc, line)) updateLineHeight(line, 0); + }); + + if (marker.clearOnEnter) on(marker, "beforeCursorEnter", function() { marker.clear(); }); + + if (marker.readOnly) { + sawReadOnlySpans = true; + if (doc.history.done.length || doc.history.undone.length) + doc.clearHistory(); + } + if (marker.collapsed) { + marker.id = ++nextMarkerId; + marker.atomic = true; + } + if (cm) { + if (updateMaxLine) cm.curOp.updateMaxLine = true; + if (marker.className || marker.title || marker.startStyle || marker.endStyle || marker.collapsed) + regChange(cm, from.line, to.line + 1); + if (marker.atomic) reCheckSelection(cm); + } + return marker; + } + + // SHARED TEXTMARKERS + + function SharedTextMarker(markers, primary) { + this.markers = markers; + this.primary = primary; + for (var i = 0, me = this; i < markers.length; ++i) { + markers[i].parent = this; + on(markers[i], "clear", function(){me.clear();}); + } + } + CodeMirror.SharedTextMarker = SharedTextMarker; + eventMixin(SharedTextMarker); + + SharedTextMarker.prototype.clear = function() { + if (this.explicitlyCleared) return; + this.explicitlyCleared = true; + for (var i = 0; i < this.markers.length; ++i) + this.markers[i].clear(); + signalLater(this, "clear"); + }; + SharedTextMarker.prototype.find = function() { + return this.primary.find(); + }; + + function markTextShared(doc, from, to, options, type) { + options = copyObj(options); + options.shared = false; + var markers = [markText(doc, from, to, options, type)], primary = markers[0]; + var widget = options.replacedWith; + linkedDocs(doc, function(doc) { + if (widget) options.replacedWith = widget.cloneNode(true); + markers.push(markText(doc, clipPos(doc, from), clipPos(doc, to), options, type)); + for (var i = 0; i < doc.linked.length; ++i) + if (doc.linked[i].isParent) return; + primary = lst(markers); + }); + return new SharedTextMarker(markers, primary); + } + + // TEXTMARKER SPANS + + function getMarkedSpanFor(spans, marker) { + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.marker == marker) return span; + } + } + function removeMarkedSpan(spans, span) { + for (var r, i = 0; i < spans.length; ++i) + if (spans[i] != span) (r || (r = [])).push(spans[i]); + return r; + } + function addMarkedSpan(line, span) { + line.markedSpans = line.markedSpans ? line.markedSpans.concat([span]) : [span]; + span.marker.attachLine(line); + } + + function markedSpansBefore(old, startCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh); + if (startsBefore || span.from == startCh && marker.type == "bookmark" && (!isInsert || !span.marker.insertLeft)) { + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh); + (nw || (nw = [])).push({from: span.from, + to: endsAfter ? null : span.to, + marker: marker}); + } + } + return nw; + } + + function markedSpansAfter(old, endCh, isInsert) { + if (old) for (var i = 0, nw; i < old.length; ++i) { + var span = old[i], marker = span.marker; + var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh); + if (endsAfter || span.from == endCh && marker.type == "bookmark" && (!isInsert || span.marker.insertLeft)) { + var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh); + (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh, + to: span.to == null ? null : span.to - endCh, + marker: marker}); + } + } + return nw; + } + + function stretchSpansOverChange(doc, change) { + var oldFirst = isLine(doc, change.from.line) && getLine(doc, change.from.line).markedSpans; + var oldLast = isLine(doc, change.to.line) && getLine(doc, change.to.line).markedSpans; + if (!oldFirst && !oldLast) return null; + + var startCh = change.from.ch, endCh = change.to.ch, isInsert = posEq(change.from, change.to); + // Get the spans that 'stick out' on both sides + var first = markedSpansBefore(oldFirst, startCh, isInsert); + var last = markedSpansAfter(oldLast, endCh, isInsert); + + // Next, merge those two ends + var sameLine = change.text.length == 1, offset = lst(change.text).length + (sameLine ? startCh : 0); + if (first) { + // Fix up .to properties of first + for (var i = 0; i < first.length; ++i) { + var span = first[i]; + if (span.to == null) { + var found = getMarkedSpanFor(last, span.marker); + if (!found) span.to = startCh; + else if (sameLine) span.to = found.to == null ? null : found.to + offset; + } + } + } + if (last) { + // Fix up .from in last (or move them into first in case of sameLine) + for (var i = 0; i < last.length; ++i) { + var span = last[i]; + if (span.to != null) span.to += offset; + if (span.from == null) { + var found = getMarkedSpanFor(first, span.marker); + if (!found) { + span.from = offset; + if (sameLine) (first || (first = [])).push(span); + } + } else { + span.from += offset; + if (sameLine) (first || (first = [])).push(span); + } + } + } + // Make sure we didn't create any zero-length spans + if (first) first = clearEmptySpans(first); + if (last && last != first) last = clearEmptySpans(last); + + var newMarkers = [first]; + if (!sameLine) { + // Fill gap with whole-line-spans + var gap = change.text.length - 2, gapMarkers; + if (gap > 0 && first) + for (var i = 0; i < first.length; ++i) + if (first[i].to == null) + (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker}); + for (var i = 0; i < gap; ++i) + newMarkers.push(gapMarkers); + newMarkers.push(last); + } + return newMarkers; + } + + function clearEmptySpans(spans) { + for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if (span.from != null && span.from == span.to && span.marker.clearWhenEmpty !== false) + spans.splice(i--, 1); + } + if (!spans.length) return null; + return spans; + } + + function mergeOldSpans(doc, change) { + var old = getOldSpans(doc, change); + var stretched = stretchSpansOverChange(doc, change); + if (!old) return stretched; + if (!stretched) return old; + + for (var i = 0; i < old.length; ++i) { + var oldCur = old[i], stretchCur = stretched[i]; + if (oldCur && stretchCur) { + spans: for (var j = 0; j < stretchCur.length; ++j) { + var span = stretchCur[j]; + for (var k = 0; k < oldCur.length; ++k) + if (oldCur[k].marker == span.marker) continue spans; + oldCur.push(span); + } + } else if (stretchCur) { + old[i] = stretchCur; + } + } + return old; + } + + function removeReadOnlyRanges(doc, from, to) { + var markers = null; + doc.iter(from.line, to.line + 1, function(line) { + if (line.markedSpans) for (var i = 0; i < line.markedSpans.length; ++i) { + var mark = line.markedSpans[i].marker; + if (mark.readOnly && (!markers || indexOf(markers, mark) == -1)) + (markers || (markers = [])).push(mark); + } + }); + if (!markers) return null; + var parts = [{from: from, to: to}]; + for (var i = 0; i < markers.length; ++i) { + var mk = markers[i], m = mk.find(); + for (var j = 0; j < parts.length; ++j) { + var p = parts[j]; + if (posLess(p.to, m.from) || posLess(m.to, p.from)) continue; + var newParts = [j, 1]; + if (posLess(p.from, m.from) || !mk.inclusiveLeft && posEq(p.from, m.from)) + newParts.push({from: p.from, to: m.from}); + if (posLess(m.to, p.to) || !mk.inclusiveRight && posEq(p.to, m.to)) + newParts.push({from: m.to, to: p.to}); + parts.splice.apply(parts, newParts); + j += newParts.length - 1; + } + } + return parts; + } + + function extraLeft(marker) { return marker.inclusiveLeft ? -1 : 0; } + function extraRight(marker) { return marker.inclusiveRight ? 1 : 0; } + + function compareCollapsedMarkers(a, b) { + var lenDiff = a.lines.length - b.lines.length; + if (lenDiff != 0) return lenDiff; + var aPos = a.find(), bPos = b.find(); + var fromCmp = cmp(aPos.from, bPos.from) || extraLeft(a) - extraLeft(b); + if (fromCmp) return -fromCmp; + var toCmp = cmp(aPos.to, bPos.to) || extraRight(a) - extraRight(b); + if (toCmp) return toCmp; + return b.id - a.id; + } + + function collapsedSpanAtSide(line, start) { + var sps = sawCollapsedSpans && line.markedSpans, found; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (sp.marker.collapsed && (start ? sp.from : sp.to) == null && + (!found || compareCollapsedMarkers(found, sp.marker) < 0)) + found = sp.marker; + } + return found; + } + function collapsedSpanAtStart(line) { return collapsedSpanAtSide(line, true); } + function collapsedSpanAtEnd(line) { return collapsedSpanAtSide(line, false); } + + function conflictingCollapsedRange(doc, lineNo, from, to, marker) { + var line = getLine(doc, lineNo); + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) { + var sp = sps[i]; + if (!sp.marker.collapsed) continue; + var found = sp.marker.find(true); + var fromCmp = cmp(found.from, from) || extraLeft(sp.marker) - extraLeft(marker); + var toCmp = cmp(found.to, to) || extraRight(sp.marker) - extraRight(marker); + if (fromCmp >= 0 && toCmp <= 0 || fromCmp <= 0 && toCmp >= 0) continue; + if (fromCmp <= 0 && (cmp(found.to, from) || extraRight(sp.marker) - extraLeft(marker)) > 0 || + fromCmp >= 0 && (cmp(found.from, to) || extraLeft(sp.marker) - extraRight(marker)) < 0) + return true; + } + } + + function visualLine(doc, line) { + var merged; + while (merged = collapsedSpanAtStart(line)) + line = getLine(doc, merged.find().from.line); + return line; + } + + function lineIsHidden(doc, line) { + var sps = sawCollapsedSpans && line.markedSpans; + if (sps) for (var sp, i = 0; i < sps.length; ++i) { + sp = sps[i]; + if (!sp.marker.collapsed) continue; + if (sp.from == null) return true; + if (sp.marker.replacedWith) continue; + if (sp.from == 0 && sp.marker.inclusiveLeft && lineIsHiddenInner(doc, line, sp)) + return true; + } + } + function lineIsHiddenInner(doc, line, span) { + if (span.to == null) { + var end = span.marker.find().to, endLine = getLine(doc, end.line); + return lineIsHiddenInner(doc, endLine, getMarkedSpanFor(endLine.markedSpans, span.marker)); + } + if (span.marker.inclusiveRight && span.to == line.text.length) + return true; + for (var sp, i = 0; i < line.markedSpans.length; ++i) { + sp = line.markedSpans[i]; + if (sp.marker.collapsed && !sp.marker.replacedWith && sp.from == span.to && + (sp.to == null || sp.to != span.from) && + (sp.marker.inclusiveLeft || span.marker.inclusiveRight) && + lineIsHiddenInner(doc, line, sp)) return true; + } + } + + function detachMarkedSpans(line) { + var spans = line.markedSpans; + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.detachLine(line); + line.markedSpans = null; + } + + function attachMarkedSpans(line, spans) { + if (!spans) return; + for (var i = 0; i < spans.length; ++i) + spans[i].marker.attachLine(line); + line.markedSpans = spans; + } + + // LINE WIDGETS + + var LineWidget = CodeMirror.LineWidget = function(cm, node, options) { + if (options) for (var opt in options) if (options.hasOwnProperty(opt)) + this[opt] = options[opt]; + this.cm = cm; + this.node = node; + }; + eventMixin(LineWidget); + function widgetOperation(f) { + return function() { + var withOp = !this.cm.curOp; + if (withOp) startOperation(this.cm); + try {var result = f.apply(this, arguments);} + finally {if (withOp) endOperation(this.cm);} + return result; + }; + } + LineWidget.prototype.clear = widgetOperation(function() { + var ws = this.line.widgets, no = lineNo(this.line); + if (no == null || !ws) return; + for (var i = 0; i < ws.length; ++i) if (ws[i] == this) ws.splice(i--, 1); + if (!ws.length) this.line.widgets = null; + var aboveVisible = heightAtLine(this.cm, this.line) < this.cm.doc.scrollTop; + updateLineHeight(this.line, Math.max(0, this.line.height - widgetHeight(this))); + if (aboveVisible) addToScrollPos(this.cm, 0, -this.height); + regChange(this.cm, no, no + 1); + }); + LineWidget.prototype.changed = widgetOperation(function() { + var oldH = this.height; + this.height = null; + var diff = widgetHeight(this) - oldH; + if (!diff) return; + updateLineHeight(this.line, this.line.height + diff); + var no = lineNo(this.line); + regChange(this.cm, no, no + 1); + }); + + function widgetHeight(widget) { + if (widget.height != null) return widget.height; + if (!widget.node.parentNode || widget.node.parentNode.nodeType != 1) + removeChildrenAndAdd(widget.cm.display.measure, elt("div", [widget.node], null, "position: relative")); + return widget.height = widget.node.offsetHeight; + } + + function addLineWidget(cm, handle, node, options) { + var widget = new LineWidget(cm, node, options); + if (widget.noHScroll) cm.display.alignWidgets = true; + changeLine(cm, handle, function(line) { + var widgets = line.widgets || (line.widgets = []); + if (widget.insertAt == null) widgets.push(widget); + else widgets.splice(Math.min(widgets.length - 1, Math.max(0, widget.insertAt)), 0, widget); + widget.line = line; + if (!lineIsHidden(cm.doc, line) || widget.showIfHidden) { + var aboveVisible = heightAtLine(cm, line) < cm.doc.scrollTop; + updateLineHeight(line, line.height + widgetHeight(widget)); + if (aboveVisible) addToScrollPos(cm, 0, widget.height); + } + return true; + }); + return widget; + } + + // LINE DATA STRUCTURE + + // Line objects. These hold state related to a line, including + // highlighting info (the styles array). + var Line = CodeMirror.Line = function(text, markedSpans, estimateHeight) { + this.text = text; + attachMarkedSpans(this, markedSpans); + this.height = estimateHeight ? estimateHeight(this) : 1; + }; + eventMixin(Line); + Line.prototype.lineNo = function() { return lineNo(this); }; + + function updateLine(line, text, markedSpans, estimateHeight) { + line.text = text; + if (line.stateAfter) line.stateAfter = null; + if (line.styles) line.styles = null; + if (line.order != null) line.order = null; + detachMarkedSpans(line); + attachMarkedSpans(line, markedSpans); + var estHeight = estimateHeight ? estimateHeight(line) : 1; + if (estHeight != line.height) updateLineHeight(line, estHeight); + } + + function cleanUpLine(line) { + line.parent = null; + detachMarkedSpans(line); + } + + // Run the given mode's parser over a line, update the styles + // array, which contains alternating fragments of text and CSS + // classes. + function runMode(cm, text, mode, state, f, forceToEnd) { + var flattenSpans = mode.flattenSpans; + if (flattenSpans == null) flattenSpans = cm.options.flattenSpans; + var curStart = 0, curStyle = null; + var stream = new StringStream(text, cm.options.tabSize), style; + if (text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol()) { + if (stream.pos > cm.options.maxHighlightLength) { + flattenSpans = false; + if (forceToEnd) processLine(cm, text, state, stream.pos); + stream.pos = text.length; + style = null; + } else { + style = mode.token(stream, state); + } + if (cm.options.addModeClass) { + var mName = CodeMirror.innerMode(mode, state).mode.name; + if (mName) style = "m-" + (style ? mName + " " + style : mName); + } + if (!flattenSpans || curStyle != style) { + if (curStart < stream.start) f(stream.start, curStyle); + curStart = stream.start; curStyle = style; + } + stream.start = stream.pos; + } + while (curStart < stream.pos) { + // Webkit seems to refuse to render text nodes longer than 57444 characters + var pos = Math.min(stream.pos, curStart + 50000); + f(pos, curStyle); + curStart = pos; + } + } + + function highlightLine(cm, line, state, forceToEnd) { + // A styles array always starts with a number identifying the + // mode/overlays that it is based on (for easy invalidation). + var st = [cm.state.modeGen]; + // Compute the base array of styles + runMode(cm, line.text, cm.doc.mode, state, function(end, style) { + st.push(end, style); + }, forceToEnd); + + // Run overlays, adjust style array. + for (var o = 0; o < cm.state.overlays.length; ++o) { + var overlay = cm.state.overlays[o], i = 1, at = 0; + runMode(cm, line.text, overlay.mode, true, function(end, style) { + var start = i; + // Ensure there's a token end at the current position, and that i points at it + while (at < end) { + var i_end = st[i]; + if (i_end > end) + st.splice(i, 1, end, st[i+1], i_end); + i += 2; + at = Math.min(end, i_end); + } + if (!style) return; + if (overlay.opaque) { + st.splice(start, i - start, end, style); + i = start + 2; + } else { + for (; start < i; start += 2) { + var cur = st[start+1]; + st[start+1] = cur ? cur + " " + style : style; + } + } + }); + } + + return st; + } + + function getLineStyles(cm, line) { + if (!line.styles || line.styles[0] != cm.state.modeGen) + line.styles = highlightLine(cm, line, line.stateAfter = getStateBefore(cm, lineNo(line))); + return line.styles; + } + + // Lightweight form of highlight -- proceed over this line and + // update state, but don't save a style array. + function processLine(cm, text, state, startAt) { + var mode = cm.doc.mode; + var stream = new StringStream(text, cm.options.tabSize); + stream.start = stream.pos = startAt || 0; + if (text == "" && mode.blankLine) mode.blankLine(state); + while (!stream.eol() && stream.pos <= cm.options.maxHighlightLength) { + mode.token(stream, state); + stream.start = stream.pos; + } + } + + var styleToClassCache = {}, styleToClassCacheWithMode = {}; + function interpretTokenStyle(style, builder) { + if (!style) return null; + for (;;) { + var lineClass = style.match(/(?:^|\s)line-(background-)?(\S+)/); + if (!lineClass) break; + style = style.slice(0, lineClass.index) + style.slice(lineClass.index + lineClass[0].length); + var prop = lineClass[1] ? "bgClass" : "textClass"; + if (builder[prop] == null) + builder[prop] = lineClass[2]; + else if (!(new RegExp("(?:^|\s)" + lineClass[2] + "(?:$|\s)")).test(builder[prop])) + builder[prop] += " " + lineClass[2]; + } + var cache = builder.cm.options.addModeClass ? styleToClassCacheWithMode : styleToClassCache; + return cache[style] || + (cache[style] = "cm-" + style.replace(/ +/g, " cm-")); + } + + function buildLineContent(cm, realLine, measure, copyWidgets) { + var merged, line = realLine, empty = true; + while (merged = collapsedSpanAtStart(line)) + line = getLine(cm.doc, merged.find().from.line); + + var builder = {pre: elt("pre"), col: 0, pos: 0, + measure: null, measuredSomething: false, cm: cm, + copyWidgets: copyWidgets}; + + do { + if (line.text) empty = false; + builder.measure = line == realLine && measure; + builder.pos = 0; + builder.addToken = builder.measure ? buildTokenMeasure : buildToken; + if ((old_ie || webkit) && cm.getOption("lineWrapping")) + builder.addToken = buildTokenSplitSpaces(builder.addToken); + var next = insertLineContent(line, builder, getLineStyles(cm, line)); + if (measure && line == realLine && !builder.measuredSomething) { + measure[0] = builder.pre.appendChild(zeroWidthElement(cm.display.measure)); + builder.measuredSomething = true; + } + if (next) line = getLine(cm.doc, next.to.line); + } while (next); + + if (measure && !builder.measuredSomething && !measure[0]) + measure[0] = builder.pre.appendChild(empty ? elt("span", "\u00a0") : zeroWidthElement(cm.display.measure)); + if (!builder.pre.firstChild && !lineIsHidden(cm.doc, realLine)) + builder.pre.appendChild(document.createTextNode("\u00a0")); + + var order; + // Work around problem with the reported dimensions of single-char + // direction spans on IE (issue #1129). See also the comment in + // cursorCoords. + if (measure && ie && (order = getOrder(line))) { + var l = order.length - 1; + if (order[l].from == order[l].to) --l; + var last = order[l], prev = order[l - 1]; + if (last.from + 1 == last.to && prev && last.level < prev.level) { + var span = measure[builder.pos - 1]; + if (span) span.parentNode.insertBefore(span.measureRight = zeroWidthElement(cm.display.measure), + span.nextSibling); + } + } + + var textClass = builder.textClass ? builder.textClass + " " + (realLine.textClass || "") : realLine.textClass; + if (textClass) builder.pre.className = textClass; + + signal(cm, "renderLine", cm, realLine, builder.pre); + return builder; + } + + function defaultSpecialCharPlaceholder(ch) { + var token = elt("span", "\u2022", "cm-invalidchar"); + token.title = "\\u" + ch.charCodeAt(0).toString(16); + return token; + } + + function buildToken(builder, text, style, startStyle, endStyle, title) { + if (!text) return; + var special = builder.cm.options.specialChars; + if (!special.test(text)) { + builder.col += text.length; + var content = document.createTextNode(text); + } else { + var content = document.createDocumentFragment(), pos = 0; + while (true) { + special.lastIndex = pos; + var m = special.exec(text); + var skipped = m ? m.index - pos : text.length - pos; + if (skipped) { + content.appendChild(document.createTextNode(text.slice(pos, pos + skipped))); + builder.col += skipped; + } + if (!m) break; + pos += skipped + 1; + if (m[0] == "\t") { + var tabSize = builder.cm.options.tabSize, tabWidth = tabSize - builder.col % tabSize; + content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab")); + builder.col += tabWidth; + } else { + var token = builder.cm.options.specialCharPlaceholder(m[0]); + content.appendChild(token); + builder.col += 1; + } + } + } + if (style || startStyle || endStyle || builder.measure) { + var fullStyle = style || ""; + if (startStyle) fullStyle += startStyle; + if (endStyle) fullStyle += endStyle; + var token = elt("span", [content], fullStyle); + if (title) token.title = title; + return builder.pre.appendChild(token); + } + builder.pre.appendChild(content); + } + + function buildTokenMeasure(builder, text, style, startStyle, endStyle) { + var wrapping = builder.cm.options.lineWrapping; + for (var i = 0; i < text.length; ++i) { + var start = i == 0, to = i + 1; + while (to < text.length && isExtendingChar(text.charAt(to))) ++to; + var ch = text.slice(i, to); + i = to - 1; + if (i && wrapping && spanAffectsWrapping(text, i)) + builder.pre.appendChild(elt("wbr")); + var old = builder.measure[builder.pos]; + var span = builder.measure[builder.pos] = + buildToken(builder, ch, style, + start && startStyle, i == text.length - 1 && endStyle); + if (old) span.leftSide = old.leftSide || old; + // In IE single-space nodes wrap differently than spaces + // embedded in larger text nodes, except when set to + // white-space: normal (issue #1268). + if (old_ie && wrapping && ch == " " && i && !/\s/.test(text.charAt(i - 1)) && + i < text.length - 1 && !/\s/.test(text.charAt(i + 1))) + span.style.whiteSpace = "normal"; + builder.pos += ch.length; + } + if (text.length) builder.measuredSomething = true; + } + + function buildTokenSplitSpaces(inner) { + function split(old) { + var out = " "; + for (var i = 0; i < old.length - 2; ++i) out += i % 2 ? " " : "\u00a0"; + out += " "; + return out; + } + return function(builder, text, style, startStyle, endStyle, title) { + return inner(builder, text.replace(/ {3,}/g, split), style, startStyle, endStyle, title); + }; + } + + function buildCollapsedSpan(builder, size, marker, ignoreWidget) { + var widget = !ignoreWidget && marker.replacedWith; + if (widget) { + if (builder.copyWidgets) widget = widget.cloneNode(true); + builder.pre.appendChild(widget); + if (builder.measure) { + if (size) { + builder.measure[builder.pos] = widget; + } else { + var elt = zeroWidthElement(builder.cm.display.measure); + if (marker.type == "bookmark" && !marker.insertLeft) + builder.measure[builder.pos] = builder.pre.appendChild(elt); + else if (builder.measure[builder.pos]) + return; + else + builder.measure[builder.pos] = builder.pre.insertBefore(elt, widget); + } + builder.measuredSomething = true; + } + } + builder.pos += size; + } + + // Outputs a number of spans to make up a line, taking highlighting + // and marked text into account. + function insertLineContent(line, builder, styles) { + var spans = line.markedSpans, allText = line.text, at = 0; + if (!spans) { + for (var i = 1; i < styles.length; i+=2) + builder.addToken(builder, allText.slice(at, at = styles[i]), interpretTokenStyle(styles[i+1], builder)); + return; + } + + var len = allText.length, pos = 0, i = 1, text = "", style; + var nextChange = 0, spanStyle, spanEndStyle, spanStartStyle, title, collapsed; + for (;;) { + if (nextChange == pos) { // Update current marker set + spanStyle = spanEndStyle = spanStartStyle = title = ""; + collapsed = null; nextChange = Infinity; + var foundBookmarks = []; + for (var j = 0; j < spans.length; ++j) { + var sp = spans[j], m = sp.marker; + if (sp.from <= pos && (sp.to == null || sp.to > pos)) { + if (sp.to != null && nextChange > sp.to) { nextChange = sp.to; spanEndStyle = ""; } + if (m.className) spanStyle += " " + m.className; + if (m.startStyle && sp.from == pos) spanStartStyle += " " + m.startStyle; + if (m.endStyle && sp.to == nextChange) spanEndStyle += " " + m.endStyle; + if (m.title && !title) title = m.title; + if (m.collapsed && (!collapsed || compareCollapsedMarkers(collapsed.marker, m) < 0)) + collapsed = sp; + } else if (sp.from > pos && nextChange > sp.from) { + nextChange = sp.from; + } + if (m.type == "bookmark" && sp.from == pos && m.replacedWith) foundBookmarks.push(m); + } + if (collapsed && (collapsed.from || 0) == pos) { + buildCollapsedSpan(builder, (collapsed.to == null ? len : collapsed.to) - pos, + collapsed.marker, collapsed.from == null); + if (collapsed.to == null) return collapsed.marker.find(); + } + if (!collapsed && foundBookmarks.length) for (var j = 0; j < foundBookmarks.length; ++j) + buildCollapsedSpan(builder, 0, foundBookmarks[j]); + } + if (pos >= len) break; + + var upto = Math.min(len, nextChange); + while (true) { + if (text) { + var end = pos + text.length; + if (!collapsed) { + var tokenText = end > upto ? text.slice(0, upto - pos) : text; + builder.addToken(builder, tokenText, style ? style + spanStyle : spanStyle, + spanStartStyle, pos + tokenText.length == nextChange ? spanEndStyle : "", title); + } + if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;} + pos = end; + spanStartStyle = ""; + } + text = allText.slice(at, at = styles[i++]); + style = interpretTokenStyle(styles[i++], builder); + } + } + } + + // DOCUMENT DATA STRUCTURE + + function updateDoc(doc, change, markedSpans, selAfter, estimateHeight) { + function spansFor(n) {return markedSpans ? markedSpans[n] : null;} + function update(line, text, spans) { + updateLine(line, text, spans, estimateHeight); + signalLater(line, "change", line, change); + } + + var from = change.from, to = change.to, text = change.text; + var firstLine = getLine(doc, from.line), lastLine = getLine(doc, to.line); + var lastText = lst(text), lastSpans = spansFor(text.length - 1), nlines = to.line - from.line; + + // First adjust the line structure + if (from.ch == 0 && to.ch == 0 && lastText == "" && + (!doc.cm || doc.cm.options.wholeLineUpdateBefore)) { + // This is a whole-line replace. Treated specially to make + // sure line objects move the way they are supposed to. + for (var i = 0, e = text.length - 1, added = []; i < e; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + update(lastLine, lastLine.text, lastSpans); + if (nlines) doc.remove(from.line, nlines); + if (added.length) doc.insert(from.line, added); + } else if (firstLine == lastLine) { + if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + lastText + firstLine.text.slice(to.ch), lastSpans); + } else { + for (var added = [], i = 1, e = text.length - 1; i < e; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + added.push(new Line(lastText + firstLine.text.slice(to.ch), lastSpans, estimateHeight)); + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + doc.insert(from.line + 1, added); + } + } else if (text.length == 1) { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0] + lastLine.text.slice(to.ch), spansFor(0)); + doc.remove(from.line + 1, nlines); + } else { + update(firstLine, firstLine.text.slice(0, from.ch) + text[0], spansFor(0)); + update(lastLine, lastText + lastLine.text.slice(to.ch), lastSpans); + for (var i = 1, e = text.length - 1, added = []; i < e; ++i) + added.push(new Line(text[i], spansFor(i), estimateHeight)); + if (nlines > 1) doc.remove(from.line + 1, nlines - 1); + doc.insert(from.line + 1, added); + } + + signalLater(doc, "change", doc, change); + setSelection(doc, selAfter.anchor, selAfter.head, null, true); + } + + function LeafChunk(lines) { + this.lines = lines; + this.parent = null; + for (var i = 0, e = lines.length, height = 0; i < e; ++i) { + lines[i].parent = this; + height += lines[i].height; + } + this.height = height; + } + + LeafChunk.prototype = { + chunkSize: function() { return this.lines.length; }, + removeInner: function(at, n) { + for (var i = at, e = at + n; i < e; ++i) { + var line = this.lines[i]; + this.height -= line.height; + cleanUpLine(line); + signalLater(line, "delete"); + } + this.lines.splice(at, n); + }, + collapse: function(lines) { + lines.splice.apply(lines, [lines.length, 0].concat(this.lines)); + }, + insertInner: function(at, lines, height) { + this.height += height; + this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at)); + for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this; + }, + iterN: function(at, n, op) { + for (var e = at + n; at < e; ++at) + if (op(this.lines[at])) return true; + } + }; + + function BranchChunk(children) { + this.children = children; + var size = 0, height = 0; + for (var i = 0, e = children.length; i < e; ++i) { + var ch = children[i]; + size += ch.chunkSize(); height += ch.height; + ch.parent = this; + } + this.size = size; + this.height = height; + this.parent = null; + } + + BranchChunk.prototype = { + chunkSize: function() { return this.size; }, + removeInner: function(at, n) { + this.size -= n; + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var rm = Math.min(n, sz - at), oldHeight = child.height; + child.removeInner(at, rm); + this.height -= oldHeight - child.height; + if (sz == rm) { this.children.splice(i--, 1); child.parent = null; } + if ((n -= rm) == 0) break; + at = 0; + } else at -= sz; + } + if (this.size - n < 25) { + var lines = []; + this.collapse(lines); + this.children = [new LeafChunk(lines)]; + this.children[0].parent = this; + } + }, + collapse: function(lines) { + for (var i = 0, e = this.children.length; i < e; ++i) this.children[i].collapse(lines); + }, + insertInner: function(at, lines, height) { + this.size += lines.length; + this.height += height; + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at <= sz) { + child.insertInner(at, lines, height); + if (child.lines && child.lines.length > 50) { + while (child.lines.length > 50) { + var spilled = child.lines.splice(child.lines.length - 25, 25); + var newleaf = new LeafChunk(spilled); + child.height -= newleaf.height; + this.children.splice(i + 1, 0, newleaf); + newleaf.parent = this; + } + this.maybeSpill(); + } + break; + } + at -= sz; + } + }, + maybeSpill: function() { + if (this.children.length <= 10) return; + var me = this; + do { + var spilled = me.children.splice(me.children.length - 5, 5); + var sibling = new BranchChunk(spilled); + if (!me.parent) { // Become the parent node + var copy = new BranchChunk(me.children); + copy.parent = me; + me.children = [copy, sibling]; + me = copy; + } else { + me.size -= sibling.size; + me.height -= sibling.height; + var myIndex = indexOf(me.parent.children, me); + me.parent.children.splice(myIndex + 1, 0, sibling); + } + sibling.parent = me.parent; + } while (me.children.length > 10); + me.parent.maybeSpill(); + }, + iterN: function(at, n, op) { + for (var i = 0, e = this.children.length; i < e; ++i) { + var child = this.children[i], sz = child.chunkSize(); + if (at < sz) { + var used = Math.min(n, sz - at); + if (child.iterN(at, used, op)) return true; + if ((n -= used) == 0) break; + at = 0; + } else at -= sz; + } + } + }; + + var nextDocId = 0; + var Doc = CodeMirror.Doc = function(text, mode, firstLine) { + if (!(this instanceof Doc)) return new Doc(text, mode, firstLine); + if (firstLine == null) firstLine = 0; + + BranchChunk.call(this, [new LeafChunk([new Line("", null)])]); + this.first = firstLine; + this.scrollTop = this.scrollLeft = 0; + this.cantEdit = false; + this.history = makeHistory(); + this.cleanGeneration = 1; + this.frontier = firstLine; + var start = Pos(firstLine, 0); + this.sel = {from: start, to: start, head: start, anchor: start, shift: false, extend: false, goalColumn: null}; + this.id = ++nextDocId; + this.modeOption = mode; + + if (typeof text == "string") text = splitLines(text); + updateDoc(this, {from: start, to: start, text: text}, null, {head: start, anchor: start}); + }; + + Doc.prototype = createObj(BranchChunk.prototype, { + constructor: Doc, + iter: function(from, to, op) { + if (op) this.iterN(from - this.first, to - from, op); + else this.iterN(this.first, this.first + this.size, from); + }, + + insert: function(at, lines) { + var height = 0; + for (var i = 0, e = lines.length; i < e; ++i) height += lines[i].height; + this.insertInner(at - this.first, lines, height); + }, + remove: function(at, n) { this.removeInner(at - this.first, n); }, + + getValue: function(lineSep) { + var lines = getLines(this, this.first, this.first + this.size); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + setValue: function(code) { + var top = Pos(this.first, 0), last = this.first + this.size - 1; + makeChange(this, {from: top, to: Pos(last, getLine(this, last).text.length), + text: splitLines(code), origin: "setValue"}, + {head: top, anchor: top}, true); + }, + replaceRange: function(code, from, to, origin) { + from = clipPos(this, from); + to = to ? clipPos(this, to) : from; + replaceRange(this, code, from, to, origin); + }, + getRange: function(from, to, lineSep) { + var lines = getBetween(this, clipPos(this, from), clipPos(this, to)); + if (lineSep === false) return lines; + return lines.join(lineSep || "\n"); + }, + + getLine: function(line) {var l = this.getLineHandle(line); return l && l.text;}, + setLine: function(line, text) { + if (isLine(this, line)) + replaceRange(this, text, Pos(line, 0), clipPos(this, Pos(line))); + }, + removeLine: function(line) { + if (line) replaceRange(this, "", clipPos(this, Pos(line - 1)), clipPos(this, Pos(line))); + else replaceRange(this, "", Pos(0, 0), clipPos(this, Pos(1, 0))); + }, + + getLineHandle: function(line) {if (isLine(this, line)) return getLine(this, line);}, + getLineNumber: function(line) {return lineNo(line);}, + + getLineHandleVisualStart: function(line) { + if (typeof line == "number") line = getLine(this, line); + return visualLine(this, line); + }, + + lineCount: function() {return this.size;}, + firstLine: function() {return this.first;}, + lastLine: function() {return this.first + this.size - 1;}, + + clipPos: function(pos) {return clipPos(this, pos);}, + + getCursor: function(start) { + var sel = this.sel, pos; + if (start == null || start == "head") pos = sel.head; + else if (start == "anchor") pos = sel.anchor; + else if (start == "end" || start === false) pos = sel.to; + else pos = sel.from; + return copyPos(pos); + }, + somethingSelected: function() {return !posEq(this.sel.head, this.sel.anchor);}, + + setCursor: docOperation(function(line, ch, extend) { + var pos = clipPos(this, typeof line == "number" ? Pos(line, ch || 0) : line); + if (extend) extendSelection(this, pos); + else setSelection(this, pos, pos); + }), + setSelection: docOperation(function(anchor, head, bias) { + setSelection(this, clipPos(this, anchor), clipPos(this, head || anchor), bias); + }), + extendSelection: docOperation(function(from, to, bias) { + extendSelection(this, clipPos(this, from), to && clipPos(this, to), bias); + }), + + getSelection: function(lineSep) {return this.getRange(this.sel.from, this.sel.to, lineSep);}, + replaceSelection: function(code, collapse, origin) { + makeChange(this, {from: this.sel.from, to: this.sel.to, text: splitLines(code), origin: origin}, collapse || "around"); + }, + undo: docOperation(function() {makeChangeFromHistory(this, "undo");}), + redo: docOperation(function() {makeChangeFromHistory(this, "redo");}), + + setExtending: function(val) {this.sel.extend = val;}, + + historySize: function() { + var hist = this.history; + return {undo: hist.done.length, redo: hist.undone.length}; + }, + clearHistory: function() {this.history = makeHistory(this.history.maxGeneration);}, + + markClean: function() { + this.cleanGeneration = this.changeGeneration(true); + }, + changeGeneration: function(forceSplit) { + if (forceSplit) + this.history.lastOp = this.history.lastOrigin = null; + return this.history.generation; + }, + isClean: function (gen) { + return this.history.generation == (gen || this.cleanGeneration); + }, + + getHistory: function() { + return {done: copyHistoryArray(this.history.done), + undone: copyHistoryArray(this.history.undone)}; + }, + setHistory: function(histData) { + var hist = this.history = makeHistory(this.history.maxGeneration); + hist.done = histData.done.slice(0); + hist.undone = histData.undone.slice(0); + }, + + markText: function(from, to, options) { + return markText(this, clipPos(this, from), clipPos(this, to), options, "range"); + }, + setBookmark: function(pos, options) { + var realOpts = {replacedWith: options && (options.nodeType == null ? options.widget : options), + insertLeft: options && options.insertLeft, + clearWhenEmpty: false}; + pos = clipPos(this, pos); + return markText(this, pos, pos, realOpts, "bookmark"); + }, + findMarksAt: function(pos) { + pos = clipPos(this, pos); + var markers = [], spans = getLine(this, pos.line).markedSpans; + if (spans) for (var i = 0; i < spans.length; ++i) { + var span = spans[i]; + if ((span.from == null || span.from <= pos.ch) && + (span.to == null || span.to >= pos.ch)) + markers.push(span.marker.parent || span.marker); + } + return markers; + }, + getAllMarks: function() { + var markers = []; + this.iter(function(line) { + var sps = line.markedSpans; + if (sps) for (var i = 0; i < sps.length; ++i) + if (sps[i].from != null) markers.push(sps[i].marker); + }); + return markers; + }, + + posFromIndex: function(off) { + var ch, lineNo = this.first; + this.iter(function(line) { + var sz = line.text.length + 1; + if (sz > off) { ch = off; return true; } + off -= sz; + ++lineNo; + }); + return clipPos(this, Pos(lineNo, ch)); + }, + indexFromPos: function (coords) { + coords = clipPos(this, coords); + var index = coords.ch; + if (coords.line < this.first || coords.ch < 0) return 0; + this.iter(this.first, coords.line, function (line) { + index += line.text.length + 1; + }); + return index; + }, + + copy: function(copyHistory) { + var doc = new Doc(getLines(this, this.first, this.first + this.size), this.modeOption, this.first); + doc.scrollTop = this.scrollTop; doc.scrollLeft = this.scrollLeft; + doc.sel = {from: this.sel.from, to: this.sel.to, head: this.sel.head, anchor: this.sel.anchor, + shift: this.sel.shift, extend: false, goalColumn: this.sel.goalColumn}; + if (copyHistory) { + doc.history.undoDepth = this.history.undoDepth; + doc.setHistory(this.getHistory()); + } + return doc; + }, + + linkedDoc: function(options) { + if (!options) options = {}; + var from = this.first, to = this.first + this.size; + if (options.from != null && options.from > from) from = options.from; + if (options.to != null && options.to < to) to = options.to; + var copy = new Doc(getLines(this, from, to), options.mode || this.modeOption, from); + if (options.sharedHist) copy.history = this.history; + (this.linked || (this.linked = [])).push({doc: copy, sharedHist: options.sharedHist}); + copy.linked = [{doc: this, isParent: true, sharedHist: options.sharedHist}]; + return copy; + }, + unlinkDoc: function(other) { + if (other instanceof CodeMirror) other = other.doc; + if (this.linked) for (var i = 0; i < this.linked.length; ++i) { + var link = this.linked[i]; + if (link.doc != other) continue; + this.linked.splice(i, 1); + other.unlinkDoc(this); + break; + } + // If the histories were shared, split them again + if (other.history == this.history) { + var splitIds = [other.id]; + linkedDocs(other, function(doc) {splitIds.push(doc.id);}, true); + other.history = makeHistory(); + other.history.done = copyHistoryArray(this.history.done, splitIds); + other.history.undone = copyHistoryArray(this.history.undone, splitIds); + } + }, + iterLinkedDocs: function(f) {linkedDocs(this, f);}, + + getMode: function() {return this.mode;}, + getEditor: function() {return this.cm;} + }); + + Doc.prototype.eachLine = Doc.prototype.iter; + + // The Doc methods that should be available on CodeMirror instances + var dontDelegate = "iter insert remove copy getEditor".split(" "); + for (var prop in Doc.prototype) if (Doc.prototype.hasOwnProperty(prop) && indexOf(dontDelegate, prop) < 0) + CodeMirror.prototype[prop] = (function(method) { + return function() {return method.apply(this.doc, arguments);}; + })(Doc.prototype[prop]); + + eventMixin(Doc); + + function linkedDocs(doc, f, sharedHistOnly) { + function propagate(doc, skip, sharedHist) { + if (doc.linked) for (var i = 0; i < doc.linked.length; ++i) { + var rel = doc.linked[i]; + if (rel.doc == skip) continue; + var shared = sharedHist && rel.sharedHist; + if (sharedHistOnly && !shared) continue; + f(rel.doc, shared); + propagate(rel.doc, doc, shared); + } + } + propagate(doc, null, true); + } + + function attachDoc(cm, doc) { + if (doc.cm) throw new Error("This document is already in use."); + cm.doc = doc; + doc.cm = cm; + estimateLineHeights(cm); + loadMode(cm); + if (!cm.options.lineWrapping) computeMaxLength(cm); + cm.options.mode = doc.modeOption; + regChange(cm); + } + + // LINE UTILITIES + + function getLine(chunk, n) { + n -= chunk.first; + while (!chunk.lines) { + for (var i = 0;; ++i) { + var child = chunk.children[i], sz = child.chunkSize(); + if (n < sz) { chunk = child; break; } + n -= sz; + } + } + return chunk.lines[n]; + } + + function getBetween(doc, start, end) { + var out = [], n = start.line; + doc.iter(start.line, end.line + 1, function(line) { + var text = line.text; + if (n == end.line) text = text.slice(0, end.ch); + if (n == start.line) text = text.slice(start.ch); + out.push(text); + ++n; + }); + return out; + } + function getLines(doc, from, to) { + var out = []; + doc.iter(from, to, function(line) { out.push(line.text); }); + return out; + } + + function updateLineHeight(line, height) { + var diff = height - line.height; + for (var n = line; n; n = n.parent) n.height += diff; + } + + function lineNo(line) { + if (line.parent == null) return null; + var cur = line.parent, no = indexOf(cur.lines, line); + for (var chunk = cur.parent; chunk; cur = chunk, chunk = chunk.parent) { + for (var i = 0;; ++i) { + if (chunk.children[i] == cur) break; + no += chunk.children[i].chunkSize(); + } + } + return no + cur.first; + } + + function lineAtHeight(chunk, h) { + var n = chunk.first; + outer: do { + for (var i = 0, e = chunk.children.length; i < e; ++i) { + var child = chunk.children[i], ch = child.height; + if (h < ch) { chunk = child; continue outer; } + h -= ch; + n += child.chunkSize(); + } + return n; + } while (!chunk.lines); + for (var i = 0, e = chunk.lines.length; i < e; ++i) { + var line = chunk.lines[i], lh = line.height; + if (h < lh) break; + h -= lh; + } + return n + i; + } + + function heightAtLine(cm, lineObj) { + lineObj = visualLine(cm.doc, lineObj); + + var h = 0, chunk = lineObj.parent; + for (var i = 0; i < chunk.lines.length; ++i) { + var line = chunk.lines[i]; + if (line == lineObj) break; + else h += line.height; + } + for (var p = chunk.parent; p; chunk = p, p = chunk.parent) { + for (var i = 0; i < p.children.length; ++i) { + var cur = p.children[i]; + if (cur == chunk) break; + else h += cur.height; + } + } + return h; + } + + function getOrder(line) { + var order = line.order; + if (order == null) order = line.order = bidiOrdering(line.text); + return order; + } + + // HISTORY + + function makeHistory(startGen) { + return { + // Arrays of history events. Doing something adds an event to + // done and clears undo. Undoing moves events from done to + // undone, redoing moves them in the other direction. + done: [], undone: [], undoDepth: Infinity, + // Used to track when changes can be merged into a single undo + // event + lastTime: 0, lastOp: null, lastOrigin: null, + // Used by the isClean() method + generation: startGen || 1, maxGeneration: startGen || 1 + }; + } + + function attachLocalSpans(doc, change, from, to) { + var existing = change["spans_" + doc.id], n = 0; + doc.iter(Math.max(doc.first, from), Math.min(doc.first + doc.size, to), function(line) { + if (line.markedSpans) + (existing || (existing = change["spans_" + doc.id] = {}))[n] = line.markedSpans; + ++n; + }); + } + + function historyChangeFromChange(doc, change) { + var from = { line: change.from.line, ch: change.from.ch }; + var histChange = {from: from, to: changeEnd(change), text: getBetween(doc, change.from, change.to)}; + attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1); + linkedDocs(doc, function(doc) {attachLocalSpans(doc, histChange, change.from.line, change.to.line + 1);}, true); + return histChange; + } + + function addToHistory(doc, change, selAfter, opId) { + var hist = doc.history; + hist.undone.length = 0; + var time = +new Date, cur = lst(hist.done); + + if (cur && + (hist.lastOp == opId || + hist.lastOrigin == change.origin && change.origin && + ((change.origin.charAt(0) == "+" && doc.cm && hist.lastTime > time - doc.cm.options.historyEventDelay) || + change.origin.charAt(0) == "*"))) { + // Merge this change into the last event + var last = lst(cur.changes); + if (posEq(change.from, change.to) && posEq(change.from, last.to)) { + // Optimized case for simple insertion -- don't want to add + // new changesets for every character typed + last.to = changeEnd(change); + } else { + // Add new sub-event + cur.changes.push(historyChangeFromChange(doc, change)); + } + cur.anchorAfter = selAfter.anchor; cur.headAfter = selAfter.head; + } else { + // Can not be merged, start a new event. + cur = {changes: [historyChangeFromChange(doc, change)], + generation: hist.generation, + anchorBefore: doc.sel.anchor, headBefore: doc.sel.head, + anchorAfter: selAfter.anchor, headAfter: selAfter.head}; + hist.done.push(cur); + while (hist.done.length > hist.undoDepth) + hist.done.shift(); + } + hist.generation = ++hist.maxGeneration; + hist.lastTime = time; + hist.lastOp = opId; + hist.lastOrigin = change.origin; + } + + function removeClearedSpans(spans) { + if (!spans) return null; + for (var i = 0, out; i < spans.length; ++i) { + if (spans[i].marker.explicitlyCleared) { if (!out) out = spans.slice(0, i); } + else if (out) out.push(spans[i]); + } + return !out ? spans : out.length ? out : null; + } + + function getOldSpans(doc, change) { + var found = change["spans_" + doc.id]; + if (!found) return null; + for (var i = 0, nw = []; i < change.text.length; ++i) + nw.push(removeClearedSpans(found[i])); + return nw; + } + + // Used both to provide a JSON-safe object in .getHistory, and, when + // detaching a document, to split the history in two + function copyHistoryArray(events, newGroup) { + for (var i = 0, copy = []; i < events.length; ++i) { + var event = events[i], changes = event.changes, newChanges = []; + copy.push({changes: newChanges, anchorBefore: event.anchorBefore, headBefore: event.headBefore, + anchorAfter: event.anchorAfter, headAfter: event.headAfter}); + for (var j = 0; j < changes.length; ++j) { + var change = changes[j], m; + newChanges.push({from: change.from, to: change.to, text: change.text}); + if (newGroup) for (var prop in change) if (m = prop.match(/^spans_(\d+)$/)) { + if (indexOf(newGroup, Number(m[1])) > -1) { + lst(newChanges)[prop] = change[prop]; + delete change[prop]; + } + } + } + } + return copy; + } + + // Rebasing/resetting history to deal with externally-sourced changes + + function rebaseHistSel(pos, from, to, diff) { + if (to < pos.line) { + pos.line += diff; + } else if (from < pos.line) { + pos.line = from; + pos.ch = 0; + } + } + + // Tries to rebase an array of history events given a change in the + // document. If the change touches the same lines as the event, the + // event, and everything 'behind' it, is discarded. If the change is + // before the event, the event's positions are updated. Uses a + // copy-on-write scheme for the positions, to avoid having to + // reallocate them all on every rebase, but also avoid problems with + // shared position objects being unsafely updated. + function rebaseHistArray(array, from, to, diff) { + for (var i = 0; i < array.length; ++i) { + var sub = array[i], ok = true; + for (var j = 0; j < sub.changes.length; ++j) { + var cur = sub.changes[j]; + if (!sub.copied) { cur.from = copyPos(cur.from); cur.to = copyPos(cur.to); } + if (to < cur.from.line) { + cur.from.line += diff; + cur.to.line += diff; + } else if (from <= cur.to.line) { + ok = false; + break; + } + } + if (!sub.copied) { + sub.anchorBefore = copyPos(sub.anchorBefore); sub.headBefore = copyPos(sub.headBefore); + sub.anchorAfter = copyPos(sub.anchorAfter); sub.readAfter = copyPos(sub.headAfter); + sub.copied = true; + } + if (!ok) { + array.splice(0, i + 1); + i = 0; + } else { + rebaseHistSel(sub.anchorBefore); rebaseHistSel(sub.headBefore); + rebaseHistSel(sub.anchorAfter); rebaseHistSel(sub.headAfter); + } + } + } + + function rebaseHist(hist, change) { + var from = change.from.line, to = change.to.line, diff = change.text.length - (to - from) - 1; + rebaseHistArray(hist.done, from, to, diff); + rebaseHistArray(hist.undone, from, to, diff); + } + + // EVENT OPERATORS + + function stopMethod() {e_stop(this);} + // Ensure an event has a stop method. + function addStop(event) { + if (!event.stop) event.stop = stopMethod; + return event; + } + + function e_preventDefault(e) { + if (e.preventDefault) e.preventDefault(); + else e.returnValue = false; + } + function e_stopPropagation(e) { + if (e.stopPropagation) e.stopPropagation(); + else e.cancelBubble = true; + } + function e_defaultPrevented(e) { + return e.defaultPrevented != null ? e.defaultPrevented : e.returnValue == false; + } + function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);} + CodeMirror.e_stop = e_stop; + CodeMirror.e_preventDefault = e_preventDefault; + CodeMirror.e_stopPropagation = e_stopPropagation; + + function e_target(e) {return e.target || e.srcElement;} + function e_button(e) { + var b = e.which; + if (b == null) { + if (e.button & 1) b = 1; + else if (e.button & 2) b = 3; + else if (e.button & 4) b = 2; + } + if (mac && e.ctrlKey && b == 1) b = 3; + return b; + } + + // EVENT HANDLING + + function on(emitter, type, f) { + if (emitter.addEventListener) + emitter.addEventListener(type, f, false); + else if (emitter.attachEvent) + emitter.attachEvent("on" + type, f); + else { + var map = emitter._handlers || (emitter._handlers = {}); + var arr = map[type] || (map[type] = []); + arr.push(f); + } + } + + function off(emitter, type, f) { + if (emitter.removeEventListener) + emitter.removeEventListener(type, f, false); + else if (emitter.detachEvent) + emitter.detachEvent("on" + type, f); + else { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + for (var i = 0; i < arr.length; ++i) + if (arr[i] == f) { arr.splice(i, 1); break; } + } + } + + function signal(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + for (var i = 0; i < arr.length; ++i) arr[i].apply(null, args); + } + + var delayedCallbacks, delayedCallbackDepth = 0; + function signalLater(emitter, type /*, values...*/) { + var arr = emitter._handlers && emitter._handlers[type]; + if (!arr) return; + var args = Array.prototype.slice.call(arguments, 2); + if (!delayedCallbacks) { + ++delayedCallbackDepth; + delayedCallbacks = []; + setTimeout(fireDelayed, 0); + } + function bnd(f) {return function(){f.apply(null, args);};}; + for (var i = 0; i < arr.length; ++i) + delayedCallbacks.push(bnd(arr[i])); + } + + function signalDOMEvent(cm, e, override) { + signal(cm, override || e.type, cm, e); + return e_defaultPrevented(e) || e.codemirrorIgnore; + } + + function fireDelayed() { + --delayedCallbackDepth; + var delayed = delayedCallbacks; + delayedCallbacks = null; + for (var i = 0; i < delayed.length; ++i) delayed[i](); + } + + function hasHandler(emitter, type) { + var arr = emitter._handlers && emitter._handlers[type]; + return arr && arr.length > 0; + } + + CodeMirror.on = on; CodeMirror.off = off; CodeMirror.signal = signal; + + function eventMixin(ctor) { + ctor.prototype.on = function(type, f) {on(this, type, f);}; + ctor.prototype.off = function(type, f) {off(this, type, f);}; + } + + // MISC UTILITIES + + // Number of pixels added to scroller and sizer to hide scrollbar + var scrollerCutOff = 30; + + // Returned or thrown by various protocols to signal 'I'm not + // handling this'. + var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}}; + + function Delayed() {this.id = null;} + Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}}; + + // Counts the column offset in a string, taking tabs into account. + // Used mostly to find indentation. + function countColumn(string, end, tabSize, startIndex, startValue) { + if (end == null) { + end = string.search(/[^\s\u00a0]/); + if (end == -1) end = string.length; + } + for (var i = startIndex || 0, n = startValue || 0; i < end; ++i) { + if (string.charAt(i) == "\t") n += tabSize - (n % tabSize); + else ++n; + } + return n; + } + CodeMirror.countColumn = countColumn; + + var spaceStrs = [""]; + function spaceStr(n) { + while (spaceStrs.length <= n) + spaceStrs.push(lst(spaceStrs) + " "); + return spaceStrs[n]; + } + + function lst(arr) { return arr[arr.length-1]; } + + function selectInput(node) { + if (ios) { // Mobile Safari apparently has a bug where select() is broken. + node.selectionStart = 0; + node.selectionEnd = node.value.length; + } else { + // Suppress mysterious IE10 errors + try { node.select(); } + catch(_e) {} + } + } + + function indexOf(collection, elt) { + if (collection.indexOf) return collection.indexOf(elt); + for (var i = 0, e = collection.length; i < e; ++i) + if (collection[i] == elt) return i; + return -1; + } + + function createObj(base, props) { + function Obj() {} + Obj.prototype = base; + var inst = new Obj(); + if (props) copyObj(props, inst); + return inst; + } + + function copyObj(obj, target) { + if (!target) target = {}; + for (var prop in obj) if (obj.hasOwnProperty(prop)) target[prop] = obj[prop]; + return target; + } + + function emptyArray(size) { + for (var a = [], i = 0; i < size; ++i) a.push(undefined); + return a; + } + + function bind(f) { + var args = Array.prototype.slice.call(arguments, 1); + return function(){return f.apply(null, args);}; + } + + var nonASCIISingleCaseWordChar = /[\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/; + function isWordChar(ch) { + return /\w/.test(ch) || ch > "\x80" && + (ch.toUpperCase() != ch.toLowerCase() || nonASCIISingleCaseWordChar.test(ch)); + } + + function isEmpty(obj) { + for (var n in obj) if (obj.hasOwnProperty(n) && obj[n]) return false; + return true; + } + + var extendingChars = /[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/; + function isExtendingChar(ch) { return ch.charCodeAt(0) >= 768 && extendingChars.test(ch); } + + // DOM UTILITIES + + function elt(tag, content, className, style) { + var e = document.createElement(tag); + if (className) e.className = className; + if (style) e.style.cssText = style; + if (typeof content == "string") setTextContent(e, content); + else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]); + return e; + } + + function removeChildren(e) { + for (var count = e.childNodes.length; count > 0; --count) + e.removeChild(e.firstChild); + return e; + } + + function removeChildrenAndAdd(parent, e) { + return removeChildren(parent).appendChild(e); + } + + function setTextContent(e, str) { + if (ie_lt9) { + e.innerHTML = ""; + e.appendChild(document.createTextNode(str)); + } else e.textContent = str; + } + + function getRect(node) { + return node.getBoundingClientRect(); + } + CodeMirror.replaceGetRect = function(f) { getRect = f; }; + + // FEATURE DETECTION + + // Detect drag-and-drop + var dragAndDrop = function() { + // There is *some* kind of drag-and-drop support in IE6-8, but I + // couldn't get it to work yet. + if (ie_lt9) return false; + var div = elt('div'); + return "draggable" in div || "dragDrop" in div; + }(); + + // For a reason I have yet to figure out, some browsers disallow + // word wrapping between certain characters *only* if a new inline + // element is started between them. This makes it hard to reliably + // measure the position of things, since that requires inserting an + // extra span. This terribly fragile set of tests matches the + // character combinations that suffer from this phenomenon on the + // various browsers. + function spanAffectsWrapping() { return false; } + if (gecko) // Only for "$'" + spanAffectsWrapping = function(str, i) { + return str.charCodeAt(i - 1) == 36 && str.charCodeAt(i) == 39; + }; + else if (safari && !/Version\/([6-9]|\d\d)\b/.test(navigator.userAgent)) + spanAffectsWrapping = function(str, i) { + return /\-[^ \-?]|\?[^ !\'\"\),.\-\/:;\?\]\}]/.test(str.slice(i - 1, i + 1)); + }; + else if (webkit && /Chrome\/(?:29|[3-9]\d|\d\d\d)\./.test(navigator.userAgent)) + spanAffectsWrapping = function(str, i) { + var code = str.charCodeAt(i - 1); + return code >= 8208 && code <= 8212; + }; + else if (webkit) + spanAffectsWrapping = function(str, i) { + if (i > 1 && str.charCodeAt(i - 1) == 45) { + if (/\w/.test(str.charAt(i - 2)) && /[^\-?\.]/.test(str.charAt(i))) return true; + if (i > 2 && /[\d\.,]/.test(str.charAt(i - 2)) && /[\d\.,]/.test(str.charAt(i))) return false; + } + return /[~!#%&*)=+}\]\\|\"\.>,:;][({[<]|-[^\-?\.\u2010-\u201f\u2026]|\?[\w~`@#$%\^&*(_=+{[|><]|\u2026[\w~`@#$%\^&*(_=+{[><]/.test(str.slice(i - 1, i + 1)); + }; + + var knownScrollbarWidth; + function scrollbarWidth(measure) { + if (knownScrollbarWidth != null) return knownScrollbarWidth; + var test = elt("div", null, null, "width: 50px; height: 50px; overflow-x: scroll"); + removeChildrenAndAdd(measure, test); + if (test.offsetWidth) + knownScrollbarWidth = test.offsetHeight - test.clientHeight; + return knownScrollbarWidth || 0; + } + + var zwspSupported; + function zeroWidthElement(measure) { + if (zwspSupported == null) { + var test = elt("span", "\u200b"); + removeChildrenAndAdd(measure, elt("span", [test, document.createTextNode("x")])); + if (measure.firstChild.offsetHeight != 0) + zwspSupported = test.offsetWidth <= 1 && test.offsetHeight > 2 && !ie_lt8; + } + if (zwspSupported) return elt("span", "\u200b"); + else return elt("span", "\u00a0", null, "display: inline-block; width: 1px; margin-right: -1px"); + } + + // See if "".split is the broken IE version, if so, provide an + // alternative way to split lines. + var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) { + var pos = 0, result = [], l = string.length; + while (pos <= l) { + var nl = string.indexOf("\n", pos); + if (nl == -1) nl = string.length; + var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl); + var rt = line.indexOf("\r"); + if (rt != -1) { + result.push(line.slice(0, rt)); + pos += rt + 1; + } else { + result.push(line); + pos = nl + 1; + } + } + return result; + } : function(string){return string.split(/\r\n?|\n/);}; + CodeMirror.splitLines = splitLines; + + var hasSelection = window.getSelection ? function(te) { + try { return te.selectionStart != te.selectionEnd; } + catch(e) { return false; } + } : function(te) { + try {var range = te.ownerDocument.selection.createRange();} + catch(e) {} + if (!range || range.parentElement() != te) return false; + return range.compareEndPoints("StartToEnd", range) != 0; + }; + + var hasCopyEvent = (function() { + var e = elt("div"); + if ("oncopy" in e) return true; + e.setAttribute("oncopy", "return;"); + return typeof e.oncopy == 'function'; + })(); + + // KEY NAMING + + var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt", + 19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End", + 36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert", + 46: "Delete", 59: ";", 61: "=", 91: "Mod", 92: "Mod", 93: "Mod", 107: "=", 109: "-", 127: "Delete", + 173: "-", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\", + 221: "]", 222: "'", 63232: "Up", 63233: "Down", 63234: "Left", 63235: "Right", 63272: "Delete", + 63273: "Home", 63275: "End", 63276: "PageUp", 63277: "PageDown", 63302: "Insert"}; + CodeMirror.keyNames = keyNames; + (function() { + // Number keys + for (var i = 0; i < 10; i++) keyNames[i + 48] = keyNames[i + 96] = String(i); + // Alphabetic keys + for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i); + // Function keys + for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i; + })(); + + // BIDI HELPERS + + function iterateBidiSections(order, from, to, f) { + if (!order) return f(from, to, "ltr"); + var found = false; + for (var i = 0; i < order.length; ++i) { + var part = order[i]; + if (part.from < to && part.to > from || from == to && part.to == from) { + f(Math.max(part.from, from), Math.min(part.to, to), part.level == 1 ? "rtl" : "ltr"); + found = true; + } + } + if (!found) f(from, to, "ltr"); + } + + function bidiLeft(part) { return part.level % 2 ? part.to : part.from; } + function bidiRight(part) { return part.level % 2 ? part.from : part.to; } + + function lineLeft(line) { var order = getOrder(line); return order ? bidiLeft(order[0]) : 0; } + function lineRight(line) { + var order = getOrder(line); + if (!order) return line.text.length; + return bidiRight(lst(order)); + } + + function lineStart(cm, lineN) { + var line = getLine(cm.doc, lineN); + var visual = visualLine(cm.doc, line); + if (visual != line) lineN = lineNo(visual); + var order = getOrder(visual); + var ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual); + return Pos(lineN, ch); + } + function lineEnd(cm, lineN) { + var merged, line; + while (merged = collapsedSpanAtEnd(line = getLine(cm.doc, lineN))) + lineN = merged.find().to.line; + var order = getOrder(line); + var ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line); + return Pos(lineN, ch); + } + + function compareBidiLevel(order, a, b) { + var linedir = order[0].level; + if (a == linedir) return true; + if (b == linedir) return false; + return a < b; + } + var bidiOther; + function getBidiPartAt(order, pos) { + bidiOther = null; + for (var i = 0, found; i < order.length; ++i) { + var cur = order[i]; + if (cur.from < pos && cur.to > pos) return i; + if ((cur.from == pos || cur.to == pos)) { + if (found == null) { + found = i; + } else if (compareBidiLevel(order, cur.level, order[found].level)) { + if (cur.from != cur.to) bidiOther = found; + return i; + } else { + if (cur.from != cur.to) bidiOther = i; + return found; + } + } + } + return found; + } + + function moveInLine(line, pos, dir, byUnit) { + if (!byUnit) return pos + dir; + do pos += dir; + while (pos > 0 && isExtendingChar(line.text.charAt(pos))); + return pos; + } + + // This is somewhat involved. It is needed in order to move + // 'visually' through bi-directional text -- i.e., pressing left + // should make the cursor go left, even when in RTL text. The + // tricky part is the 'jumps', where RTL and LTR text touch each + // other. This often requires the cursor offset to move more than + // one unit, in order to visually move one unit. + function moveVisually(line, start, dir, byUnit) { + var bidi = getOrder(line); + if (!bidi) return moveLogically(line, start, dir, byUnit); + var pos = getBidiPartAt(bidi, start), part = bidi[pos]; + var target = moveInLine(line, start, part.level % 2 ? -dir : dir, byUnit); + + for (;;) { + if (target > part.from && target < part.to) return target; + if (target == part.from || target == part.to) { + if (getBidiPartAt(bidi, target) == pos) return target; + part = bidi[pos += dir]; + return (dir > 0) == part.level % 2 ? part.to : part.from; + } else { + part = bidi[pos += dir]; + if (!part) return null; + if ((dir > 0) == part.level % 2) + target = moveInLine(line, part.to, -1, byUnit); + else + target = moveInLine(line, part.from, 1, byUnit); + } + } + } + + function moveLogically(line, start, dir, byUnit) { + var target = start + dir; + if (byUnit) while (target > 0 && isExtendingChar(line.text.charAt(target))) target += dir; + return target < 0 || target > line.text.length ? null : target; + } + + // Bidirectional ordering algorithm + // See http://unicode.org/reports/tr9/tr9-13.html for the algorithm + // that this (partially) implements. + + // One-char codes used for character types: + // L (L): Left-to-Right + // R (R): Right-to-Left + // r (AL): Right-to-Left Arabic + // 1 (EN): European Number + // + (ES): European Number Separator + // % (ET): European Number Terminator + // n (AN): Arabic Number + // , (CS): Common Number Separator + // m (NSM): Non-Spacing Mark + // b (BN): Boundary Neutral + // s (B): Paragraph Separator + // t (S): Segment Separator + // w (WS): Whitespace + // N (ON): Other Neutrals + + // Returns null if characters are ordered as they appear + // (left-to-right), or an array of sections ({from, to, level} + // objects) in the order in which they occur visually. + var bidiOrdering = (function() { + // Character types for codepoints 0 to 0xff + var lowTypes = "bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLL"; + // Character types for codepoints 0x600 to 0x6ff + var arabicTypes = "rrrrrrrrrrrr,rNNmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmrrrrrrrnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmNmmmmrrrrrrrrrrrrrrrrrr"; + function charType(code) { + if (code <= 0xff) return lowTypes.charAt(code); + else if (0x590 <= code && code <= 0x5f4) return "R"; + else if (0x600 <= code && code <= 0x6ff) return arabicTypes.charAt(code - 0x600); + else if (0x700 <= code && code <= 0x8ac) return "r"; + else return "L"; + } + + var bidiRE = /[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/; + var isNeutral = /[stwN]/, isStrong = /[LRr]/, countsAsLeft = /[Lb1n]/, countsAsNum = /[1n]/; + // Browsers seem to always treat the boundaries of block elements as being L. + var outerType = "L"; + + return function(str) { + if (!bidiRE.test(str)) return false; + var len = str.length, types = []; + for (var i = 0, type; i < len; ++i) + types.push(type = charType(str.charCodeAt(i))); + + // W1. Examine each non-spacing mark (NSM) in the level run, and + // change the type of the NSM to the type of the previous + // character. If the NSM is at the start of the level run, it will + // get the type of sor. + for (var i = 0, prev = outerType; i < len; ++i) { + var type = types[i]; + if (type == "m") types[i] = prev; + else prev = type; + } + + // W2. Search backwards from each instance of a European number + // until the first strong type (R, L, AL, or sor) is found. If an + // AL is found, change the type of the European number to Arabic + // number. + // W3. Change all ALs to R. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (type == "1" && cur == "r") types[i] = "n"; + else if (isStrong.test(type)) { cur = type; if (type == "r") types[i] = "R"; } + } + + // W4. A single European separator between two European numbers + // changes to a European number. A single common separator between + // two numbers of the same type changes to that type. + for (var i = 1, prev = types[0]; i < len - 1; ++i) { + var type = types[i]; + if (type == "+" && prev == "1" && types[i+1] == "1") types[i] = "1"; + else if (type == "," && prev == types[i+1] && + (prev == "1" || prev == "n")) types[i] = prev; + prev = type; + } + + // W5. A sequence of European terminators adjacent to European + // numbers changes to all European numbers. + // W6. Otherwise, separators and terminators change to Other + // Neutral. + for (var i = 0; i < len; ++i) { + var type = types[i]; + if (type == ",") types[i] = "N"; + else if (type == "%") { + for (var end = i + 1; end < len && types[end] == "%"; ++end) {} + var replace = (i && types[i-1] == "!") || (end < len && types[end] == "1") ? "1" : "N"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // W7. Search backwards from each instance of a European number + // until the first strong type (R, L, or sor) is found. If an L is + // found, then change the type of the European number to L. + for (var i = 0, cur = outerType; i < len; ++i) { + var type = types[i]; + if (cur == "L" && type == "1") types[i] = "L"; + else if (isStrong.test(type)) cur = type; + } + + // N1. A sequence of neutrals takes the direction of the + // surrounding strong text if the text on both sides has the same + // direction. European and Arabic numbers act as if they were R in + // terms of their influence on neutrals. Start-of-level-run (sor) + // and end-of-level-run (eor) are used at level run boundaries. + // N2. Any remaining neutrals take the embedding direction. + for (var i = 0; i < len; ++i) { + if (isNeutral.test(types[i])) { + for (var end = i + 1; end < len && isNeutral.test(types[end]); ++end) {} + var before = (i ? types[i-1] : outerType) == "L"; + var after = (end < len ? types[end] : outerType) == "L"; + var replace = before || after ? "L" : "R"; + for (var j = i; j < end; ++j) types[j] = replace; + i = end - 1; + } + } + + // Here we depart from the documented algorithm, in order to avoid + // building up an actual levels array. Since there are only three + // levels (0, 1, 2) in an implementation that doesn't take + // explicit embedding into account, we can build up the order on + // the fly, without following the level-based algorithm. + var order = [], m; + for (var i = 0; i < len;) { + if (countsAsLeft.test(types[i])) { + var start = i; + for (++i; i < len && countsAsLeft.test(types[i]); ++i) {} + order.push({from: start, to: i, level: 0}); + } else { + var pos = i, at = order.length; + for (++i; i < len && types[i] != "L"; ++i) {} + for (var j = pos; j < i;) { + if (countsAsNum.test(types[j])) { + if (pos < j) order.splice(at, 0, {from: pos, to: j, level: 1}); + var nstart = j; + for (++j; j < i && countsAsNum.test(types[j]); ++j) {} + order.splice(at, 0, {from: nstart, to: j, level: 2}); + pos = j; + } else ++j; + } + if (pos < i) order.splice(at, 0, {from: pos, to: i, level: 1}); + } + } + if (order[0].level == 1 && (m = str.match(/^\s+/))) { + order[0].from = m[0].length; + order.unshift({from: 0, to: m[0].length, level: 0}); + } + if (lst(order).level == 1 && (m = str.match(/\s+$/))) { + lst(order).to -= m[0].length; + order.push({from: len - m[0].length, to: len, level: 0}); + } + if (order[0].level != lst(order).level) + order.push({from: len, to: len, level: order[0].level}); + + return order; + }; + })(); + + // THE END + + CodeMirror.version = "3.21.0"; + + return CodeMirror; +})(); diff --git a/fhem/www/codemirror/mode/css/css.js b/fhem/www/codemirror/mode/css/css.js new file mode 100644 index 000000000..8f6fe7df4 --- /dev/null +++ b/fhem/www/codemirror/mode/css/css.js @@ -0,0 +1,683 @@ +CodeMirror.defineMode("css", function(config, parserConfig) { + "use strict"; + + if (!parserConfig.propertyKeywords) parserConfig = CodeMirror.resolveMode("text/css"); + + var indentUnit = config.indentUnit, + tokenHooks = parserConfig.tokenHooks, + mediaTypes = parserConfig.mediaTypes || {}, + mediaFeatures = parserConfig.mediaFeatures || {}, + propertyKeywords = parserConfig.propertyKeywords || {}, + colorKeywords = parserConfig.colorKeywords || {}, + valueKeywords = parserConfig.valueKeywords || {}, + fontProperties = parserConfig.fontProperties || {}, + allowNested = parserConfig.allowNested; + + var type, override; + function ret(style, tp) { type = tp; return style; } + + // Tokenizers + + function tokenBase(stream, state) { + var ch = stream.next(); + if (tokenHooks[ch]) { + var result = tokenHooks[ch](stream, state); + if (result !== false) return result; + } + if (ch == "@") { + stream.eatWhile(/[\w\\\-]/); + return ret("def", stream.current()); + } else if (ch == "=" || (ch == "~" || ch == "|") && stream.eat("=")) { + return ret(null, "compare"); + } else if (ch == "\"" || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "#") { + stream.eatWhile(/[\w\\\-]/); + return ret("atom", "hash"); + } else if (ch == "!") { + stream.match(/^\s*\w*/); + return ret("keyword", "important"); + } else if (/\d/.test(ch) || ch == "." && stream.eat(/\d/)) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (ch === "-") { + if (/[\d.]/.test(stream.peek())) { + stream.eatWhile(/[\w.%]/); + return ret("number", "unit"); + } else if (stream.match(/^[^-]+-/)) { + return ret("meta", "meta"); + } + } else if (/[,+>*\/]/.test(ch)) { + return ret(null, "select-op"); + } else if (ch == "." && stream.match(/^-?[_a-z][_a-z0-9-]*/i)) { + return ret("qualifier", "qualifier"); + } else if (/[:;{}\[\]\(\)]/.test(ch)) { + return ret(null, ch); + } else if (ch == "u" && stream.match("rl(")) { + stream.backUp(1); + state.tokenize = tokenParenthesized; + return ret("property", "word"); + } else if (/[\w\\\-]/.test(ch)) { + stream.eatWhile(/[\w\\\-]/); + return ret("property", "word"); + } else { + return ret(null, null); + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + if (quote == ")") stream.backUp(1); + break; + } + escaped = !escaped && ch == "\\"; + } + if (ch == quote || !escaped && quote != ")") state.tokenize = null; + return ret("string", "string"); + }; + } + + function tokenParenthesized(stream, state) { + stream.next(); // Must be '(' + if (!stream.match(/\s*[\"\']/, false)) + state.tokenize = tokenString(")"); + else + state.tokenize = null; + return ret(null, "("); + } + + // Context management + + function Context(type, indent, prev) { + this.type = type; + this.indent = indent; + this.prev = prev; + } + + function pushContext(state, stream, type) { + state.context = new Context(type, stream.indentation() + indentUnit, state.context); + return type; + } + + function popContext(state) { + state.context = state.context.prev; + return state.context.type; + } + + function pass(type, stream, state) { + return states[state.context.type](type, stream, state); + } + function popAndPass(type, stream, state, n) { + for (var i = n || 1; i > 0; i--) + state.context = state.context.prev; + return pass(type, stream, state); + } + + // Parser + + function wordAsValue(stream) { + var word = stream.current().toLowerCase(); + if (valueKeywords.hasOwnProperty(word)) + override = "atom"; + else if (colorKeywords.hasOwnProperty(word)) + override = "keyword"; + else + override = "variable"; + } + + var states = {}; + + states.top = function(type, stream, state) { + if (type == "{") { + return pushContext(state, stream, "block"); + } else if (type == "}" && state.context.prev) { + return popContext(state); + } else if (type == "@media") { + return pushContext(state, stream, "media"); + } else if (type == "@font-face") { + return "font_face_before"; + } else if (type && type.charAt(0) == "@") { + return pushContext(state, stream, "at"); + } else if (type == "hash") { + override = "builtin"; + } else if (type == "word") { + override = "tag"; + } else if (type == "variable-definition") { + return "maybeprop"; + } else if (type == "interpolation") { + return pushContext(state, stream, "interpolation"); + } else if (type == ":") { + return "pseudo"; + } else if (allowNested && type == "(") { + return pushContext(state, stream, "params"); + } + return state.context.type; + }; + + states.block = function(type, stream, state) { + if (type == "word") { + if (propertyKeywords.hasOwnProperty(stream.current().toLowerCase())) { + override = "property"; + return "maybeprop"; + } else if (allowNested) { + override = stream.match(/^\s*:/, false) ? "property" : "tag"; + return "block"; + } else { + override += " error"; + return "maybeprop"; + } + } else if (type == "meta") { + return "block"; + } else if (!allowNested && (type == "hash" || type == "qualifier")) { + override = "error"; + return "block"; + } else { + return states.top(type, stream, state); + } + }; + + states.maybeprop = function(type, stream, state) { + if (type == ":") return pushContext(state, stream, "prop"); + return pass(type, stream, state); + }; + + states.prop = function(type, stream, state) { + if (type == ";") return popContext(state); + if (type == "{" && allowNested) return pushContext(state, stream, "propBlock"); + if (type == "}" || type == "{") return popAndPass(type, stream, state); + if (type == "(") return pushContext(state, stream, "parens"); + + if (type == "hash" && !/^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/.test(stream.current())) { + override += " error"; + } else if (type == "word") { + wordAsValue(stream); + } else if (type == "interpolation") { + return pushContext(state, stream, "interpolation"); + } + return "prop"; + }; + + states.propBlock = function(type, _stream, state) { + if (type == "}") return popContext(state); + if (type == "word") { override = "property"; return "maybeprop"; } + return state.context.type; + }; + + states.parens = function(type, stream, state) { + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == ")") return popContext(state); + return "parens"; + }; + + states.pseudo = function(type, stream, state) { + if (type == "word") { + override = "variable-3"; + return state.context.type; + } + return pass(type, stream, state); + }; + + states.media = function(type, stream, state) { + if (type == "(") return pushContext(state, stream, "media_parens"); + if (type == "}") return popAndPass(type, stream, state); + if (type == "{") return popContext(state) && pushContext(state, stream, allowNested ? "block" : "top"); + + if (type == "word") { + var word = stream.current().toLowerCase(); + if (word == "only" || word == "not" || word == "and") + override = "keyword"; + else if (mediaTypes.hasOwnProperty(word)) + override = "attribute"; + else if (mediaFeatures.hasOwnProperty(word)) + override = "property"; + else + override = "error"; + } + return state.context.type; + }; + + states.media_parens = function(type, stream, state) { + if (type == ")") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state, 2); + return states.media(type, stream, state); + }; + + states.font_face_before = function(type, stream, state) { + if (type == "{") + return pushContext(state, stream, "font_face"); + return pass(type, stream, state); + }; + + states.font_face = function(type, stream, state) { + if (type == "}") return popContext(state); + if (type == "word") { + if (!fontProperties.hasOwnProperty(stream.current().toLowerCase())) + override = "error"; + else + override = "property"; + return "maybeprop"; + } + return "font_face"; + }; + + states.at = function(type, stream, state) { + if (type == ";") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == "word") override = "tag"; + else if (type == "hash") override = "builtin"; + return "at"; + }; + + states.interpolation = function(type, stream, state) { + if (type == "}") return popContext(state); + if (type == "{" || type == ";") return popAndPass(type, stream, state); + if (type != "variable") override = "error"; + return "interpolation"; + }; + + states.params = function(type, stream, state) { + if (type == ")") return popContext(state); + if (type == "{" || type == "}") return popAndPass(type, stream, state); + if (type == "word") wordAsValue(stream); + return "params"; + }; + + return { + startState: function(base) { + return {tokenize: null, + state: "top", + context: new Context("top", base || 0, null)}; + }, + + token: function(stream, state) { + if (!state.tokenize && stream.eatSpace()) return null; + var style = (state.tokenize || tokenBase)(stream, state); + if (style && typeof style == "object") { + type = style[1]; + style = style[0]; + } + override = style; + state.state = states[state.state](type, stream, state); + return override; + }, + + indent: function(state, textAfter) { + var cx = state.context, ch = textAfter && textAfter.charAt(0); + var indent = cx.indent; + if (cx.prev && + (ch == "}" && (cx.type == "block" || cx.type == "top" || cx.type == "interpolation" || cx.type == "font_face") || + ch == ")" && (cx.type == "parens" || cx.type == "params" || cx.type == "media_parens") || + ch == "{" && (cx.type == "at" || cx.type == "media"))) { + indent = cx.indent - indentUnit; + cx = cx.prev; + } + return indent; + }, + + electricChars: "}", + blockCommentStart: "/*", + blockCommentEnd: "*/", + fold: "brace" + }; +}); + +(function() { + function keySet(array) { + var keys = {}; + for (var i = 0; i < array.length; ++i) { + keys[array[i]] = true; + } + return keys; + } + + var mediaTypes_ = [ + "all", "aural", "braille", "handheld", "print", "projection", "screen", + "tty", "tv", "embossed" + ], mediaTypes = keySet(mediaTypes_); + + var mediaFeatures_ = [ + "width", "min-width", "max-width", "height", "min-height", "max-height", + "device-width", "min-device-width", "max-device-width", "device-height", + "min-device-height", "max-device-height", "aspect-ratio", + "min-aspect-ratio", "max-aspect-ratio", "device-aspect-ratio", + "min-device-aspect-ratio", "max-device-aspect-ratio", "color", "min-color", + "max-color", "color-index", "min-color-index", "max-color-index", + "monochrome", "min-monochrome", "max-monochrome", "resolution", + "min-resolution", "max-resolution", "scan", "grid" + ], mediaFeatures = keySet(mediaFeatures_); + + var propertyKeywords_ = [ + "align-content", "align-items", "align-self", "alignment-adjust", + "alignment-baseline", "anchor-point", "animation", "animation-delay", + "animation-direction", "animation-duration", "animation-iteration-count", + "animation-name", "animation-play-state", "animation-timing-function", + "appearance", "azimuth", "backface-visibility", "background", + "background-attachment", "background-clip", "background-color", + "background-image", "background-origin", "background-position", + "background-repeat", "background-size", "baseline-shift", "binding", + "bleed", "bookmark-label", "bookmark-level", "bookmark-state", + "bookmark-target", "border", "border-bottom", "border-bottom-color", + "border-bottom-left-radius", "border-bottom-right-radius", + "border-bottom-style", "border-bottom-width", "border-collapse", + "border-color", "border-image", "border-image-outset", + "border-image-repeat", "border-image-slice", "border-image-source", + "border-image-width", "border-left", "border-left-color", + "border-left-style", "border-left-width", "border-radius", "border-right", + "border-right-color", "border-right-style", "border-right-width", + "border-spacing", "border-style", "border-top", "border-top-color", + "border-top-left-radius", "border-top-right-radius", "border-top-style", + "border-top-width", "border-width", "bottom", "box-decoration-break", + "box-shadow", "box-sizing", "break-after", "break-before", "break-inside", + "caption-side", "clear", "clip", "color", "color-profile", "column-count", + "column-fill", "column-gap", "column-rule", "column-rule-color", + "column-rule-style", "column-rule-width", "column-span", "column-width", + "columns", "content", "counter-increment", "counter-reset", "crop", "cue", + "cue-after", "cue-before", "cursor", "direction", "display", + "dominant-baseline", "drop-initial-after-adjust", + "drop-initial-after-align", "drop-initial-before-adjust", + "drop-initial-before-align", "drop-initial-size", "drop-initial-value", + "elevation", "empty-cells", "fit", "fit-position", "flex", "flex-basis", + "flex-direction", "flex-flow", "flex-grow", "flex-shrink", "flex-wrap", + "float", "float-offset", "flow-from", "flow-into", "font", "font-feature-settings", + "font-family", "font-kerning", "font-language-override", "font-size", "font-size-adjust", + "font-stretch", "font-style", "font-synthesis", "font-variant", + "font-variant-alternates", "font-variant-caps", "font-variant-east-asian", + "font-variant-ligatures", "font-variant-numeric", "font-variant-position", + "font-weight", "grid-cell", "grid-column", "grid-column-align", + "grid-column-sizing", "grid-column-span", "grid-columns", "grid-flow", + "grid-row", "grid-row-align", "grid-row-sizing", "grid-row-span", + "grid-rows", "grid-template", "hanging-punctuation", "height", "hyphens", + "icon", "image-orientation", "image-rendering", "image-resolution", + "inline-box-align", "justify-content", "left", "letter-spacing", + "line-break", "line-height", "line-stacking", "line-stacking-ruby", + "line-stacking-shift", "line-stacking-strategy", "list-style", + "list-style-image", "list-style-position", "list-style-type", "margin", + "margin-bottom", "margin-left", "margin-right", "margin-top", + "marker-offset", "marks", "marquee-direction", "marquee-loop", + "marquee-play-count", "marquee-speed", "marquee-style", "max-height", + "max-width", "min-height", "min-width", "move-to", "nav-down", "nav-index", + "nav-left", "nav-right", "nav-up", "opacity", "order", "orphans", "outline", + "outline-color", "outline-offset", "outline-style", "outline-width", + "overflow", "overflow-style", "overflow-wrap", "overflow-x", "overflow-y", + "padding", "padding-bottom", "padding-left", "padding-right", "padding-top", + "page", "page-break-after", "page-break-before", "page-break-inside", + "page-policy", "pause", "pause-after", "pause-before", "perspective", + "perspective-origin", "pitch", "pitch-range", "play-during", "position", + "presentation-level", "punctuation-trim", "quotes", "region-break-after", + "region-break-before", "region-break-inside", "region-fragment", + "rendering-intent", "resize", "rest", "rest-after", "rest-before", "richness", + "right", "rotation", "rotation-point", "ruby-align", "ruby-overhang", + "ruby-position", "ruby-span", "shape-inside", "shape-outside", "size", + "speak", "speak-as", "speak-header", + "speak-numeral", "speak-punctuation", "speech-rate", "stress", "string-set", + "tab-size", "table-layout", "target", "target-name", "target-new", + "target-position", "text-align", "text-align-last", "text-decoration", + "text-decoration-color", "text-decoration-line", "text-decoration-skip", + "text-decoration-style", "text-emphasis", "text-emphasis-color", + "text-emphasis-position", "text-emphasis-style", "text-height", + "text-indent", "text-justify", "text-outline", "text-overflow", "text-shadow", + "text-size-adjust", "text-space-collapse", "text-transform", "text-underline-position", + "text-wrap", "top", "transform", "transform-origin", "transform-style", + "transition", "transition-delay", "transition-duration", + "transition-property", "transition-timing-function", "unicode-bidi", + "vertical-align", "visibility", "voice-balance", "voice-duration", + "voice-family", "voice-pitch", "voice-range", "voice-rate", "voice-stress", + "voice-volume", "volume", "white-space", "widows", "width", "word-break", + "word-spacing", "word-wrap", "z-index", "zoom", + // SVG-specific + "clip-path", "clip-rule", "mask", "enable-background", "filter", "flood-color", + "flood-opacity", "lighting-color", "stop-color", "stop-opacity", "pointer-events", + "color-interpolation", "color-interpolation-filters", "color-profile", + "color-rendering", "fill", "fill-opacity", "fill-rule", "image-rendering", + "marker", "marker-end", "marker-mid", "marker-start", "shape-rendering", "stroke", + "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", + "stroke-miterlimit", "stroke-opacity", "stroke-width", "text-rendering", + "baseline-shift", "dominant-baseline", "glyph-orientation-horizontal", + "glyph-orientation-vertical", "kerning", "text-anchor", "writing-mode" + ], propertyKeywords = keySet(propertyKeywords_); + + var colorKeywords_ = [ + "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", + "bisque", "black", "blanchedalmond", "blue", "blueviolet", "brown", + "burlywood", "cadetblue", "chartreuse", "chocolate", "coral", "cornflowerblue", + "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod", + "darkgray", "darkgreen", "darkkhaki", "darkmagenta", "darkolivegreen", + "darkorange", "darkorchid", "darkred", "darksalmon", "darkseagreen", + "darkslateblue", "darkslategray", "darkturquoise", "darkviolet", + "deeppink", "deepskyblue", "dimgray", "dodgerblue", "firebrick", + "floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", + "gold", "goldenrod", "gray", "grey", "green", "greenyellow", "honeydew", + "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender", + "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", + "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightpink", + "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", + "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", + "maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", + "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", + "mediumvioletred", "midnightblue", "mintcream", "mistyrose", "moccasin", + "navajowhite", "navy", "oldlace", "olive", "olivedrab", "orange", "orangered", + "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred", + "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", + "purple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", + "sandybrown", "seagreen", "seashell", "sienna", "silver", "skyblue", + "slateblue", "slategray", "snow", "springgreen", "steelblue", "tan", + "teal", "thistle", "tomato", "turquoise", "violet", "wheat", "white", + "whitesmoke", "yellow", "yellowgreen" + ], colorKeywords = keySet(colorKeywords_); + + var valueKeywords_ = [ + "above", "absolute", "activeborder", "activecaption", "afar", + "after-white-space", "ahead", "alias", "all", "all-scroll", "alternate", + "always", "amharic", "amharic-abegede", "antialiased", "appworkspace", + "arabic-indic", "armenian", "asterisks", "auto", "avoid", "avoid-column", "avoid-page", + "avoid-region", "background", "backwards", "baseline", "below", "bidi-override", "binary", + "bengali", "blink", "block", "block-axis", "bold", "bolder", "border", "border-box", + "both", "bottom", "break", "break-all", "break-word", "button", "button-bevel", + "buttonface", "buttonhighlight", "buttonshadow", "buttontext", "cambodian", + "capitalize", "caps-lock-indicator", "caption", "captiontext", "caret", + "cell", "center", "checkbox", "circle", "cjk-earthly-branch", + "cjk-heavenly-stem", "cjk-ideographic", "clear", "clip", "close-quote", + "col-resize", "collapse", "column", "compact", "condensed", "contain", "content", + "content-box", "context-menu", "continuous", "copy", "cover", "crop", + "cross", "crosshair", "currentcolor", "cursive", "dashed", "decimal", + "decimal-leading-zero", "default", "default-button", "destination-atop", + "destination-in", "destination-out", "destination-over", "devanagari", + "disc", "discard", "document", "dot-dash", "dot-dot-dash", "dotted", + "double", "down", "e-resize", "ease", "ease-in", "ease-in-out", "ease-out", + "element", "ellipse", "ellipsis", "embed", "end", "ethiopic", "ethiopic-abegede", + "ethiopic-abegede-am-et", "ethiopic-abegede-gez", "ethiopic-abegede-ti-er", + "ethiopic-abegede-ti-et", "ethiopic-halehame-aa-er", + "ethiopic-halehame-aa-et", "ethiopic-halehame-am-et", + "ethiopic-halehame-gez", "ethiopic-halehame-om-et", + "ethiopic-halehame-sid-et", "ethiopic-halehame-so-et", + "ethiopic-halehame-ti-er", "ethiopic-halehame-ti-et", + "ethiopic-halehame-tig", "ew-resize", "expanded", "extra-condensed", + "extra-expanded", "fantasy", "fast", "fill", "fixed", "flat", "footnotes", + "forwards", "from", "geometricPrecision", "georgian", "graytext", "groove", + "gujarati", "gurmukhi", "hand", "hangul", "hangul-consonant", "hebrew", + "help", "hidden", "hide", "higher", "highlight", "highlighttext", + "hiragana", "hiragana-iroha", "horizontal", "hsl", "hsla", "icon", "ignore", + "inactiveborder", "inactivecaption", "inactivecaptiontext", "infinite", + "infobackground", "infotext", "inherit", "initial", "inline", "inline-axis", + "inline-block", "inline-table", "inset", "inside", "intrinsic", "invert", + "italic", "justify", "kannada", "katakana", "katakana-iroha", "keep-all", "khmer", + "landscape", "lao", "large", "larger", "left", "level", "lighter", + "line-through", "linear", "lines", "list-item", "listbox", "listitem", + "local", "logical", "loud", "lower", "lower-alpha", "lower-armenian", + "lower-greek", "lower-hexadecimal", "lower-latin", "lower-norwegian", + "lower-roman", "lowercase", "ltr", "malayalam", "match", + "media-controls-background", "media-current-time-display", + "media-fullscreen-button", "media-mute-button", "media-play-button", + "media-return-to-realtime-button", "media-rewind-button", + "media-seek-back-button", "media-seek-forward-button", "media-slider", + "media-sliderthumb", "media-time-remaining-display", "media-volume-slider", + "media-volume-slider-container", "media-volume-sliderthumb", "medium", + "menu", "menulist", "menulist-button", "menulist-text", + "menulist-textfield", "menutext", "message-box", "middle", "min-intrinsic", + "mix", "mongolian", "monospace", "move", "multiple", "myanmar", "n-resize", + "narrower", "ne-resize", "nesw-resize", "no-close-quote", "no-drop", + "no-open-quote", "no-repeat", "none", "normal", "not-allowed", "nowrap", + "ns-resize", "nw-resize", "nwse-resize", "oblique", "octal", "open-quote", + "optimizeLegibility", "optimizeSpeed", "oriya", "oromo", "outset", + "outside", "outside-shape", "overlay", "overline", "padding", "padding-box", + "painted", "page", "paused", "persian", "plus-darker", "plus-lighter", "pointer", + "polygon", "portrait", "pre", "pre-line", "pre-wrap", "preserve-3d", "progress", "push-button", + "radio", "read-only", "read-write", "read-write-plaintext-only", "rectangle", "region", + "relative", "repeat", "repeat-x", "repeat-y", "reset", "reverse", "rgb", "rgba", + "ridge", "right", "round", "row-resize", "rtl", "run-in", "running", + "s-resize", "sans-serif", "scroll", "scrollbar", "se-resize", "searchfield", + "searchfield-cancel-button", "searchfield-decoration", + "searchfield-results-button", "searchfield-results-decoration", + "semi-condensed", "semi-expanded", "separate", "serif", "show", "sidama", + "single", "skip-white-space", "slide", "slider-horizontal", + "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "slow", + "small", "small-caps", "small-caption", "smaller", "solid", "somali", + "source-atop", "source-in", "source-out", "source-over", "space", "square", + "square-button", "start", "static", "status-bar", "stretch", "stroke", + "sub", "subpixel-antialiased", "super", "sw-resize", "table", + "table-caption", "table-cell", "table-column", "table-column-group", + "table-footer-group", "table-header-group", "table-row", "table-row-group", + "telugu", "text", "text-bottom", "text-top", "textarea", "textfield", "thai", + "thick", "thin", "threeddarkshadow", "threedface", "threedhighlight", + "threedlightshadow", "threedshadow", "tibetan", "tigre", "tigrinya-er", + "tigrinya-er-abegede", "tigrinya-et", "tigrinya-et-abegede", "to", "top", + "transparent", "ultra-condensed", "ultra-expanded", "underline", "up", + "upper-alpha", "upper-armenian", "upper-greek", "upper-hexadecimal", + "upper-latin", "upper-norwegian", "upper-roman", "uppercase", "urdu", "url", + "vertical", "vertical-text", "visible", "visibleFill", "visiblePainted", + "visibleStroke", "visual", "w-resize", "wait", "wave", "wider", + "window", "windowframe", "windowtext", "x-large", "x-small", "xor", + "xx-large", "xx-small" + ], valueKeywords = keySet(valueKeywords_); + + var fontProperties_ = [ + "font-family", "src", "unicode-range", "font-variant", "font-feature-settings", + "font-stretch", "font-weight", "font-style" + ], fontProperties = keySet(fontProperties_); + + var allWords = mediaTypes_.concat(mediaFeatures_).concat(propertyKeywords_).concat(colorKeywords_).concat(valueKeywords_); + CodeMirror.registerHelper("hintWords", "css", allWords); + + function tokenCComment(stream, state) { + var maybeEnd = false, ch; + while ((ch = stream.next()) != null) { + if (maybeEnd && ch == "/") { + state.tokenize = null; + break; + } + maybeEnd = (ch == "*"); + } + return ["comment", "comment"]; + } + + function tokenSGMLComment(stream, state) { + if (stream.skipTo("-->")) { + stream.match("-->"); + state.tokenize = null; + } else { + stream.skipToEnd(); + } + return ["comment", "comment"]; + } + + CodeMirror.defineMIME("text/css", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + tokenHooks: { + "<": function(stream, state) { + if (!stream.match("!--")) return false; + state.tokenize = tokenSGMLComment; + return tokenSGMLComment(stream, state); + }, + "/": function(stream, state) { + if (!stream.eat("*")) return false; + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } + }, + name: "css" + }); + + CodeMirror.defineMIME("text/x-scss", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + allowNested: true, + tokenHooks: { + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + ":": function(stream) { + if (stream.match(/\s*{/)) + return [null, "{"]; + return false; + }, + "$": function(stream) { + stream.match(/^[\w-]+/); + if (stream.match(/^\s*:/, false)) + return ["variable-2", "variable-definition"]; + return ["variable-2", "variable"]; + }, + "#": function(stream) { + if (!stream.eat("{")) return false; + return [null, "interpolation"]; + } + }, + name: "css", + helperType: "scss" + }); + + CodeMirror.defineMIME("text/x-less", { + mediaTypes: mediaTypes, + mediaFeatures: mediaFeatures, + propertyKeywords: propertyKeywords, + colorKeywords: colorKeywords, + valueKeywords: valueKeywords, + fontProperties: fontProperties, + allowNested: true, + tokenHooks: { + "/": function(stream, state) { + if (stream.eat("/")) { + stream.skipToEnd(); + return ["comment", "comment"]; + } else if (stream.eat("*")) { + state.tokenize = tokenCComment; + return tokenCComment(stream, state); + } else { + return ["operator", "operator"]; + } + }, + "@": function(stream) { + if (stream.match(/^(charset|document|font-face|import|keyframes|media|namespace|page|supports)\b/, false)) return false; + stream.eatWhile(/[\w\\\-]/); + if (stream.match(/^\s*:/, false)) + return ["variable-2", "variable-definition"]; + return ["variable-2", "variable"]; + }, + "&": function() { + return ["atom", "atom"]; + } + }, + name: "css", + helperType: "less" + }); +})(); diff --git a/fhem/www/codemirror/mode/css/index.html b/fhem/www/codemirror/mode/css/index.html new file mode 100644 index 000000000..80ef518e9 --- /dev/null +++ b/fhem/www/codemirror/mode/css/index.html @@ -0,0 +1,70 @@ + + +CodeMirror: CSS mode + + + + + + + + + +
+

CSS mode

+
+ + +

MIME types defined: text/css, text/x-scss (demo), text/x-less (demo).

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/fhem/www/codemirror/mode/css/less.html b/fhem/www/codemirror/mode/css/less.html new file mode 100644 index 000000000..1030ca43f --- /dev/null +++ b/fhem/www/codemirror/mode/css/less.html @@ -0,0 +1,152 @@ + + +CodeMirror: LESS mode + + + + + + + + + + +
+

LESS mode

+
+ + +

The LESS mode is a sub-mode of the CSS mode (defined in css.js.

+ +

Parsing/Highlighting Tests: normal, verbose.

+
diff --git a/fhem/www/codemirror/mode/css/less_test.js b/fhem/www/codemirror/mode/css/less_test.js new file mode 100644 index 000000000..ea64f91d1 --- /dev/null +++ b/fhem/www/codemirror/mode/css/less_test.js @@ -0,0 +1,48 @@ +(function() { + "use strict"; + + var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-less"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "less"); } + + MT("variable", + "[variable-2 @base]: [atom #f04615];", + "[qualifier .class] {", + " [property width]: [variable percentage]([number 0.5]); [comment // returns `50%`]", + " [property color]: [variable saturate]([variable-2 @base], [number 5%]);", + "}"); + + MT("amp", + "[qualifier .child], [qualifier .sibling] {", + " [qualifier .parent] [atom &] {", + " [property color]: [keyword black];", + " }", + " [atom &] + [atom &] {", + " [property color]: [keyword red];", + " }", + "}"); + + MT("mixin", + "[qualifier .mixin] ([variable dark]; [variable-2 @color]) {", + " [property color]: [variable darken]([variable-2 @color], [number 10%]);", + "}", + "[qualifier .mixin] ([variable light]; [variable-2 @color]) {", + " [property color]: [variable lighten]([variable-2 @color], [number 10%]);", + "}", + "[qualifier .mixin] ([variable-2 @_]; [variable-2 @color]) {", + " [property display]: [atom block];", + "}", + "[variable-2 @switch]: [variable light];", + "[qualifier .class] {", + " [qualifier .mixin]([variable-2 @switch]; [atom #888]);", + "}"); + + MT("nest", + "[qualifier .one] {", + " [def @media] ([property width]: [number 400px]) {", + " [property font-size]: [number 1.2em];", + " [def @media] [attribute print] [keyword and] [property color] {", + " [property color]: [keyword blue];", + " }", + " }", + "}"); +})(); diff --git a/fhem/www/codemirror/mode/css/scss.html b/fhem/www/codemirror/mode/css/scss.html new file mode 100644 index 000000000..0677d08fe --- /dev/null +++ b/fhem/www/codemirror/mode/css/scss.html @@ -0,0 +1,157 @@ + + +CodeMirror: SCSS mode + + + + + + + + + +
+

SCSS mode

+
+ + +

The SCSS mode is a sub-mode of the CSS mode (defined in css.js.

+ +

Parsing/Highlighting Tests: normal, verbose.

+ +
diff --git a/fhem/www/codemirror/mode/css/scss_test.js b/fhem/www/codemirror/mode/css/scss_test.js new file mode 100644 index 000000000..c51cb42bb --- /dev/null +++ b/fhem/www/codemirror/mode/css/scss_test.js @@ -0,0 +1,107 @@ +(function() { + var mode = CodeMirror.getMode({indentUnit: 2}, "text/x-scss"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1), "scss"); } + + MT('url_with_quotation', + "[tag foo] { [property background]:[atom url]([string test.jpg]) }"); + + MT('url_with_double_quotes', + "[tag foo] { [property background]:[atom url]([string \"test.jpg\"]) }"); + + MT('url_with_single_quotes', + "[tag foo] { [property background]:[atom url]([string \'test.jpg\']) }"); + + MT('string', + "[def @import] [string \"compass/css3\"]"); + + MT('important_keyword', + "[tag foo] { [property background]:[atom url]([string \'test.jpg\']) [keyword !important] }"); + + MT('variable', + "[variable-2 $blue]:[atom #333]"); + + MT('variable_as_attribute', + "[tag foo] { [property color]:[variable-2 $blue] }"); + + MT('numbers', + "[tag foo] { [property padding]:[number 10px] [number 10] [number 10em] [number 8in] }"); + + MT('number_percentage', + "[tag foo] { [property width]:[number 80%] }"); + + MT('selector', + "[builtin #hello][qualifier .world]{}"); + + MT('singleline_comment', + "[comment // this is a comment]"); + + MT('multiline_comment', + "[comment /*foobar*/]"); + + MT('attribute_with_hyphen', + "[tag foo] { [property font-size]:[number 10px] }"); + + MT('string_after_attribute', + "[tag foo] { [property content]:[string \"::\"] }"); + + MT('directives', + "[def @include] [qualifier .mixin]"); + + MT('basic_structure', + "[tag p] { [property background]:[keyword red]; }"); + + MT('nested_structure', + "[tag p] { [tag a] { [property color]:[keyword red]; } }"); + + MT('mixin', + "[def @mixin] [tag table-base] {}"); + + MT('number_without_semicolon', + "[tag p] {[property width]:[number 12]}", + "[tag a] {[property color]:[keyword red];}"); + + MT('atom_in_nested_block', + "[tag p] { [tag a] { [property color]:[atom #000]; } }"); + + MT('interpolation_in_property', + "[tag foo] { #{[variable-2 $hello]}:[number 2]; }"); + + MT('interpolation_in_selector', + "[tag foo]#{[variable-2 $hello]} { [property color]:[atom #000]; }"); + + MT('interpolation_error', + "[tag foo]#{[error foo]} { [property color]:[atom #000]; }"); + + MT("divide_operator", + "[tag foo] { [property width]:[number 4] [operator /] [number 2] }"); + + MT('nested_structure_with_id_selector', + "[tag p] { [builtin #hello] { [property color]:[keyword red]; } }"); + + MT('indent_mixin', + "[def @mixin] [tag container] (", + " [variable-2 $a]: [number 10],", + " [variable-2 $b]: [number 10])", + "{}"); + + MT('indent_nested', + "[tag foo] {", + " [tag bar] {", + " }", + "}"); + + MT('indent_parentheses', + "[tag foo] {", + " [property color]: [variable darken]([variable-2 $blue],", + " [number 9%]);", + "}"); + + MT('indent_vardef', + "[variable-2 $name]:", + " [string 'val'];", + "[tag tag] {", + " [tag inner] {", + " [property margin]: [number 3px];", + " }", + "}"); +})(); diff --git a/fhem/www/codemirror/mode/css/test.js b/fhem/www/codemirror/mode/css/test.js new file mode 100644 index 000000000..b3f47767e --- /dev/null +++ b/fhem/www/codemirror/mode/css/test.js @@ -0,0 +1,119 @@ +(function() { + var mode = CodeMirror.getMode({indentUnit: 2}, "css"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + + // Error, because "foobarhello" is neither a known type or property, but + // property was expected (after "and"), and it should be in parenthese. + MT("atMediaUnknownType", + "[def @media] [attribute screen] [keyword and] [error foobarhello] { }"); + + // Soft error, because "foobarhello" is not a known property or type. + MT("atMediaUnknownProperty", + "[def @media] [attribute screen] [keyword and] ([error foobarhello]) { }"); + + // Make sure nesting works with media queries + MT("atMediaMaxWidthNested", + "[def @media] [attribute screen] [keyword and] ([property max-width]: [number 25px]) { [tag foo] { } }"); + + MT("tagSelector", + "[tag foo] { }"); + + MT("classSelector", + "[qualifier .foo-bar_hello] { }"); + + MT("idSelector", + "[builtin #foo] { [error #foo] }"); + + MT("tagSelectorUnclosed", + "[tag foo] { [property margin]: [number 0] } [tag bar] { }"); + + MT("tagStringNoQuotes", + "[tag foo] { [property font-family]: [variable hello] [variable world]; }"); + + MT("tagStringDouble", + "[tag foo] { [property font-family]: [string \"hello world\"]; }"); + + MT("tagStringSingle", + "[tag foo] { [property font-family]: [string 'hello world']; }"); + + MT("tagColorKeyword", + "[tag foo] {", + " [property color]: [keyword black];", + " [property color]: [keyword navy];", + " [property color]: [keyword yellow];", + "}"); + + MT("tagColorHex3", + "[tag foo] { [property background]: [atom #fff]; }"); + + MT("tagColorHex6", + "[tag foo] { [property background]: [atom #ffffff]; }"); + + MT("tagColorHex4", + "[tag foo] { [property background]: [atom&error #ffff]; }"); + + MT("tagColorHexInvalid", + "[tag foo] { [property background]: [atom&error #ffg]; }"); + + MT("tagNegativeNumber", + "[tag foo] { [property margin]: [number -5px]; }"); + + MT("tagPositiveNumber", + "[tag foo] { [property padding]: [number 5px]; }"); + + MT("tagVendor", + "[tag foo] { [meta -foo-][property box-sizing]: [meta -foo-][atom border-box]; }"); + + MT("tagBogusProperty", + "[tag foo] { [property&error barhelloworld]: [number 0]; }"); + + MT("tagTwoProperties", + "[tag foo] { [property margin]: [number 0]; [property padding]: [number 0]; }"); + + MT("tagTwoPropertiesURL", + "[tag foo] { [property background]: [atom url]([string //example.com/foo.png]); [property padding]: [number 0]; }"); + + MT("commentSGML", + "[comment ]"); + + MT("commentSGML2", + "[comment ] [tag div] {}"); + + MT("indent_tagSelector", + "[tag strong], [tag em] {", + " [property background]: [atom rgba](", + " [number 255], [number 255], [number 0], [number .2]", + " );", + "}"); + + MT("indent_atMedia", + "[def @media] {", + " [tag foo] {", + " [property color]:", + " [keyword yellow];", + " }", + "}"); + + MT("indent_comma", + "[tag foo] {", + " [property font-family]: [variable verdana],", + " [atom sans-serif];", + "}"); + + MT("indent_parentheses", + "[tag foo]:[variable-3 before] {", + " [property background]: [atom url](", + "[string blahblah]", + "[string etc]", + "[string ]) [keyword !important];", + "}"); + + MT("font_face", + "[def @font-face] {", + " [property font-family]: [string 'myfont'];", + " [error nonsense]: [string 'abc'];", + " [property src]: [atom url]([string http://blah]),", + " [atom url]([string http://foo]);", + "}"); +})(); diff --git a/fhem/www/codemirror/mode/fhem/fhem.js b/fhem/www/codemirror/mode/fhem/fhem.js new file mode 100644 index 000000000..8e06b3dbb --- /dev/null +++ b/fhem/www/codemirror/mode/fhem/fhem.js @@ -0,0 +1,1020 @@ +// CodeMirror2 mode/perl/perl.js (text/x-perl) beta 0.10 (2011-11-08) +// This is a part of CodeMirror from https://github.com/sabaca/CodeMirror_mode_perl (mail@sabaca.com) +CodeMirror.defineMode("fhem",function(){ + // http://perldoc.perl.org + var PERL={ // null - magic touch + // 1 - keyword + // 2 - def + // 3 - atom + // 4 - operator + // 5 - variable-2 (predefined) + // [x,y] - x=1,2,3; y=must be defined if x{...} + // PERL operators + '->' : 4, + '++' : 4, + '--' : 4, + '**' : 4, + // ! ~ \ and unary + and - + '=~' : 4, + '!~' : 4, + '*' : 4, + '/' : 4, + '%' : 4, + 'x' : 4, + '+' : 4, + '-' : 4, + '.' : 4, + '<<' : 4, + '>>' : 4, + // named unary operators + '<' : 4, + '>' : 4, + '<=' : 4, + '>=' : 4, + 'lt' : 4, + 'gt' : 4, + 'le' : 4, + 'ge' : 4, + '==' : 4, + '!=' : 4, + '<=>' : 4, + 'eq' : 4, + 'ne' : 4, + 'cmp' : 4, + '~~' : 4, + '&' : 4, + '|' : 4, + '^' : 4, + '&&' : 4, + '||' : 4, + '//' : 4, + '..' : 4, + '...' : 4, + '?' : 4, + ':' : 4, + '=' : 4, + '+=' : 4, + '-=' : 4, + '*=' : 4, // etc. ??? + ',' : 4, + '=>' : 4, + '::' : 4, + // list operators (rightward) + 'not' : 4, + 'and' : 4, + 'or' : 4, + 'xor' : 4, + // PERL predefined variables (I know, what this is a paranoid idea, but may be needed for people, who learn PERL, and for me as well, ...and may be for you?;) + 'BEGIN' : [5,1], + 'END' : [5,1], + 'PRINT' : [5,1], + 'PRINTF' : [5,1], + 'GETC' : [5,1], + 'READ' : [5,1], + 'READLINE' : [5,1], + 'DESTROY' : [5,1], + 'TIE' : [5,1], + 'TIEHANDLE' : [5,1], + 'UNTIE' : [5,1], + 'STDIN' : 5, + 'STDIN_TOP' : 5, + 'STDOUT' : 5, + 'STDOUT_TOP' : 5, + 'STDERR' : 5, + 'STDERR_TOP' : 5, + '$ARG' : 5, + '$_' : 5, + '@ARG' : 5, + '@_' : 5, + '$LIST_SEPARATOR' : 5, + '$"' : 5, + '$PROCESS_ID' : 5, + '$PID' : 5, + '$$' : 5, + '$REAL_GROUP_ID' : 5, + '$GID' : 5, + '$(' : 5, + '$EFFECTIVE_GROUP_ID' : 5, + '$EGID' : 5, + '$)' : 5, + '$PROGRAM_NAME' : 5, + '$0' : 5, + '$SUBSCRIPT_SEPARATOR' : 5, + '$SUBSEP' : 5, + '$;' : 5, + '$REAL_USER_ID' : 5, + '$UID' : 5, + '$<' : 5, + '$EFFECTIVE_USER_ID' : 5, + '$EUID' : 5, + '$>' : 5, + '$a' : 5, + '$b' : 5, + '$COMPILING' : 5, + '$^C' : 5, + '$DEBUGGING' : 5, + '$^D' : 5, + '${^ENCODING}' : 5, + '$ENV' : 5, + '%ENV' : 5, + '$SYSTEM_FD_MAX' : 5, + '$^F' : 5, + '@F' : 5, + '${^GLOBAL_PHASE}' : 5, + '$^H' : 5, + '%^H' : 5, + '@INC' : 5, + '%INC' : 5, + '$INPLACE_EDIT' : 5, + '$^I' : 5, + '$^M' : 5, + '$OSNAME' : 5, + '$^O' : 5, + '${^OPEN}' : 5, + '$PERLDB' : 5, + '$^P' : 5, + '$SIG' : 5, + '%SIG' : 5, + '$BASETIME' : 5, + '$^T' : 5, + '${^TAINT}' : 5, + '${^UNICODE}' : 5, + '${^UTF8CACHE}' : 5, + '${^UTF8LOCALE}' : 5, + '$PERL_VERSION' : 5, + '$^V' : 5, + '${^WIN32_SLOPPY_STAT}' : 5, + '$EXECUTABLE_NAME' : 5, + '$^X' : 5, + '$1' : 5, // - regexp $1, $2... + '$MATCH' : 5, + '$&' : 5, + '${^MATCH}' : 5, + '$PREMATCH' : 5, + '$`' : 5, + '${^PREMATCH}' : 5, + '$POSTMATCH' : 5, + "$'" : 5, + '${^POSTMATCH}' : 5, + '$LAST_PAREN_MATCH' : 5, + '$+' : 5, + '$LAST_SUBMATCH_RESULT' : 5, + '$^N' : 5, + '@LAST_MATCH_END' : 5, + '@+' : 5, + '%LAST_PAREN_MATCH' : 5, + '%+' : 5, + '@LAST_MATCH_START' : 5, + '@-' : 5, + '%LAST_MATCH_START' : 5, + '%-' : 5, + '$LAST_REGEXP_CODE_RESULT' : 5, + '$^R' : 5, + '${^RE_DEBUG_FLAGS}' : 5, + '${^RE_TRIE_MAXBUF}' : 5, + '$ARGV' : 5, + '@ARGV' : 5, + 'ARGV' : 5, + 'ARGVOUT' : 5, + '$OUTPUT_FIELD_SEPARATOR' : 5, + '$OFS' : 5, + '$,' : 5, + '$INPUT_LINE_NUMBER' : 5, + '$NR' : 5, + '$.' : 5, + '$INPUT_RECORD_SEPARATOR' : 5, + '$RS' : 5, + '$/' : 5, + '$OUTPUT_RECORD_SEPARATOR' : 5, + '$ORS' : 5, + '$\\' : 5, + '$OUTPUT_AUTOFLUSH' : 5, + '$|' : 5, + '$ACCUMULATOR' : 5, + '$^A' : 5, + '$FORMAT_FORMFEED' : 5, + '$^L' : 5, + '$FORMAT_PAGE_NUMBER' : 5, + '$%' : 5, + '$FORMAT_LINES_LEFT' : 5, + '$-' : 5, + '$FORMAT_LINE_BREAK_CHARACTERS' : 5, + '$:' : 5, + '$FORMAT_LINES_PER_PAGE' : 5, + '$=' : 5, + '$FORMAT_TOP_NAME' : 5, + '$^' : 5, + '$FORMAT_NAME' : 5, + '$~' : 5, + '${^CHILD_ERROR_NATIVE}' : 5, + '$EXTENDED_OS_ERROR' : 5, + '$^E' : 5, + '$EXCEPTIONS_BEING_CAUGHT' : 5, + '$^S' : 5, + '$WARNING' : 5, + '$^W' : 5, + '${^WARNING_BITS}' : 5, + '$OS_ERROR' : 5, + '$ERRNO' : 5, + '$!' : 5, + '%OS_ERROR' : 5, + '%ERRNO' : 5, + '%!' : 5, + '$CHILD_ERROR' : 5, + '$?' : 5, + '$EVAL_ERROR' : 5, + '$@' : 5, + '$OFMT' : 5, + '$#' : 5, + '$*' : 5, + '$ARRAY_BASE' : 5, + '$[' : 5, + '$OLD_PERL_VERSION' : 5, + '$]' : 5, + // PERL blocks + 'if' :[1,1], + elsif :[1,1], + 'else' :[1,1], + 'while' :[1,1], + unless :[1,1], + 'for' :[1,1], + foreach :[1,1], + // PERL functions + 'abs' :1, // - absolute value function + accept :1, // - accept an incoming socket connect + alarm :1, // - schedule a SIGALRM + 'atan2' :1, // - arctangent of Y/X in the range -PI to PI + bind :1, // - binds an address to a socket + binmode :1, // - prepare binary files for I/O + bless :1, // - create an object + bootstrap :1, // + 'break' :1, // - break out of a "given" block + caller :1, // - get context of the current subroutine call + chdir :1, // - change your current working directory + chmod :1, // - changes the permissions on a list of files + chomp :1, // - remove a trailing record separator from a string + chop :1, // - remove the last character from a string + chown :1, // - change the owership on a list of files + chr :1, // - get character this number represents + chroot :1, // - make directory new root for path lookups + close :1, // - close file (or pipe or socket) handle + closedir :1, // - close directory handle + connect :1, // - connect to a remote socket + 'continue' :[1,1], // - optional trailing block in a while or foreach + 'cos' :1, // - cosine function + crypt :1, // - one-way passwd-style encryption + dbmclose :1, // - breaks binding on a tied dbm file + dbmopen :1, // - create binding on a tied dbm file + 'default' :1, // + defined :1, // - test whether a value, variable, or function is defined + 'delete' :1, // - deletes a value from a hash + die :1, // - raise an exception or bail out + 'do' :1, // - turn a BLOCK into a TERM + dump :1, // - create an immediate core dump + each :1, // - retrieve the next key/value pair from a hash + endgrent :1, // - be done using group file + endhostent :1, // - be done using hosts file + endnetent :1, // - be done using networks file + endprotoent :1, // - be done using protocols file + endpwent :1, // - be done using passwd file + endservent :1, // - be done using services file + eof :1, // - test a filehandle for its end + 'eval' :1, // - catch exceptions or compile and run code + 'exec' :1, // - abandon this program to run another + exists :1, // - test whether a hash key is present + exit :1, // - terminate this program + 'exp' :1, // - raise I to a power + fcntl :1, // - file control system call + fileno :1, // - return file descriptor from filehandle + flock :1, // - lock an entire file with an advisory lock + fork :1, // - create a new process just like this one + format :1, // - declare a picture format with use by the write() function + formline :1, // - internal function used for formats + getc :1, // - get the next character from the filehandle + getgrent :1, // - get next group record + getgrgid :1, // - get group record given group user ID + getgrnam :1, // - get group record given group name + gethostbyaddr :1, // - get host record given its address + gethostbyname :1, // - get host record given name + gethostent :1, // - get next hosts record + getlogin :1, // - return who logged in at this tty + getnetbyaddr :1, // - get network record given its address + getnetbyname :1, // - get networks record given name + getnetent :1, // - get next networks record + getpeername :1, // - find the other end of a socket connection + getpgrp :1, // - get process group + getppid :1, // - get parent process ID + getpriority :1, // - get current nice value + getprotobyname :1, // - get protocol record given name + getprotobynumber :1, // - get protocol record numeric protocol + getprotoent :1, // - get next protocols record + getpwent :1, // - get next passwd record + getpwnam :1, // - get passwd record given user login name + getpwuid :1, // - get passwd record given user ID + getservbyname :1, // - get services record given its name + getservbyport :1, // - get services record given numeric port + getservent :1, // - get next services record + getsockname :1, // - retrieve the sockaddr for a given socket + getsockopt :1, // - get socket options on a given socket + given :1, // + glob :1, // - expand filenames using wildcards + gmtime :1, // - convert UNIX time into record or string using Greenwich time + 'goto' :1, // - create spaghetti code + grep :1, // - locate elements in a list test true against a given criterion + hex :1, // - convert a string to a hexadecimal number + 'import' :1, // - patch a module's namespace into your own + index :1, // - find a substring within a string + 'int' :1, // - get the integer portion of a number + ioctl :1, // - system-dependent device control system call + 'join' :1, // - join a list into a string using a separator + keys :1, // - retrieve list of indices from a hash + kill :1, // - send a signal to a process or process group + last :1, // - exit a block prematurely + lc :1, // - return lower-case version of a string + lcfirst :1, // - return a string with just the next letter in lower case + length :1, // - return the number of bytes in a string + 'link' :1, // - create a hard link in the filesytem + listen :1, // - register your socket as a server + local : 2, // - create a temporary value for a global variable (dynamic scoping) + localtime :1, // - convert UNIX time into record or string using local time + lock :1, // - get a thread lock on a variable, subroutine, or method + 'log' :1, // - retrieve the natural logarithm for a number + lstat :1, // - stat a symbolic link + m :null, // - match a string with a regular expression pattern + map :1, // - apply a change to a list to get back a new list with the changes + mkdir :1, // - create a directory + msgctl :1, // - SysV IPC message control operations + msgget :1, // - get SysV IPC message queue + msgrcv :1, // - receive a SysV IPC message from a message queue + msgsnd :1, // - send a SysV IPC message to a message queue + my : 2, // - declare and assign a local variable (lexical scoping) + 'new' :1, // + next :1, // - iterate a block prematurely + no :1, // - unimport some module symbols or semantics at compile time + oct :1, // - convert a string to an octal number + open :1, // - open a file, pipe, or descriptor + opendir :1, // - open a directory + ord :1, // - find a character's numeric representation + our : 2, // - declare and assign a package variable (lexical scoping) + pack :1, // - convert a list into a binary representation + 'package' :1, // - declare a separate global namespace + pipe :1, // - open a pair of connected filehandles + pop :1, // - remove the last element from an array and return it + pos :1, // - find or set the offset for the last/next m//g search + print :1, // - output a list to a filehandle + printf :1, // - output a formatted list to a filehandle + prototype :1, // - get the prototype (if any) of a subroutine + push :1, // - append one or more elements to an array + q :null, // - singly quote a string + qq :null, // - doubly quote a string + qr :null, // - Compile pattern + quotemeta :null, // - quote regular expression magic characters + qw :null, // - quote a list of words + qx :null, // - backquote quote a string + rand :1, // - retrieve the next pseudorandom number + read :1, // - fixed-length buffered input from a filehandle + readdir :1, // - get a directory from a directory handle + readline :1, // - fetch a record from a file + readlink :1, // - determine where a symbolic link is pointing + readpipe :1, // - execute a system command and collect standard output + recv :1, // - receive a message over a Socket + redo :1, // - start this loop iteration over again + ref :1, // - find out the type of thing being referenced + rename :1, // - change a filename + require :1, // - load in external functions from a library at runtime + reset :1, // - clear all variables of a given name + 'return' :1, // - get out of a function early + reverse :1, // - flip a string or a list + rewinddir :1, // - reset directory handle + rindex :1, // - right-to-left substring search + rmdir :1, // - remove a directory + s :null, // - replace a pattern with a string + say :1, // - print with newline + scalar :1, // - force a scalar context + seek :1, // - reposition file pointer for random-access I/O + seekdir :1, // - reposition directory pointer + select :1, // - reset default output or do I/O multiplexing + semctl :1, // - SysV semaphore control operations + semget :1, // - get set of SysV semaphores + semop :1, // - SysV semaphore operations + send :1, // - send a message over a socket + setgrent :1, // - prepare group file for use + sethostent :1, // - prepare hosts file for use + setnetent :1, // - prepare networks file for use + setpgrp :1, // - set the process group of a process + setpriority :1, // - set a process's nice value + setprotoent :1, // - prepare protocols file for use + setpwent :1, // - prepare passwd file for use + setservent :1, // - prepare services file for use + setsockopt :1, // - set some socket options + shift :1, // - remove the first element of an array, and return it + shmctl :1, // - SysV shared memory operations + shmget :1, // - get SysV shared memory segment identifier + shmread :1, // - read SysV shared memory + shmwrite :1, // - write SysV shared memory + shutdown :1, // - close down just half of a socket connection + 'sin' :1, // - return the sine of a number + sleep :1, // - block for some number of seconds + socket :1, // - create a socket + socketpair :1, // - create a pair of sockets + 'sort' :1, // - sort a list of values + splice :1, // - add or remove elements anywhere in an array + 'split' :1, // - split up a string using a regexp delimiter + sprintf :1, // - formatted print into a string + 'sqrt' :1, // - square root function + srand :1, // - seed the random number generator + stat :1, // - get a file's status information + state :1, // - declare and assign a state variable (persistent lexical scoping) + study :1, // - optimize input data for repeated searches + 'sub' :1, // - declare a subroutine, possibly anonymously + 'substr' :1, // - get or alter a portion of a stirng + symlink :1, // - create a symbolic link to a file + syscall :1, // - execute an arbitrary system call + sysopen :1, // - open a file, pipe, or descriptor + sysread :1, // - fixed-length unbuffered input from a filehandle + sysseek :1, // - position I/O pointer on handle used with sysread and syswrite + system :1, // - run a separate program + syswrite :1, // - fixed-length unbuffered output to a filehandle + tell :1, // - get current seekpointer on a filehandle + telldir :1, // - get current seekpointer on a directory handle + tie :1, // - bind a variable to an object class + tied :1, // - get a reference to the object underlying a tied variable + time :1, // - return number of seconds since 1970 + times :1, // - return elapsed time for self and child processes + tr :null, // - transliterate a string + truncate :1, // - shorten a file + uc :1, // - return upper-case version of a string + ucfirst :1, // - return a string with just the next letter in upper case + umask :1, // - set file creation mode mask + undef :1, // - remove a variable or function definition + unlink :1, // - remove one link to a file + unpack :1, // - convert binary structure into normal perl variables + unshift :1, // - prepend more elements to the beginning of a list + untie :1, // - break a tie binding to a variable + use :1, // - load in a module at compile time + utime :1, // - set a file's last access and modify times + values :1, // - return a list of the values in a hash + vec :1, // - test or set particular bits in a string + wait :1, // - wait for any child process to die + waitpid :1, // - wait for a particular child process to die + wantarray :1, // - get void vs scalar vs list context of current subroutine call + warn :1, // - print debugging info + when :1, // + write :1, // - print a picture record + + define :2, + attr :2, + at :2, + notify :2, + set :2, + ReadingsVal :1, + Value :1, + fhem :1, + y :null}; // - transliterate a string + + keywords.foreach( function( k, v ) { + PERL[v] = 2; + }); + + var RXstyle="string-2"; + var RXmodifiers=/[goseximacplud]/; // NOTE: "m", "s", "y" and "tr" need to correct real modifiers for each regexp type + + function tokenChain(stream,state,chain,style,tail){ // NOTE: chain.length > 2 is not working now (it's for s[...][...]geos;) + state.chain=null; // 12 3tail + state.style=null; + state.tail=null; + state.tokenize=function(stream,state){ + var e=false,c,i=0; + while(c=stream.next()){ + if(c===chain[i]&&!e){ + if(chain[++i]!==undefined){ + state.chain=chain[i]; + state.style=style; + state.tail=tail;} + else if(tail) + stream.eatWhile(tail); + state.tokenize=tokenPerl; + return style;} + e=!e&&c=="\\";} + return style;}; + return state.tokenize(stream,state);} + + function tokenSOMETHING(stream,state,string){ + state.tokenize=function(stream,state){ + if(stream.string==string) + state.tokenize=tokenPerl; + stream.skipToEnd(); + return "string";}; + return state.tokenize(stream,state);} + + function tokenPerl(stream,state){ + if(stream.eatSpace()) + return null; + if(state.chain) + return tokenChain(stream,state,state.chain,state.style,state.tail); + if(stream.match(/^\-?[\d\.]/,false)) + if(stream.match(/^(\-?(\d*\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F]+|0b[01]+|\d+(e[+-]?\d+)?)/)) + return 'number'; + if(stream.match(/^<<(?=\w)/)){ // NOTE: <"],RXstyle,RXmodifiers);} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers);}} + else if(c=="q"){ + c=stream.look(1); + if(c=="("){ + stream.eatSuffix(2); + return tokenChain(stream,state,[")"],"string");} + if(c=="["){ + stream.eatSuffix(2); + return tokenChain(stream,state,["]"],"string");} + if(c=="{"){ +stream.eatSuffix(2); + return tokenChain(stream,state,["}"],"string");} + if(c=="<"){ + stream.eatSuffix(2); + return tokenChain(stream,state,[">"],"string");} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],"string");}} + else if(c=="w"){ + c=stream.look(1); + if(c=="("){ + stream.eatSuffix(2); + return tokenChain(stream,state,[")"],"bracket");} + if(c=="["){ + stream.eatSuffix(2); + return tokenChain(stream,state,["]"],"bracket");} + if(c=="{"){ + stream.eatSuffix(2); + return tokenChain(stream,state,["}"],"bracket");} + if(c=="<"){ + stream.eatSuffix(2); + return tokenChain(stream,state,[">"],"bracket");} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],"bracket");}} + else if(c=="r"){ + c=stream.look(1); + if(c=="("){ + stream.eatSuffix(2); + return tokenChain(stream,state,[")"],RXstyle,RXmodifiers);} + if(c=="["){ + stream.eatSuffix(2); + return tokenChain(stream,state,["]"],RXstyle,RXmodifiers);} + if(c=="{"){ + stream.eatSuffix(2); + return tokenChain(stream,state,["}"],RXstyle,RXmodifiers);} + if(c=="<"){ + stream.eatSuffix(2); + return tokenChain(stream,state,[">"],RXstyle,RXmodifiers);} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers);}} + else if(/[\^'"!~\/(\[{<]/.test(c)){ + if(c=="("){ + stream.eatSuffix(1); + return tokenChain(stream,state,[")"],"string");} + if(c=="["){ + stream.eatSuffix(1); + return tokenChain(stream,state,["]"],"string");} + if(c=="{"){ + stream.eatSuffix(1); + return tokenChain(stream,state,["}"],"string");} + if(c=="<"){ + stream.eatSuffix(1); + return tokenChain(stream,state,[">"],"string");} + if(/[\^'"!~\/]/.test(c)){ + return tokenChain(stream,state,[stream.eat(c)],"string");}}}} + if(ch=="m"){ + var c=stream.look(-2); + if(!(c&&/\w/.test(c))){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(/[\^'"!~\/]/.test(c)){ + return tokenChain(stream,state,[c],RXstyle,RXmodifiers);} + if(c=="("){ + return tokenChain(stream,state,[")"],RXstyle,RXmodifiers);} + if(c=="["){ + return tokenChain(stream,state,["]"],RXstyle,RXmodifiers);} + if(c=="{"){ + return tokenChain(stream,state,["}"],RXstyle,RXmodifiers);} + if(c=="<"){ + return tokenChain(stream,state,[">"],RXstyle,RXmodifiers);}}}} + if(ch=="s"){ + var c=/[\/>\]})\w]/.test(stream.look(-2)); + if(!c){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(c=="[") + return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); + if(c=="{") + return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); + if(c=="<") + return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); + if(c=="(") + return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); + return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}} + if(ch=="y"){ + var c=/[\/>\]})\w]/.test(stream.look(-2)); + if(!c){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(c=="[") + return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); + if(c=="{") + return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); + if(c=="<") + return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); + if(c=="(") + return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); + return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}} + if(ch=="t"){ + var c=/[\/>\]})\w]/.test(stream.look(-2)); + if(!c){ + c=stream.eat("r");if(c){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(c=="[") + return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); + if(c=="{") + return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); + if(c=="<") + return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); + if(c=="(") + return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); + return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}}} + if(ch=="`"){ + return tokenChain(stream,state,[ch],"variable-2");} + if(ch=="/"){ + if(!/~\s*$/.test(stream.prefix())) + return "operator"; + else + return tokenChain(stream,state,[ch],RXstyle,RXmodifiers);} + if(ch=="$"){ + var p=stream.pos; + if(stream.eatWhile(/\d/)||stream.eat("{")&&stream.eatWhile(/\d/)&&stream.eat("}")) + return "variable-2"; + else + stream.pos=p;} + if(/[$@%]/.test(ch)){ + var p=stream.pos; + if(stream.eat("^")&&stream.eat(/[A-Z]/)||!/[@$%&]/.test(stream.look(-2))&&stream.eat(/[=|\\\-#?@;:&`~\^!\[\]*'"$+.,\/<>()]/)){ + var c=stream.current(); + if(PERL[c]) + return "variable-2";} + stream.pos=p;} + if(/[$@%&]/.test(ch)){ + if(stream.eatWhile(/[\w$\[\]]/)||stream.eat("{")&&stream.eatWhile(/[\w$\[\]]/)&&stream.eat("}")){ + var c=stream.current(); + if(PERL[c]) + return "variable-2"; + else + return "variable";}} + if(ch=="#"){ + if(stream.look(-2)!="$"){ + stream.skipToEnd(); + return "comment";}} + if(/[:+\-\^*$&%@=<>!?|\/~\.]/.test(ch)){ + var p=stream.pos; + stream.eatWhile(/[:+\-\^*$&%@=<>!?|\/~\.]/); + if(PERL[stream.current()]) + return "operator"; + else + stream.pos=p;} + if(ch=="_"){ + if(stream.pos==1){ + if(stream.suffix(6)=="_END__"){ + return tokenChain(stream,state,['\0'],"comment");} + else if(stream.suffix(7)=="_DATA__"){ + return tokenChain(stream,state,['\0'],"variable-2");} + else if(stream.suffix(7)=="_C__"){ + return tokenChain(stream,state,['\0'],"string");}}} + if(/\w/.test(ch)){ + var p=stream.pos; + if(stream.look(-2)=="{"&&(stream.look(0)=="}"||stream.eatWhile(/\w/)&&stream.look(0)=="}")) + return "string"; + else + stream.pos=p;} + if(/[A-Z]/.test(ch)){ + var l=stream.look(-2); + var p=stream.pos; + stream.eatWhile(/[A-Z_]/); + if(/[\da-z]/.test(stream.look(0))){ + stream.pos=p;} + else{ + var c=PERL[stream.current()]; + if(!c) + return "meta"; + if(c[1]) + c=c[0]; + if(l!=":"){ + if(c==1) + return "keyword"; + else if(c==2) + return "def"; + else if(c==3) + return "atom"; + else if(c==4) + return "operator"; + else if(c==5) + return "variable-2"; + else + return "meta";} + else + return "meta";}} + if(/[a-zA-Z_]/.test(ch)){ + var l=stream.look(-2); + stream.eatWhile(/\w/); + var c=PERL[stream.current()]; + if(!c) + return "meta"; + if(c[1]) + c=c[0]; + if(l!=":"){ + if(c==1) + return "keyword"; + else if(c==2) + return "def"; + else if(c==3) + return "atom"; + else if(c==4) + return "operator"; + else if(c==5) + return "variable-2"; + else + return "meta";} + else + return "meta";} + return null;} + + return{ + startState:function(){ + return{ + tokenize:tokenPerl, + chain:null, + style:null, + tail:null};}, + token:function(stream,state){ + return (state.tokenize||tokenPerl)(stream,state);}, + electricChars:"{}"};}); + +CodeMirror.defineMIME("text/x-fhem", "fhem"); + +// it's like "peek", but need for look-ahead or look-behind if index < 0 +CodeMirror.StringStream.prototype.look=function(c){ + return this.string.charAt(this.pos+(c||0));}; + +// return a part of prefix of current stream from current position +CodeMirror.StringStream.prototype.prefix=function(c){ + if(c){ + var x=this.pos-c; + return this.string.substr((x>=0?x:0),c);} + else{ + return this.string.substr(0,this.pos-1);}}; + +// return a part of suffix of current stream from current position +CodeMirror.StringStream.prototype.suffix=function(c){ + var y=this.string.length; + var x=y-this.pos+1; + return this.string.substr(this.pos,(c&&c=(y=this.string.length-1)) + this.pos=y; + else + this.pos=x;}; + +Array.prototype.foreach = function( callback ) { + for( var k=0; k iname){ + startIndex = middle + 1; + } + //recalculate middle + middle = Math.floor((stopIndex + startIndex)/2); + } + //make sure it's the right value + return (iname != name) ? null : items[middle]; + } + + function finish(keys,devs,vars,word) { + var res = []; + keys.foreach( function( k, v ) { + if( v.lastIndexOf(word,0)==0) + res.push({text:v,className:"hintkeyword"}); + }); + devs.foreach( function( k, v ) { + if( v.NAME.lastIndexOf(word,0)==0) { + if( v.ATTR && v.ATTR.alias != null) { + res.push({ text:v.NAME, displayText: v.NAME + " # " + v.ATTR.alias}); + } + else { + res.push(v.NAME); + } + } + }); + vars.foreach( function( k, v ) { + if( v.lastIndexOf(word,0)==0) + res.push({text:v,className:"hintvariable"}); + }); + callback( { + list: res, + from: CodeMirror.Pos(cur.line, start), + to: CodeMirror.Pos(cur.line, end) + }); + } +}); diff --git a/fhem/www/codemirror/mode/javascript/index.html b/fhem/www/codemirror/mode/javascript/index.html new file mode 100644 index 000000000..45d70ffdb --- /dev/null +++ b/fhem/www/codemirror/mode/javascript/index.html @@ -0,0 +1,107 @@ + + +CodeMirror: JavaScript mode + + + + + + + + + + + + +
+

JavaScript mode

+ + +
+ + + +

+ JavaScript mode supports a two configuration + options: +

    +
  • json which will set the mode to expect JSON + data rather than a JavaScript program.
  • +
  • typescript which will activate additional + syntax highlighting and some other things for TypeScript code + (demo).
  • +
  • statementIndent which (given a number) will + determine the amount of indentation to use for statements + continued on a new line.
  • +
+

+ +

MIME types defined: text/javascript, application/json, text/typescript, application/typescript.

+
diff --git a/fhem/www/codemirror/mode/javascript/javascript.js b/fhem/www/codemirror/mode/javascript/javascript.js new file mode 100644 index 000000000..fbf574b4f --- /dev/null +++ b/fhem/www/codemirror/mode/javascript/javascript.js @@ -0,0 +1,630 @@ +// TODO actually recognize syntax of TypeScript constructs + +CodeMirror.defineMode("javascript", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var statementIndent = parserConfig.statementIndent; + var jsonMode = parserConfig.json; + var isTS = parserConfig.typescript; + + // Tokenizer + + var keywords = function(){ + function kw(type) {return {type: type, style: "keyword"};} + var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c"); + var operator = kw("operator"), atom = {type: "atom", style: "atom"}; + + var jsKeywords = { + "if": kw("if"), "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B, + "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C, "debugger": C, + "var": kw("var"), "const": kw("var"), "let": kw("var"), + "function": kw("function"), "catch": kw("catch"), + "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"), + "in": operator, "typeof": operator, "instanceof": operator, + "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom, + "this": kw("this"), "module": kw("module"), "class": kw("class"), "super": kw("atom"), + "yield": C, "export": kw("export"), "import": kw("import"), "extends": C + }; + + // Extend the 'normal' keywords with the TypeScript language extensions + if (isTS) { + var type = {type: "variable", style: "variable-3"}; + var tsKeywords = { + // object-like things + "interface": kw("interface"), + "extends": kw("extends"), + "constructor": kw("constructor"), + + // scope modifiers + "public": kw("public"), + "private": kw("private"), + "protected": kw("protected"), + "static": kw("static"), + + // types + "string": type, "number": type, "bool": type, "any": type + }; + + for (var attr in tsKeywords) { + jsKeywords[attr] = tsKeywords[attr]; + } + } + + return jsKeywords; + }(); + + var isOperatorChar = /[+\-*&%=<>!?|~^]/; + + function readRegexp(stream) { + var escaped = false, next, inSet = false; + while ((next = stream.next()) != null) { + if (!escaped) { + if (next == "/" && !inSet) return; + if (next == "[") inSet = true; + else if (inSet && next == "]") inSet = false; + } + escaped = !escaped && next == "\\"; + } + } + + // Used as scratch variables to communicate multiple values without + // consing up tons of objects. + var type, content; + function ret(tp, style, cont) { + type = tp; content = cont; + return style; + } + function tokenBase(stream, state) { + var ch = stream.next(); + if (ch == '"' || ch == "'") { + state.tokenize = tokenString(ch); + return state.tokenize(stream, state); + } else if (ch == "." && stream.match(/^\d+(?:[eE][+\-]?\d+)?/)) { + return ret("number", "number"); + } else if (ch == "." && stream.match("..")) { + return ret("spread", "meta"); + } else if (/[\[\]{}\(\),;\:\.]/.test(ch)) { + return ret(ch); + } else if (ch == "=" && stream.eat(">")) { + return ret("=>", "operator"); + } else if (ch == "0" && stream.eat(/x/i)) { + stream.eatWhile(/[\da-f]/i); + return ret("number", "number"); + } else if (/\d/.test(ch)) { + stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/); + return ret("number", "number"); + } else if (ch == "/") { + if (stream.eat("*")) { + state.tokenize = tokenComment; + return tokenComment(stream, state); + } else if (stream.eat("/")) { + stream.skipToEnd(); + return ret("comment", "comment"); + } else if (state.lastType == "operator" || state.lastType == "keyword c" || + state.lastType == "sof" || /^[\[{}\(,;:]$/.test(state.lastType)) { + readRegexp(stream); + stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla + return ret("regexp", "string-2"); + } else { + stream.eatWhile(isOperatorChar); + return ret("operator", "operator", stream.current()); + } + } else if (ch == "`") { + state.tokenize = tokenQuasi; + return tokenQuasi(stream, state); + } else if (ch == "#") { + stream.skipToEnd(); + return ret("error", "error"); + } else if (isOperatorChar.test(ch)) { + stream.eatWhile(isOperatorChar); + return ret("operator", "operator", stream.current()); + } else { + stream.eatWhile(/[\w\$_]/); + var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word]; + return (known && state.lastType != ".") ? ret(known.type, known.style, word) : + ret("variable", "variable", word); + } + } + + function tokenString(quote) { + return function(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (next == quote && !escaped) break; + escaped = !escaped && next == "\\"; + } + if (!escaped) state.tokenize = tokenBase; + return ret("string", "string"); + }; + } + + function tokenComment(stream, state) { + var maybeEnd = false, ch; + while (ch = stream.next()) { + if (ch == "/" && maybeEnd) { + state.tokenize = tokenBase; + break; + } + maybeEnd = (ch == "*"); + } + return ret("comment", "comment"); + } + + function tokenQuasi(stream, state) { + var escaped = false, next; + while ((next = stream.next()) != null) { + if (!escaped && (next == "`" || next == "$" && stream.eat("{"))) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && next == "\\"; + } + return ret("quasi", "string-2", stream.current()); + } + + var brackets = "([{}])"; + // This is a crude lookahead trick to try and notice that we're + // parsing the argument patterns for a fat-arrow function before we + // actually hit the arrow token. It only works if the arrow is on + // the same line as the arguments and there's no strange noise + // (comments) in between. Fallback is to only notice when we hit the + // arrow, and not declare the arguments as locals for the arrow + // body. + function findFatArrow(stream, state) { + if (state.fatArrowAt) state.fatArrowAt = null; + var arrow = stream.string.indexOf("=>", stream.start); + if (arrow < 0) return; + + var depth = 0, sawSomething = false; + for (var pos = arrow - 1; pos >= 0; --pos) { + var ch = stream.string.charAt(pos); + var bracket = brackets.indexOf(ch); + if (bracket >= 0 && bracket < 3) { + if (!depth) { ++pos; break; } + if (--depth == 0) break; + } else if (bracket >= 3 && bracket < 6) { + ++depth; + } else if (/[$\w]/.test(ch)) { + sawSomething = true; + } else if (sawSomething && !depth) { + ++pos; + break; + } + } + if (sawSomething && !depth) state.fatArrowAt = pos; + } + + // Parser + + var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true, "this": true}; + + function JSLexical(indented, column, type, align, prev, info) { + this.indented = indented; + this.column = column; + this.type = type; + this.prev = prev; + this.info = info; + if (align != null) this.align = align; + } + + function inScope(state, varname) { + for (var v = state.localVars; v; v = v.next) + if (v.name == varname) return true; + for (var cx = state.context; cx; cx = cx.prev) { + for (var v = cx.vars; v; v = v.next) + if (v.name == varname) return true; + } + } + + function parseJS(state, style, type, content, stream) { + var cc = state.cc; + // Communicate our context to the combinators. + // (Less wasteful than consing up a hundred closures on every call.) + cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc; + + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = true; + + while(true) { + var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement; + if (combinator(type, content)) { + while(cc.length && cc[cc.length - 1].lex) + cc.pop()(); + if (cx.marked) return cx.marked; + if (type == "variable" && inScope(state, content)) return "variable-2"; + return style; + } + } + } + + // Combinator utils + + var cx = {state: null, column: null, marked: null, cc: null}; + function pass() { + for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]); + } + function cont() { + pass.apply(null, arguments); + return true; + } + function register(varname) { + function inList(list) { + for (var v = list; v; v = v.next) + if (v.name == varname) return true; + return false; + } + var state = cx.state; + if (state.context) { + cx.marked = "def"; + if (inList(state.localVars)) return; + state.localVars = {name: varname, next: state.localVars}; + } else { + if (inList(state.globalVars)) return; + if (parserConfig.globalVars) + state.globalVars = {name: varname, next: state.globalVars}; + } + } + + // Combinators + + var defaultVars = {name: "this", next: {name: "arguments"}}; + function pushcontext() { + cx.state.context = {prev: cx.state.context, vars: cx.state.localVars}; + cx.state.localVars = defaultVars; + } + function popcontext() { + cx.state.localVars = cx.state.context.vars; + cx.state.context = cx.state.context.prev; + } + function pushlex(type, info) { + var result = function() { + var state = cx.state, indent = state.indented; + if (state.lexical.type == "stat") indent = state.lexical.indented; + state.lexical = new JSLexical(indent, cx.stream.column(), type, null, state.lexical, info); + }; + result.lex = true; + return result; + } + function poplex() { + var state = cx.state; + if (state.lexical.prev) { + if (state.lexical.type == ")") + state.indented = state.lexical.indented; + state.lexical = state.lexical.prev; + } + } + poplex.lex = true; + + function expect(wanted) { + return function(type) { + if (type == wanted) return cont(); + else if (wanted == ";") return pass(); + else return cont(arguments.callee); + }; + } + + function statement(type, value) { + if (type == "var") return cont(pushlex("vardef", value.length), vardef, expect(";"), poplex); + if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex); + if (type == "keyword b") return cont(pushlex("form"), statement, poplex); + if (type == "{") return cont(pushlex("}"), block, poplex); + if (type == ";") return cont(); + if (type == "if") return cont(pushlex("form"), expression, statement, poplex, maybeelse); + if (type == "function") return cont(functiondef); + if (type == "for") return cont(pushlex("form"), forspec, statement, poplex); + if (type == "variable") return cont(pushlex("stat"), maybelabel); + if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"), + block, poplex, poplex); + if (type == "case") return cont(expression, expect(":")); + if (type == "default") return cont(expect(":")); + if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"), + statement, poplex, popcontext); + if (type == "module") return cont(pushlex("form"), pushcontext, afterModule, popcontext, poplex); + if (type == "class") return cont(pushlex("form"), className, objlit, poplex); + if (type == "export") return cont(pushlex("form"), afterExport, poplex); + if (type == "import") return cont(pushlex("form"), afterImport, poplex); + return pass(pushlex("stat"), expression, expect(";"), poplex); + } + function expression(type) { + return expressionInner(type, false); + } + function expressionNoComma(type) { + return expressionInner(type, true); + } + function expressionInner(type, noComma) { + if (cx.state.fatArrowAt == cx.stream.start) { + var body = noComma ? arrowBodyNoComma : arrowBody; + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(pattern, ")"), poplex, expect("=>"), body, popcontext); + else if (type == "variable") return pass(pushcontext, pattern, expect("=>"), body, popcontext); + } + + var maybeop = noComma ? maybeoperatorNoComma : maybeoperatorComma; + if (atomicTypes.hasOwnProperty(type)) return cont(maybeop); + if (type == "function") return cont(functiondef); + if (type == "keyword c") return cont(noComma ? maybeexpressionNoComma : maybeexpression); + if (type == "(") return cont(pushlex(")"), maybeexpression, comprehension, expect(")"), poplex, maybeop); + if (type == "operator" || type == "spread") return cont(noComma ? expressionNoComma : expression); + if (type == "[") return cont(pushlex("]"), arrayLiteral, poplex, maybeop); + if (type == "{") return contCommasep(objprop, "}", null, maybeop); + return cont(); + } + function maybeexpression(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expression); + } + function maybeexpressionNoComma(type) { + if (type.match(/[;\}\)\],]/)) return pass(); + return pass(expressionNoComma); + } + + function maybeoperatorComma(type, value) { + if (type == ",") return cont(expression); + return maybeoperatorNoComma(type, value, false); + } + function maybeoperatorNoComma(type, value, noComma) { + var me = noComma == false ? maybeoperatorComma : maybeoperatorNoComma; + var expr = noComma == false ? expression : expressionNoComma; + if (value == "=>") return cont(pushcontext, noComma ? arrowBodyNoComma : arrowBody, popcontext); + if (type == "operator") { + if (/\+\+|--/.test(value)) return cont(me); + if (value == "?") return cont(expression, expect(":"), expr); + return cont(expr); + } + if (type == "quasi") { cx.cc.push(me); return quasi(value); } + if (type == ";") return; + if (type == "(") return contCommasep(expressionNoComma, ")", "call", me); + if (type == ".") return cont(property, me); + if (type == "[") return cont(pushlex("]"), maybeexpression, expect("]"), poplex, me); + } + function quasi(value) { + if (value.slice(value.length - 2) != "${") return cont(); + return cont(expression, continueQuasi); + } + function continueQuasi(type) { + if (type == "}") { + cx.marked = "string-2"; + cx.state.tokenize = tokenQuasi; + return cont(); + } + } + function arrowBody(type) { + findFatArrow(cx.stream, cx.state); + if (type == "{") return pass(statement); + return pass(expression); + } + function arrowBodyNoComma(type) { + findFatArrow(cx.stream, cx.state); + if (type == "{") return pass(statement); + return pass(expressionNoComma); + } + function maybelabel(type) { + if (type == ":") return cont(poplex, statement); + return pass(maybeoperatorComma, expect(";"), poplex); + } + function property(type) { + if (type == "variable") {cx.marked = "property"; return cont();} + } + function objprop(type, value) { + if (type == "variable") { + cx.marked = "property"; + if (value == "get" || value == "set") return cont(getterSetter); + } else if (type == "number" || type == "string") { + cx.marked = type + " property"; + } else if (type == "[") { + return cont(expression, expect("]"), afterprop); + } + if (atomicTypes.hasOwnProperty(type)) return cont(afterprop); + } + function getterSetter(type) { + if (type != "variable") return pass(afterprop); + cx.marked = "property"; + return cont(functiondef); + } + function afterprop(type) { + if (type == ":") return cont(expressionNoComma); + if (type == "(") return pass(functiondef); + } + function commasep(what, end) { + function proceed(type) { + if (type == ",") { + var lex = cx.state.lexical; + if (lex.info == "call") lex.pos = (lex.pos || 0) + 1; + return cont(what, proceed); + } + if (type == end) return cont(); + return cont(expect(end)); + } + return function(type) { + if (type == end) return cont(); + return pass(what, proceed); + }; + } + function contCommasep(what, end, info) { + for (var i = 3; i < arguments.length; i++) + cx.cc.push(arguments[i]); + return cont(pushlex(end, info), commasep(what, end), poplex); + } + function block(type) { + if (type == "}") return cont(); + return pass(statement, block); + } + function maybetype(type) { + if (isTS && type == ":") return cont(typedef); + } + function typedef(type) { + if (type == "variable"){cx.marked = "variable-3"; return cont();} + } + function vardef() { + return pass(pattern, maybetype, maybeAssign, vardefCont); + } + function pattern(type, value) { + if (type == "variable") { register(value); return cont(); } + if (type == "[") return contCommasep(pattern, "]"); + if (type == "{") return contCommasep(proppattern, "}"); + } + function proppattern(type, value) { + if (type == "variable" && !cx.stream.match(/^\s*:/, false)) { + register(value); + return cont(maybeAssign); + } + if (type == "variable") cx.marked = "property"; + return cont(expect(":"), pattern, maybeAssign); + } + function maybeAssign(_type, value) { + if (value == "=") return cont(expressionNoComma); + } + function vardefCont(type) { + if (type == ",") return cont(vardef); + } + function maybeelse(type, value) { + if (type == "keyword b" && value == "else") return cont(pushlex("form"), statement, poplex); + } + function forspec(type) { + if (type == "(") return cont(pushlex(")"), forspec1, expect(")"), poplex); + } + function forspec1(type) { + if (type == "var") return cont(vardef, expect(";"), forspec2); + if (type == ";") return cont(forspec2); + if (type == "variable") return cont(formaybeinof); + return pass(expression, expect(";"), forspec2); + } + function formaybeinof(_type, value) { + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } + return cont(maybeoperatorComma, forspec2); + } + function forspec2(type, value) { + if (type == ";") return cont(forspec3); + if (value == "in" || value == "of") { cx.marked = "keyword"; return cont(expression); } + return pass(expression, expect(";"), forspec3); + } + function forspec3(type) { + if (type != ")") cont(expression); + } + function functiondef(type, value) { + if (value == "*") {cx.marked = "keyword"; return cont(functiondef);} + if (type == "variable") {register(value); return cont(functiondef);} + if (type == "(") return cont(pushcontext, pushlex(")"), commasep(funarg, ")"), poplex, statement, popcontext); + } + function funarg(type) { + if (type == "spread") return cont(funarg); + return pass(pattern, maybetype); + } + function className(type, value) { + if (type == "variable") {register(value); return cont(classNameAfter);} + } + function classNameAfter(_type, value) { + if (value == "extends") return cont(expression); + } + function objlit(type) { + if (type == "{") return contCommasep(objprop, "}"); + } + function afterModule(type, value) { + if (type == "string") return cont(statement); + if (type == "variable") { register(value); return cont(maybeFrom); } + } + function afterExport(_type, value) { + if (value == "*") { cx.marked = "keyword"; return cont(maybeFrom, expect(";")); } + if (value == "default") { cx.marked = "keyword"; return cont(expression, expect(";")); } + return pass(statement); + } + function afterImport(type) { + if (type == "string") return cont(); + return pass(importSpec, maybeFrom); + } + function importSpec(type, value) { + if (type == "{") return contCommasep(importSpec, "}"); + if (type == "variable") register(value); + return cont(); + } + function maybeFrom(_type, value) { + if (value == "from") { cx.marked = "keyword"; return cont(expression); } + } + function arrayLiteral(type) { + if (type == "]") return cont(); + return pass(expressionNoComma, maybeArrayComprehension); + } + function maybeArrayComprehension(type) { + if (type == "for") return pass(comprehension, expect("]")); + if (type == ",") return cont(commasep(expressionNoComma, "]")); + return pass(commasep(expressionNoComma, "]")); + } + function comprehension(type) { + if (type == "for") return cont(forspec, comprehension); + if (type == "if") return cont(expression, comprehension); + } + + // Interface + + return { + startState: function(basecolumn) { + var state = { + tokenize: tokenBase, + lastType: "sof", + cc: [], + lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false), + localVars: parserConfig.localVars, + context: parserConfig.localVars && {vars: parserConfig.localVars}, + indented: 0 + }; + if (parserConfig.globalVars) state.globalVars = parserConfig.globalVars; + return state; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (!state.lexical.hasOwnProperty("align")) + state.lexical.align = false; + state.indented = stream.indentation(); + findFatArrow(stream, state); + } + if (state.tokenize != tokenComment && stream.eatSpace()) return null; + var style = state.tokenize(stream, state); + if (type == "comment") return style; + state.lastType = type == "operator" && (content == "++" || content == "--") ? "incdec" : type; + return parseJS(state, style, type, content, stream); + }, + + indent: function(state, textAfter) { + if (state.tokenize == tokenComment) return CodeMirror.Pass; + if (state.tokenize != tokenBase) return 0; + var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical; + // Kludge to prevent 'maybelse' from blocking lexical scope pops + for (var i = state.cc.length - 1; i >= 0; --i) { + var c = state.cc[i]; + if (c == poplex) lexical = lexical.prev; + else if (c != maybeelse) break; + } + if (lexical.type == "stat" && firstChar == "}") lexical = lexical.prev; + if (statementIndent && lexical.type == ")" && lexical.prev.type == "stat") + lexical = lexical.prev; + var type = lexical.type, closing = firstChar == type; + + if (type == "vardef") return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? lexical.info + 1 : 0); + else if (type == "form" && firstChar == "{") return lexical.indented; + else if (type == "form") return lexical.indented + indentUnit; + else if (type == "stat") + return lexical.indented + (state.lastType == "operator" || state.lastType == "," ? statementIndent || indentUnit : 0); + else if (lexical.info == "switch" && !closing && parserConfig.doubleIndentSwitch != false) + return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit); + else if (lexical.align) return lexical.column + (closing ? 0 : 1); + else return lexical.indented + (closing ? 0 : indentUnit); + }, + + electricChars: ":{}", + blockCommentStart: jsonMode ? null : "/*", + blockCommentEnd: jsonMode ? null : "*/", + lineComment: jsonMode ? null : "//", + fold: "brace", + + helperType: jsonMode ? "json" : "javascript", + jsonMode: jsonMode + }; +}); + +CodeMirror.defineMIME("text/javascript", "javascript"); +CodeMirror.defineMIME("text/ecmascript", "javascript"); +CodeMirror.defineMIME("application/javascript", "javascript"); +CodeMirror.defineMIME("application/ecmascript", "javascript"); +CodeMirror.defineMIME("application/json", {name: "javascript", json: true}); +CodeMirror.defineMIME("application/x-json", {name: "javascript", json: true}); +CodeMirror.defineMIME("text/typescript", { name: "javascript", typescript: true }); +CodeMirror.defineMIME("application/typescript", { name: "javascript", typescript: true }); diff --git a/fhem/www/codemirror/mode/javascript/test.js b/fhem/www/codemirror/mode/javascript/test.js new file mode 100644 index 000000000..911bbbfd7 --- /dev/null +++ b/fhem/www/codemirror/mode/javascript/test.js @@ -0,0 +1,113 @@ +(function() { + var mode = CodeMirror.getMode({indentUnit: 2}, "javascript"); + function MT(name) { test.mode(name, mode, Array.prototype.slice.call(arguments, 1)); } + + MT("locals", + "[keyword function] [variable foo]([def a], [def b]) { [keyword var] [def c] [operator =] [number 10]; [keyword return] [variable-2 a] [operator +] [variable-2 c] [operator +] [variable d]; }"); + + MT("comma-and-binop", + "[keyword function](){ [keyword var] [def x] [operator =] [number 1] [operator +] [number 2], [def y]; }"); + + MT("destructuring", + "([keyword function]([def a], [[[def b], [def c] ]]) {", + " [keyword let] {[def d], [property foo]: [def c][operator =][number 10], [def x]} [operator =] [variable foo]([variable-2 a]);", + " [[[variable-2 c], [variable y] ]] [operator =] [variable-2 c];", + "})();"); + + MT("class", + "[keyword class] [variable Point] [keyword extends] [variable SuperThing] {", + " [[ [string-2 /expr/] ]]: [number 24],", + " [property constructor]([def x], [def y]) {", + " [keyword super]([string 'something']);", + " [keyword this].[property x] [operator =] [variable-2 x];", + " }", + "}"); + + MT("module", + "[keyword module] [string 'foo'] {", + " [keyword export] [keyword let] [def x] [operator =] [number 42];", + " [keyword export] [keyword *] [keyword from] [string 'somewhere'];", + "}"); + + MT("import", + "[keyword function] [variable foo]() {", + " [keyword import] [def $] [keyword from] [string 'jquery'];", + " [keyword module] [def crypto] [keyword from] [string 'crypto'];", + " [keyword import] { [def encrypt], [def decrypt] } [keyword from] [string 'crypto'];", + "}"); + + MT("const", + "[keyword function] [variable f]() {", + " [keyword const] [[ [def a], [def b] ]] [operator =] [[ [number 1], [number 2] ]];", + "}"); + + MT("for/of", + "[keyword for]([keyword let] [variable of] [keyword of] [variable something]) {}"); + + MT("generator", + "[keyword function*] [variable repeat]([def n]) {", + " [keyword for]([keyword var] [def i] [operator =] [number 0]; [variable-2 i] [operator <] [variable-2 n]; [operator ++][variable-2 i])", + " [keyword yield] [variable-2 i];", + "}"); + + MT("fatArrow", + "[variable array].[property filter]([def a] [operator =>] [variable-2 a] [operator +] [number 1]);", + "[variable a];", // No longer in scope + "[keyword let] [variable f] [operator =] ([[ [def a], [def b] ]], [def c]) [operator =>] [variable-2 a] [operator +] [variable-2 c];", + "[variable c];"); + + MT("spread", + "[keyword function] [variable f]([def a], [meta ...][def b]) {", + " [variable something]([variable-2 a], [meta ...][variable-2 b]);", + "}"); + + MT("comprehension", + "[keyword function] [variable f]() {", + " [[([variable x] [operator +] [number 1]) [keyword for] ([keyword var] [def x] [keyword in] [variable y]) [keyword if] [variable pred]([variable-2 x]) ]];", + " ([variable u] [keyword for] ([keyword var] [def u] [keyword of] [variable generateValues]()) [keyword if] ([variable-2 u].[property color] [operator ===] [string 'blue']));", + "}"); + + MT("quasi", + "[variable re][string-2 `fofdlakj${][variable x] [operator +] ([variable re][string-2 `foo`]) [operator +] [number 1][string-2 }fdsa`] [operator +] [number 2]"); + + MT("indent_statement", + "[keyword var] [variable x] [operator =] [number 10]", + "[variable x] [operator +=] [variable y] [operator +]", + " [atom Infinity]", + "[keyword debugger];"); + + MT("indent_if", + "[keyword if] ([number 1])", + " [keyword break];", + "[keyword else] [keyword if] ([number 2])", + " [keyword continue];", + "[keyword else]", + " [number 10];", + "[keyword if] ([number 1]) {", + " [keyword break];", + "} [keyword else] [keyword if] ([number 2]) {", + " [keyword continue];", + "} [keyword else] {", + " [number 10];", + "}"); + + MT("indent_for", + "[keyword for] ([keyword var] [variable i] [operator =] [number 0];", + " [variable i] [operator <] [number 100];", + " [variable i][operator ++])", + " [variable doSomething]([variable i]);", + "[keyword debugger];"); + + MT("indent_c_style", + "[keyword function] [variable foo]()", + "{", + " [keyword debugger];", + "}"); + + MT("multilinestring", + "[keyword var] [variable x] [operator =] [string 'foo\\]", + "[string bar'];"); + + MT("scary_regexp", + "[string-2 /foo[[/]]bar/];"); +})(); diff --git a/fhem/www/codemirror/mode/javascript/typescript.html b/fhem/www/codemirror/mode/javascript/typescript.html new file mode 100644 index 000000000..9cc5f4932 --- /dev/null +++ b/fhem/www/codemirror/mode/javascript/typescript.html @@ -0,0 +1,61 @@ + + +CodeMirror: TypeScript mode + + + + + + + + + +
+

TypeScript mode

+ + +
+ + + +

This is a specialization of the JavaScript mode.

+
diff --git a/fhem/www/codemirror/mode/perl/index.html b/fhem/www/codemirror/mode/perl/index.html new file mode 100644 index 000000000..2b7578efe --- /dev/null +++ b/fhem/www/codemirror/mode/perl/index.html @@ -0,0 +1,75 @@ + + +CodeMirror: Perl mode + + + + + + + + + +
+

Perl mode

+ + +
+ + + +

MIME types defined: text/x-perl.

+
diff --git a/fhem/www/codemirror/mode/perl/perl.js b/fhem/www/codemirror/mode/perl/perl.js new file mode 100644 index 000000000..5954b1a61 --- /dev/null +++ b/fhem/www/codemirror/mode/perl/perl.js @@ -0,0 +1,816 @@ +// CodeMirror2 mode/perl/perl.js (text/x-perl) beta 0.10 (2011-11-08) +// This is a part of CodeMirror from https://github.com/sabaca/CodeMirror_mode_perl (mail@sabaca.com) +CodeMirror.defineMode("perl",function(){ + // http://perldoc.perl.org + var PERL={ // null - magic touch + // 1 - keyword + // 2 - def + // 3 - atom + // 4 - operator + // 5 - variable-2 (predefined) + // [x,y] - x=1,2,3; y=must be defined if x{...} + // PERL operators + '->' : 4, + '++' : 4, + '--' : 4, + '**' : 4, + // ! ~ \ and unary + and - + '=~' : 4, + '!~' : 4, + '*' : 4, + '/' : 4, + '%' : 4, + 'x' : 4, + '+' : 4, + '-' : 4, + '.' : 4, + '<<' : 4, + '>>' : 4, + // named unary operators + '<' : 4, + '>' : 4, + '<=' : 4, + '>=' : 4, + 'lt' : 4, + 'gt' : 4, + 'le' : 4, + 'ge' : 4, + '==' : 4, + '!=' : 4, + '<=>' : 4, + 'eq' : 4, + 'ne' : 4, + 'cmp' : 4, + '~~' : 4, + '&' : 4, + '|' : 4, + '^' : 4, + '&&' : 4, + '||' : 4, + '//' : 4, + '..' : 4, + '...' : 4, + '?' : 4, + ':' : 4, + '=' : 4, + '+=' : 4, + '-=' : 4, + '*=' : 4, // etc. ??? + ',' : 4, + '=>' : 4, + '::' : 4, + // list operators (rightward) + 'not' : 4, + 'and' : 4, + 'or' : 4, + 'xor' : 4, + // PERL predefined variables (I know, what this is a paranoid idea, but may be needed for people, who learn PERL, and for me as well, ...and may be for you?;) + 'BEGIN' : [5,1], + 'END' : [5,1], + 'PRINT' : [5,1], + 'PRINTF' : [5,1], + 'GETC' : [5,1], + 'READ' : [5,1], + 'READLINE' : [5,1], + 'DESTROY' : [5,1], + 'TIE' : [5,1], + 'TIEHANDLE' : [5,1], + 'UNTIE' : [5,1], + 'STDIN' : 5, + 'STDIN_TOP' : 5, + 'STDOUT' : 5, + 'STDOUT_TOP' : 5, + 'STDERR' : 5, + 'STDERR_TOP' : 5, + '$ARG' : 5, + '$_' : 5, + '@ARG' : 5, + '@_' : 5, + '$LIST_SEPARATOR' : 5, + '$"' : 5, + '$PROCESS_ID' : 5, + '$PID' : 5, + '$$' : 5, + '$REAL_GROUP_ID' : 5, + '$GID' : 5, + '$(' : 5, + '$EFFECTIVE_GROUP_ID' : 5, + '$EGID' : 5, + '$)' : 5, + '$PROGRAM_NAME' : 5, + '$0' : 5, + '$SUBSCRIPT_SEPARATOR' : 5, + '$SUBSEP' : 5, + '$;' : 5, + '$REAL_USER_ID' : 5, + '$UID' : 5, + '$<' : 5, + '$EFFECTIVE_USER_ID' : 5, + '$EUID' : 5, + '$>' : 5, + '$a' : 5, + '$b' : 5, + '$COMPILING' : 5, + '$^C' : 5, + '$DEBUGGING' : 5, + '$^D' : 5, + '${^ENCODING}' : 5, + '$ENV' : 5, + '%ENV' : 5, + '$SYSTEM_FD_MAX' : 5, + '$^F' : 5, + '@F' : 5, + '${^GLOBAL_PHASE}' : 5, + '$^H' : 5, + '%^H' : 5, + '@INC' : 5, + '%INC' : 5, + '$INPLACE_EDIT' : 5, + '$^I' : 5, + '$^M' : 5, + '$OSNAME' : 5, + '$^O' : 5, + '${^OPEN}' : 5, + '$PERLDB' : 5, + '$^P' : 5, + '$SIG' : 5, + '%SIG' : 5, + '$BASETIME' : 5, + '$^T' : 5, + '${^TAINT}' : 5, + '${^UNICODE}' : 5, + '${^UTF8CACHE}' : 5, + '${^UTF8LOCALE}' : 5, + '$PERL_VERSION' : 5, + '$^V' : 5, + '${^WIN32_SLOPPY_STAT}' : 5, + '$EXECUTABLE_NAME' : 5, + '$^X' : 5, + '$1' : 5, // - regexp $1, $2... + '$MATCH' : 5, + '$&' : 5, + '${^MATCH}' : 5, + '$PREMATCH' : 5, + '$`' : 5, + '${^PREMATCH}' : 5, + '$POSTMATCH' : 5, + "$'" : 5, + '${^POSTMATCH}' : 5, + '$LAST_PAREN_MATCH' : 5, + '$+' : 5, + '$LAST_SUBMATCH_RESULT' : 5, + '$^N' : 5, + '@LAST_MATCH_END' : 5, + '@+' : 5, + '%LAST_PAREN_MATCH' : 5, + '%+' : 5, + '@LAST_MATCH_START' : 5, + '@-' : 5, + '%LAST_MATCH_START' : 5, + '%-' : 5, + '$LAST_REGEXP_CODE_RESULT' : 5, + '$^R' : 5, + '${^RE_DEBUG_FLAGS}' : 5, + '${^RE_TRIE_MAXBUF}' : 5, + '$ARGV' : 5, + '@ARGV' : 5, + 'ARGV' : 5, + 'ARGVOUT' : 5, + '$OUTPUT_FIELD_SEPARATOR' : 5, + '$OFS' : 5, + '$,' : 5, + '$INPUT_LINE_NUMBER' : 5, + '$NR' : 5, + '$.' : 5, + '$INPUT_RECORD_SEPARATOR' : 5, + '$RS' : 5, + '$/' : 5, + '$OUTPUT_RECORD_SEPARATOR' : 5, + '$ORS' : 5, + '$\\' : 5, + '$OUTPUT_AUTOFLUSH' : 5, + '$|' : 5, + '$ACCUMULATOR' : 5, + '$^A' : 5, + '$FORMAT_FORMFEED' : 5, + '$^L' : 5, + '$FORMAT_PAGE_NUMBER' : 5, + '$%' : 5, + '$FORMAT_LINES_LEFT' : 5, + '$-' : 5, + '$FORMAT_LINE_BREAK_CHARACTERS' : 5, + '$:' : 5, + '$FORMAT_LINES_PER_PAGE' : 5, + '$=' : 5, + '$FORMAT_TOP_NAME' : 5, + '$^' : 5, + '$FORMAT_NAME' : 5, + '$~' : 5, + '${^CHILD_ERROR_NATIVE}' : 5, + '$EXTENDED_OS_ERROR' : 5, + '$^E' : 5, + '$EXCEPTIONS_BEING_CAUGHT' : 5, + '$^S' : 5, + '$WARNING' : 5, + '$^W' : 5, + '${^WARNING_BITS}' : 5, + '$OS_ERROR' : 5, + '$ERRNO' : 5, + '$!' : 5, + '%OS_ERROR' : 5, + '%ERRNO' : 5, + '%!' : 5, + '$CHILD_ERROR' : 5, + '$?' : 5, + '$EVAL_ERROR' : 5, + '$@' : 5, + '$OFMT' : 5, + '$#' : 5, + '$*' : 5, + '$ARRAY_BASE' : 5, + '$[' : 5, + '$OLD_PERL_VERSION' : 5, + '$]' : 5, + // PERL blocks + 'if' :[1,1], + elsif :[1,1], + 'else' :[1,1], + 'while' :[1,1], + unless :[1,1], + 'for' :[1,1], + foreach :[1,1], + // PERL functions + 'abs' :1, // - absolute value function + accept :1, // - accept an incoming socket connect + alarm :1, // - schedule a SIGALRM + 'atan2' :1, // - arctangent of Y/X in the range -PI to PI + bind :1, // - binds an address to a socket + binmode :1, // - prepare binary files for I/O + bless :1, // - create an object + bootstrap :1, // + 'break' :1, // - break out of a "given" block + caller :1, // - get context of the current subroutine call + chdir :1, // - change your current working directory + chmod :1, // - changes the permissions on a list of files + chomp :1, // - remove a trailing record separator from a string + chop :1, // - remove the last character from a string + chown :1, // - change the owership on a list of files + chr :1, // - get character this number represents + chroot :1, // - make directory new root for path lookups + close :1, // - close file (or pipe or socket) handle + closedir :1, // - close directory handle + connect :1, // - connect to a remote socket + 'continue' :[1,1], // - optional trailing block in a while or foreach + 'cos' :1, // - cosine function + crypt :1, // - one-way passwd-style encryption + dbmclose :1, // - breaks binding on a tied dbm file + dbmopen :1, // - create binding on a tied dbm file + 'default' :1, // + defined :1, // - test whether a value, variable, or function is defined + 'delete' :1, // - deletes a value from a hash + die :1, // - raise an exception or bail out + 'do' :1, // - turn a BLOCK into a TERM + dump :1, // - create an immediate core dump + each :1, // - retrieve the next key/value pair from a hash + endgrent :1, // - be done using group file + endhostent :1, // - be done using hosts file + endnetent :1, // - be done using networks file + endprotoent :1, // - be done using protocols file + endpwent :1, // - be done using passwd file + endservent :1, // - be done using services file + eof :1, // - test a filehandle for its end + 'eval' :1, // - catch exceptions or compile and run code + 'exec' :1, // - abandon this program to run another + exists :1, // - test whether a hash key is present + exit :1, // - terminate this program + 'exp' :1, // - raise I to a power + fcntl :1, // - file control system call + fileno :1, // - return file descriptor from filehandle + flock :1, // - lock an entire file with an advisory lock + fork :1, // - create a new process just like this one + format :1, // - declare a picture format with use by the write() function + formline :1, // - internal function used for formats + getc :1, // - get the next character from the filehandle + getgrent :1, // - get next group record + getgrgid :1, // - get group record given group user ID + getgrnam :1, // - get group record given group name + gethostbyaddr :1, // - get host record given its address + gethostbyname :1, // - get host record given name + gethostent :1, // - get next hosts record + getlogin :1, // - return who logged in at this tty + getnetbyaddr :1, // - get network record given its address + getnetbyname :1, // - get networks record given name + getnetent :1, // - get next networks record + getpeername :1, // - find the other end of a socket connection + getpgrp :1, // - get process group + getppid :1, // - get parent process ID + getpriority :1, // - get current nice value + getprotobyname :1, // - get protocol record given name + getprotobynumber :1, // - get protocol record numeric protocol + getprotoent :1, // - get next protocols record + getpwent :1, // - get next passwd record + getpwnam :1, // - get passwd record given user login name + getpwuid :1, // - get passwd record given user ID + getservbyname :1, // - get services record given its name + getservbyport :1, // - get services record given numeric port + getservent :1, // - get next services record + getsockname :1, // - retrieve the sockaddr for a given socket + getsockopt :1, // - get socket options on a given socket + given :1, // + glob :1, // - expand filenames using wildcards + gmtime :1, // - convert UNIX time into record or string using Greenwich time + 'goto' :1, // - create spaghetti code + grep :1, // - locate elements in a list test true against a given criterion + hex :1, // - convert a string to a hexadecimal number + 'import' :1, // - patch a module's namespace into your own + index :1, // - find a substring within a string + 'int' :1, // - get the integer portion of a number + ioctl :1, // - system-dependent device control system call + 'join' :1, // - join a list into a string using a separator + keys :1, // - retrieve list of indices from a hash + kill :1, // - send a signal to a process or process group + last :1, // - exit a block prematurely + lc :1, // - return lower-case version of a string + lcfirst :1, // - return a string with just the next letter in lower case + length :1, // - return the number of bytes in a string + 'link' :1, // - create a hard link in the filesytem + listen :1, // - register your socket as a server + local : 2, // - create a temporary value for a global variable (dynamic scoping) + localtime :1, // - convert UNIX time into record or string using local time + lock :1, // - get a thread lock on a variable, subroutine, or method + 'log' :1, // - retrieve the natural logarithm for a number + lstat :1, // - stat a symbolic link + m :null, // - match a string with a regular expression pattern + map :1, // - apply a change to a list to get back a new list with the changes + mkdir :1, // - create a directory + msgctl :1, // - SysV IPC message control operations + msgget :1, // - get SysV IPC message queue + msgrcv :1, // - receive a SysV IPC message from a message queue + msgsnd :1, // - send a SysV IPC message to a message queue + my : 2, // - declare and assign a local variable (lexical scoping) + 'new' :1, // + next :1, // - iterate a block prematurely + no :1, // - unimport some module symbols or semantics at compile time + oct :1, // - convert a string to an octal number + open :1, // - open a file, pipe, or descriptor + opendir :1, // - open a directory + ord :1, // - find a character's numeric representation + our : 2, // - declare and assign a package variable (lexical scoping) + pack :1, // - convert a list into a binary representation + 'package' :1, // - declare a separate global namespace + pipe :1, // - open a pair of connected filehandles + pop :1, // - remove the last element from an array and return it + pos :1, // - find or set the offset for the last/next m//g search + print :1, // - output a list to a filehandle + printf :1, // - output a formatted list to a filehandle + prototype :1, // - get the prototype (if any) of a subroutine + push :1, // - append one or more elements to an array + q :null, // - singly quote a string + qq :null, // - doubly quote a string + qr :null, // - Compile pattern + quotemeta :null, // - quote regular expression magic characters + qw :null, // - quote a list of words + qx :null, // - backquote quote a string + rand :1, // - retrieve the next pseudorandom number + read :1, // - fixed-length buffered input from a filehandle + readdir :1, // - get a directory from a directory handle + readline :1, // - fetch a record from a file + readlink :1, // - determine where a symbolic link is pointing + readpipe :1, // - execute a system command and collect standard output + recv :1, // - receive a message over a Socket + redo :1, // - start this loop iteration over again + ref :1, // - find out the type of thing being referenced + rename :1, // - change a filename + require :1, // - load in external functions from a library at runtime + reset :1, // - clear all variables of a given name + 'return' :1, // - get out of a function early + reverse :1, // - flip a string or a list + rewinddir :1, // - reset directory handle + rindex :1, // - right-to-left substring search + rmdir :1, // - remove a directory + s :null, // - replace a pattern with a string + say :1, // - print with newline + scalar :1, // - force a scalar context + seek :1, // - reposition file pointer for random-access I/O + seekdir :1, // - reposition directory pointer + select :1, // - reset default output or do I/O multiplexing + semctl :1, // - SysV semaphore control operations + semget :1, // - get set of SysV semaphores + semop :1, // - SysV semaphore operations + send :1, // - send a message over a socket + setgrent :1, // - prepare group file for use + sethostent :1, // - prepare hosts file for use + setnetent :1, // - prepare networks file for use + setpgrp :1, // - set the process group of a process + setpriority :1, // - set a process's nice value + setprotoent :1, // - prepare protocols file for use + setpwent :1, // - prepare passwd file for use + setservent :1, // - prepare services file for use + setsockopt :1, // - set some socket options + shift :1, // - remove the first element of an array, and return it + shmctl :1, // - SysV shared memory operations + shmget :1, // - get SysV shared memory segment identifier + shmread :1, // - read SysV shared memory + shmwrite :1, // - write SysV shared memory + shutdown :1, // - close down just half of a socket connection + 'sin' :1, // - return the sine of a number + sleep :1, // - block for some number of seconds + socket :1, // - create a socket + socketpair :1, // - create a pair of sockets + 'sort' :1, // - sort a list of values + splice :1, // - add or remove elements anywhere in an array + 'split' :1, // - split up a string using a regexp delimiter + sprintf :1, // - formatted print into a string + 'sqrt' :1, // - square root function + srand :1, // - seed the random number generator + stat :1, // - get a file's status information + state :1, // - declare and assign a state variable (persistent lexical scoping) + study :1, // - optimize input data for repeated searches + 'sub' :1, // - declare a subroutine, possibly anonymously + 'substr' :1, // - get or alter a portion of a stirng + symlink :1, // - create a symbolic link to a file + syscall :1, // - execute an arbitrary system call + sysopen :1, // - open a file, pipe, or descriptor + sysread :1, // - fixed-length unbuffered input from a filehandle + sysseek :1, // - position I/O pointer on handle used with sysread and syswrite + system :1, // - run a separate program + syswrite :1, // - fixed-length unbuffered output to a filehandle + tell :1, // - get current seekpointer on a filehandle + telldir :1, // - get current seekpointer on a directory handle + tie :1, // - bind a variable to an object class + tied :1, // - get a reference to the object underlying a tied variable + time :1, // - return number of seconds since 1970 + times :1, // - return elapsed time for self and child processes + tr :null, // - transliterate a string + truncate :1, // - shorten a file + uc :1, // - return upper-case version of a string + ucfirst :1, // - return a string with just the next letter in upper case + umask :1, // - set file creation mode mask + undef :1, // - remove a variable or function definition + unlink :1, // - remove one link to a file + unpack :1, // - convert binary structure into normal perl variables + unshift :1, // - prepend more elements to the beginning of a list + untie :1, // - break a tie binding to a variable + use :1, // - load in a module at compile time + utime :1, // - set a file's last access and modify times + values :1, // - return a list of the values in a hash + vec :1, // - test or set particular bits in a string + wait :1, // - wait for any child process to die + waitpid :1, // - wait for a particular child process to die + wantarray :1, // - get void vs scalar vs list context of current subroutine call + warn :1, // - print debugging info + when :1, // + write :1, // - print a picture record + y :null}; // - transliterate a string + + var RXstyle="string-2"; + var RXmodifiers=/[goseximacplud]/; // NOTE: "m", "s", "y" and "tr" need to correct real modifiers for each regexp type + + function tokenChain(stream,state,chain,style,tail){ // NOTE: chain.length > 2 is not working now (it's for s[...][...]geos;) + state.chain=null; // 12 3tail + state.style=null; + state.tail=null; + state.tokenize=function(stream,state){ + var e=false,c,i=0; + while(c=stream.next()){ + if(c===chain[i]&&!e){ + if(chain[++i]!==undefined){ + state.chain=chain[i]; + state.style=style; + state.tail=tail;} + else if(tail) + stream.eatWhile(tail); + state.tokenize=tokenPerl; + return style;} + e=!e&&c=="\\";} + return style;}; + return state.tokenize(stream,state);} + + function tokenSOMETHING(stream,state,string){ + state.tokenize=function(stream,state){ + if(stream.string==string) + state.tokenize=tokenPerl; + stream.skipToEnd(); + return "string";}; + return state.tokenize(stream,state);} + + function tokenPerl(stream,state){ + if(stream.eatSpace()) + return null; + if(state.chain) + return tokenChain(stream,state,state.chain,state.style,state.tail); + if(stream.match(/^\-?[\d\.]/,false)) + if(stream.match(/^(\-?(\d*\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F]+|0b[01]+|\d+(e[+-]?\d+)?)/)) + return 'number'; + if(stream.match(/^<<(?=\w)/)){ // NOTE: <"],RXstyle,RXmodifiers);} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers);}} + else if(c=="q"){ + c=stream.look(1); + if(c=="("){ + stream.eatSuffix(2); + return tokenChain(stream,state,[")"],"string");} + if(c=="["){ + stream.eatSuffix(2); + return tokenChain(stream,state,["]"],"string");} + if(c=="{"){ +stream.eatSuffix(2); + return tokenChain(stream,state,["}"],"string");} + if(c=="<"){ + stream.eatSuffix(2); + return tokenChain(stream,state,[">"],"string");} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],"string");}} + else if(c=="w"){ + c=stream.look(1); + if(c=="("){ + stream.eatSuffix(2); + return tokenChain(stream,state,[")"],"bracket");} + if(c=="["){ + stream.eatSuffix(2); + return tokenChain(stream,state,["]"],"bracket");} + if(c=="{"){ + stream.eatSuffix(2); + return tokenChain(stream,state,["}"],"bracket");} + if(c=="<"){ + stream.eatSuffix(2); + return tokenChain(stream,state,[">"],"bracket");} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],"bracket");}} + else if(c=="r"){ + c=stream.look(1); + if(c=="("){ + stream.eatSuffix(2); + return tokenChain(stream,state,[")"],RXstyle,RXmodifiers);} + if(c=="["){ + stream.eatSuffix(2); + return tokenChain(stream,state,["]"],RXstyle,RXmodifiers);} + if(c=="{"){ + stream.eatSuffix(2); + return tokenChain(stream,state,["}"],RXstyle,RXmodifiers);} + if(c=="<"){ + stream.eatSuffix(2); + return tokenChain(stream,state,[">"],RXstyle,RXmodifiers);} + if(/[\^'"!~\/]/.test(c)){ + stream.eatSuffix(1); + return tokenChain(stream,state,[stream.eat(c)],RXstyle,RXmodifiers);}} + else if(/[\^'"!~\/(\[{<]/.test(c)){ + if(c=="("){ + stream.eatSuffix(1); + return tokenChain(stream,state,[")"],"string");} + if(c=="["){ + stream.eatSuffix(1); + return tokenChain(stream,state,["]"],"string");} + if(c=="{"){ + stream.eatSuffix(1); + return tokenChain(stream,state,["}"],"string");} + if(c=="<"){ + stream.eatSuffix(1); + return tokenChain(stream,state,[">"],"string");} + if(/[\^'"!~\/]/.test(c)){ + return tokenChain(stream,state,[stream.eat(c)],"string");}}}} + if(ch=="m"){ + var c=stream.look(-2); + if(!(c&&/\w/.test(c))){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(/[\^'"!~\/]/.test(c)){ + return tokenChain(stream,state,[c],RXstyle,RXmodifiers);} + if(c=="("){ + return tokenChain(stream,state,[")"],RXstyle,RXmodifiers);} + if(c=="["){ + return tokenChain(stream,state,["]"],RXstyle,RXmodifiers);} + if(c=="{"){ + return tokenChain(stream,state,["}"],RXstyle,RXmodifiers);} + if(c=="<"){ + return tokenChain(stream,state,[">"],RXstyle,RXmodifiers);}}}} + if(ch=="s"){ + var c=/[\/>\]})\w]/.test(stream.look(-2)); + if(!c){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(c=="[") + return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); + if(c=="{") + return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); + if(c=="<") + return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); + if(c=="(") + return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); + return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}} + if(ch=="y"){ + var c=/[\/>\]})\w]/.test(stream.look(-2)); + if(!c){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(c=="[") + return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); + if(c=="{") + return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); + if(c=="<") + return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); + if(c=="(") + return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); + return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}} + if(ch=="t"){ + var c=/[\/>\]})\w]/.test(stream.look(-2)); + if(!c){ + c=stream.eat("r");if(c){ + c=stream.eat(/[(\[{<\^'"!~\/]/); + if(c){ + if(c=="[") + return tokenChain(stream,state,["]","]"],RXstyle,RXmodifiers); + if(c=="{") + return tokenChain(stream,state,["}","}"],RXstyle,RXmodifiers); + if(c=="<") + return tokenChain(stream,state,[">",">"],RXstyle,RXmodifiers); + if(c=="(") + return tokenChain(stream,state,[")",")"],RXstyle,RXmodifiers); + return tokenChain(stream,state,[c,c],RXstyle,RXmodifiers);}}}} + if(ch=="`"){ + return tokenChain(stream,state,[ch],"variable-2");} + if(ch=="/"){ + if(!/~\s*$/.test(stream.prefix())) + return "operator"; + else + return tokenChain(stream,state,[ch],RXstyle,RXmodifiers);} + if(ch=="$"){ + var p=stream.pos; + if(stream.eatWhile(/\d/)||stream.eat("{")&&stream.eatWhile(/\d/)&&stream.eat("}")) + return "variable-2"; + else + stream.pos=p;} + if(/[$@%]/.test(ch)){ + var p=stream.pos; + if(stream.eat("^")&&stream.eat(/[A-Z]/)||!/[@$%&]/.test(stream.look(-2))&&stream.eat(/[=|\\\-#?@;:&`~\^!\[\]*'"$+.,\/<>()]/)){ + var c=stream.current(); + if(PERL[c]) + return "variable-2";} + stream.pos=p;} + if(/[$@%&]/.test(ch)){ + if(stream.eatWhile(/[\w$\[\]]/)||stream.eat("{")&&stream.eatWhile(/[\w$\[\]]/)&&stream.eat("}")){ + var c=stream.current(); + if(PERL[c]) + return "variable-2"; + else + return "variable";}} + if(ch=="#"){ + if(stream.look(-2)!="$"){ + stream.skipToEnd(); + return "comment";}} + if(/[:+\-\^*$&%@=<>!?|\/~\.]/.test(ch)){ + var p=stream.pos; + stream.eatWhile(/[:+\-\^*$&%@=<>!?|\/~\.]/); + if(PERL[stream.current()]) + return "operator"; + else + stream.pos=p;} + if(ch=="_"){ + if(stream.pos==1){ + if(stream.suffix(6)=="_END__"){ + return tokenChain(stream,state,['\0'],"comment");} + else if(stream.suffix(7)=="_DATA__"){ + return tokenChain(stream,state,['\0'],"variable-2");} + else if(stream.suffix(7)=="_C__"){ + return tokenChain(stream,state,['\0'],"string");}}} + if(/\w/.test(ch)){ + var p=stream.pos; + if(stream.look(-2)=="{"&&(stream.look(0)=="}"||stream.eatWhile(/\w/)&&stream.look(0)=="}")) + return "string"; + else + stream.pos=p;} + if(/[A-Z]/.test(ch)){ + var l=stream.look(-2); + var p=stream.pos; + stream.eatWhile(/[A-Z_]/); + if(/[\da-z]/.test(stream.look(0))){ + stream.pos=p;} + else{ + var c=PERL[stream.current()]; + if(!c) + return "meta"; + if(c[1]) + c=c[0]; + if(l!=":"){ + if(c==1) + return "keyword"; + else if(c==2) + return "def"; + else if(c==3) + return "atom"; + else if(c==4) + return "operator"; + else if(c==5) + return "variable-2"; + else + return "meta";} + else + return "meta";}} + if(/[a-zA-Z_]/.test(ch)){ + var l=stream.look(-2); + stream.eatWhile(/\w/); + var c=PERL[stream.current()]; + if(!c) + return "meta"; + if(c[1]) + c=c[0]; + if(l!=":"){ + if(c==1) + return "keyword"; + else if(c==2) + return "def"; + else if(c==3) + return "atom"; + else if(c==4) + return "operator"; + else if(c==5) + return "variable-2"; + else + return "meta";} + else + return "meta";} + return null;} + + return{ + startState:function(){ + return{ + tokenize:tokenPerl, + chain:null, + style:null, + tail:null};}, + token:function(stream,state){ + return (state.tokenize||tokenPerl)(stream,state);}, + electricChars:"{}"};}); + +CodeMirror.defineMIME("text/x-perl", "perl"); + +// it's like "peek", but need for look-ahead or look-behind if index < 0 +CodeMirror.StringStream.prototype.look=function(c){ + return this.string.charAt(this.pos+(c||0));}; + +// return a part of prefix of current stream from current position +CodeMirror.StringStream.prototype.prefix=function(c){ + if(c){ + var x=this.pos-c; + return this.string.substr((x>=0?x:0),c);} + else{ + return this.string.substr(0,this.pos-1);}}; + +// return a part of suffix of current stream from current position +CodeMirror.StringStream.prototype.suffix=function(c){ + var y=this.string.length; + var x=y-this.pos+1; + return this.string.substr(this.pos,(c&&c=(y=this.string.length-1)) + this.pos=y; + else + this.pos=x;}; diff --git a/fhem/www/codemirror/mode/sql/index.html b/fhem/www/codemirror/mode/sql/index.html new file mode 100644 index 000000000..e6882d79c --- /dev/null +++ b/fhem/www/codemirror/mode/sql/index.html @@ -0,0 +1,75 @@ + + +CodeMirror: SQL Mode for CodeMirror + + + + + + + + + +
+

SQL Mode for CodeMirror

+
+ +
+

MIME types defined: + text/x-sql, + text/x-mysql, + text/x-mariadb, + text/x-cassandra, + text/x-plsql, + text/x-mssql. +

+ + +
diff --git a/fhem/www/codemirror/mode/sql/sql.js b/fhem/www/codemirror/mode/sql/sql.js new file mode 100644 index 000000000..903fa6740 --- /dev/null +++ b/fhem/www/codemirror/mode/sql/sql.js @@ -0,0 +1,366 @@ +CodeMirror.defineMode("sql", function(config, parserConfig) { + "use strict"; + + var client = parserConfig.client || {}, + atoms = parserConfig.atoms || {"false": true, "true": true, "null": true}, + builtin = parserConfig.builtin || {}, + keywords = parserConfig.keywords || {}, + operatorChars = parserConfig.operatorChars || /^[*+\-%<>!=&|~^]/, + support = parserConfig.support || {}, + hooks = parserConfig.hooks || {}, + dateSQL = parserConfig.dateSQL || {"date" : true, "time" : true, "timestamp" : true}; + + function tokenBase(stream, state) { + var ch = stream.next(); + + // call hooks from the mime type + if (hooks[ch]) { + var result = hooks[ch](stream, state); + if (result !== false) return result; + } + + if (support.hexNumber == true && + ((ch == "0" && stream.match(/^[xX][0-9a-fA-F]+/)) + || (ch == "x" || ch == "X") && stream.match(/^'[0-9a-fA-F]+'/))) { + // hex + // ref: http://dev.mysql.com/doc/refman/5.5/en/hexadecimal-literals.html + return "number"; + } else if (support.binaryNumber == true && + (((ch == "b" || ch == "B") && stream.match(/^'[01]+'/)) + || (ch == "0" && stream.match(/^b[01]+/)))) { + // bitstring + // ref: http://dev.mysql.com/doc/refman/5.5/en/bit-field-literals.html + return "number"; + } else if (ch.charCodeAt(0) > 47 && ch.charCodeAt(0) < 58) { + // numbers + // ref: http://dev.mysql.com/doc/refman/5.5/en/number-literals.html + stream.match(/^[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?/); + support.decimallessFloat == true && stream.eat('.'); + return "number"; + } else if (ch == "?" && (stream.eatSpace() || stream.eol() || stream.eat(";"))) { + // placeholders + return "variable-3"; + } else if (ch == "'" || (ch == '"' && support.doubleQuote)) { + // strings + // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html + state.tokenize = tokenLiteral(ch); + return state.tokenize(stream, state); + } else if ((((support.nCharCast == true && (ch == "n" || ch == "N")) + || (support.charsetCast == true && ch == "_" && stream.match(/[a-z][a-z0-9]*/i))) + && (stream.peek() == "'" || stream.peek() == '"'))) { + // charset casting: _utf8'str', N'str', n'str' + // ref: http://dev.mysql.com/doc/refman/5.5/en/string-literals.html + return "keyword"; + } else if (/^[\(\),\;\[\]]/.test(ch)) { + // no highlightning + return null; + } else if (support.commentSlashSlash && ch == "/" && stream.eat("/")) { + // 1-line comment + stream.skipToEnd(); + return "comment"; + } else if ((support.commentHash && ch == "#") + || (ch == "-" && stream.eat("-") && (!support.commentSpaceRequired || stream.eat(" ")))) { + // 1-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + stream.skipToEnd(); + return "comment"; + } else if (ch == "/" && stream.eat("*")) { + // multi-line comments + // ref: https://kb.askmonty.org/en/comment-syntax/ + state.tokenize = tokenComment; + return state.tokenize(stream, state); + } else if (ch == ".") { + // .1 for 0.1 + if (support.zerolessFloat == true && stream.match(/^(?:\d+(?:e[+-]?\d+)?)/i)) { + return "number"; + } + // .table_name (ODBC) + // // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html + if (support.ODBCdotTable == true && stream.match(/^[a-zA-Z_]+/)) { + return "variable-2"; + } + } else if (operatorChars.test(ch)) { + // operators + stream.eatWhile(operatorChars); + return null; + } else if (ch == '{' && + (stream.match(/^( )*(d|D|t|T|ts|TS)( )*'[^']*'( )*}/) || stream.match(/^( )*(d|D|t|T|ts|TS)( )*"[^"]*"( )*}/))) { + // dates (weird ODBC syntax) + // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html + return "number"; + } else { + stream.eatWhile(/^[_\w\d]/); + var word = stream.current().toLowerCase(); + // dates (standard SQL syntax) + // ref: http://dev.mysql.com/doc/refman/5.5/en/date-and-time-literals.html + if (dateSQL.hasOwnProperty(word) && (stream.match(/^( )+'[^']*'/) || stream.match(/^( )+"[^"]*"/))) + return "number"; + if (atoms.hasOwnProperty(word)) return "atom"; + if (builtin.hasOwnProperty(word)) return "builtin"; + if (keywords.hasOwnProperty(word)) return "keyword"; + if (client.hasOwnProperty(word)) return "string-2"; + return null; + } + } + + // 'string', with char specified in quote escaped by '\' + function tokenLiteral(quote) { + return function(stream, state) { + var escaped = false, ch; + while ((ch = stream.next()) != null) { + if (ch == quote && !escaped) { + state.tokenize = tokenBase; + break; + } + escaped = !escaped && ch == "\\"; + } + return "string"; + }; + } + function tokenComment(stream, state) { + while (true) { + if (stream.skipTo("*")) { + stream.next(); + if (stream.eat("/")) { + state.tokenize = tokenBase; + break; + } + } else { + stream.skipToEnd(); + break; + } + } + return "comment"; + } + + function pushContext(stream, state, type) { + state.context = { + prev: state.context, + indent: stream.indentation(), + col: stream.column(), + type: type + }; + } + + function popContext(state) { + state.indent = state.context.indent; + state.context = state.context.prev; + } + + return { + startState: function() { + return {tokenize: tokenBase, context: null}; + }, + + token: function(stream, state) { + if (stream.sol()) { + if (state.context && state.context.align == null) + state.context.align = false; + } + if (stream.eatSpace()) return null; + + var style = state.tokenize(stream, state); + if (style == "comment") return style; + + if (state.context && state.context.align == null) + state.context.align = true; + + var tok = stream.current(); + if (tok == "(") + pushContext(stream, state, ")"); + else if (tok == "[") + pushContext(stream, state, "]"); + else if (state.context && state.context.type == tok) + popContext(state); + return style; + }, + + indent: function(state, textAfter) { + var cx = state.context; + if (!cx) return 0; + var closing = textAfter.charAt(0) == cx.type; + if (cx.align) return cx.col + (closing ? 0 : 1); + else return cx.indent + (closing ? 0 : config.indentUnit); + }, + + blockCommentStart: "/*", + blockCommentEnd: "*/", + lineComment: support.commentSlashSlash ? "//" : support.commentHash ? "#" : null + }; +}); + +(function() { + "use strict"; + + // `identifier` + function hookIdentifier(stream) { + // MySQL/MariaDB identifiers + // ref: http://dev.mysql.com/doc/refman/5.6/en/identifier-qualifiers.html + var ch; + while ((ch = stream.next()) != null) { + if (ch == "`" && !stream.eat("`")) return "variable-2"; + } + return null; + } + + // variable token + function hookVar(stream) { + // variables + // @@prefix.varName @varName + // varName can be quoted with ` or ' or " + // ref: http://dev.mysql.com/doc/refman/5.5/en/user-variables.html + if (stream.eat("@")) { + stream.match(/^session\./); + stream.match(/^local\./); + stream.match(/^global\./); + } + + if (stream.eat("'")) { + stream.match(/^.*'/); + return "variable-2"; + } else if (stream.eat('"')) { + stream.match(/^.*"/); + return "variable-2"; + } else if (stream.eat("`")) { + stream.match(/^.*`/); + return "variable-2"; + } else if (stream.match(/^[0-9a-zA-Z$\.\_]+/)) { + return "variable-2"; + } + return null; + }; + + // short client keyword token + function hookClient(stream) { + // \N means NULL + // ref: http://dev.mysql.com/doc/refman/5.5/en/null-values.html + if (stream.eat("N")) { + return "atom"; + } + // \g, etc + // ref: http://dev.mysql.com/doc/refman/5.5/en/mysql-commands.html + return stream.match(/^[a-zA-Z.#!?]/) ? "variable-2" : null; + } + + // these keywords are used by all SQL dialects (however, a mode can still overwrite it) + var sqlKeywords = "alter and as asc between by count create delete desc distinct drop from having in insert into is join like not on or order select set table union update values where "; + + // turn a space-separated list into an array + function set(str) { + var obj = {}, words = str.split(" "); + for (var i = 0; i < words.length; ++i) obj[words[i]] = true; + return obj; + } + + // A generic SQL Mode. It's not a standard, it just try to support what is generally supported + CodeMirror.defineMIME("text/x-sql", { + name: "sql", + keywords: set(sqlKeywords + "begin"), + builtin: set("bool boolean bit blob enum long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision real date datetime year unsigned signed decimal numeric"), + atoms: set("false true null unknown"), + operatorChars: /^[*+\-%<>!=]/, + dateSQL: set("date time timestamp"), + support: set("ODBCdotTable doubleQuote binaryNumber hexNumber") + }); + + CodeMirror.defineMIME("text/x-mssql", { + name: "sql", + client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"), + keywords: set(sqlKeywords + "begin trigger proc view index for add constraint key primary foreign collate clustered nonclustered"), + builtin: set("bigint numeric bit smallint decimal smallmoney int tinyint money float real char varchar text nchar nvarchar ntext binary varbinary image cursor timestamp hierarchyid uniqueidentifier sql_variant xml table "), + atoms: set("false true null unknown"), + operatorChars: /^[*+\-%<>!=]/, + dateSQL: set("date datetimeoffset datetime2 smalldatetime datetime time"), + hooks: { + "@": hookVar + } + }); + + CodeMirror.defineMIME("text/x-mysql", { + name: "sql", + client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"), + keywords: set(sqlKeywords + "accessible action add after algorithm all analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general get global grant grants group groupby_concat handler hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show signal slave slow smallint snapshot soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"), + builtin: set("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"), + atoms: set("false true null unknown"), + operatorChars: /^[*+\-%<>!=&|^]/, + dateSQL: set("date time timestamp"), + support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"), + hooks: { + "@": hookVar, + "`": hookIdentifier, + "\\": hookClient + } + }); + + CodeMirror.defineMIME("text/x-mariadb", { + name: "sql", + client: set("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"), + keywords: set(sqlKeywords + "accessible action add after algorithm all always analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general generated get global grant grants group groupby_concat handler hard hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password persistent phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show shutdown signal slave slow smallint snapshot soft soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views virtual warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"), + builtin: set("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"), + atoms: set("false true null unknown"), + operatorChars: /^[*+\-%<>!=&|^]/, + dateSQL: set("date time timestamp"), + support: set("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"), + hooks: { + "@": hookVar, + "`": hookIdentifier, + "\\": hookClient + } + }); + + // the query language used by Apache Cassandra is called CQL, but this mime type + // is called Cassandra to avoid confusion with Contextual Query Language + CodeMirror.defineMIME("text/x-cassandra", { + name: "sql", + client: { }, + keywords: set("use select from using consistency where limit first reversed first and in insert into values using consistency ttl update set delete truncate begin batch apply create keyspace with columnfamily primary key index on drop alter type add any one quorum all local_quorum each_quorum"), + builtin: set("ascii bigint blob boolean counter decimal double float int text timestamp uuid varchar varint"), + atoms: set("false true"), + operatorChars: /^[<>=]/, + dateSQL: { }, + support: set("commentSlashSlash decimallessFloat"), + hooks: { } + }); + + // this is based on Peter Raganitsch's 'plsql' mode + CodeMirror.defineMIME("text/x-plsql", { + name: "sql", + client: set("appinfo arraysize autocommit autoprint autorecovery autotrace blockterminator break btitle cmdsep colsep compatibility compute concat copycommit copytypecheck define describe echo editfile embedded escape exec execute feedback flagger flush heading headsep instance linesize lno loboffset logsource long longchunksize markup native newpage numformat numwidth pagesize pause pno recsep recsepchar release repfooter repheader serveroutput shiftinout show showmode size spool sqlblanklines sqlcase sqlcode sqlcontinue sqlnumber sqlpluscompatibility sqlprefix sqlprompt sqlterminator suffix tab term termout time timing trimout trimspool ttitle underline verify version wrap"), + keywords: set("abort accept access add all alter and any array arraylen as asc assert assign at attributes audit authorization avg base_table begin between binary_integer body boolean by case cast char char_base check close cluster clusters colauth column comment commit compress connect connected constant constraint crash create current currval cursor data_base database date dba deallocate debugoff debugon decimal declare default definition delay delete desc digits dispose distinct do drop else elsif enable end entry escape exception exception_init exchange exclusive exists exit external fast fetch file for force form from function generic goto grant group having identified if immediate in increment index indexes indicator initial initrans insert interface intersect into is key level library like limited local lock log logging long loop master maxextents maxtrans member minextents minus mislabel mode modify multiset new next no noaudit nocompress nologging noparallel not nowait number_base object of off offline on online only open option or order out package parallel partition pctfree pctincrease pctused pls_integer positive positiven pragma primary prior private privileges procedure public raise range raw read rebuild record ref references refresh release rename replace resource restrict return returning reverse revoke rollback row rowid rowlabel rownum rows run savepoint schema segment select separate session set share snapshot some space split sql start statement storage subtype successful synonym tabauth table tables tablespace task terminate then to trigger truncate type union unique unlimited unrecoverable unusable update use using validate value values variable view views when whenever where while with work"), + builtin: set("abs acos add_months ascii asin atan atan2 average bfile bfilename bit blob ceil character chartorowid chr clob concat convert cos cosh count dec decode deref dual dump dup_val_on_index empty error exp false float floor found glb greatest hextoraw initcap instr instrb int integer isopen last_day least lenght lenghtb ln lower lpad ltrim lub make_ref max min mlslabel mod months_between natural naturaln nchar nclob new_time next_day nextval nls_charset_decl_len nls_charset_id nls_charset_name nls_initcap nls_lower nls_sort nls_upper nlssort no_data_found notfound null number numeric nvarchar2 nvl others power rawtohex real reftohex round rowcount rowidtochar rowtype rpad rtrim sign signtype sin sinh smallint soundex sqlcode sqlerrm sqrt stddev string substr substrb sum sysdate tan tanh to_char text to_date to_label to_multi_byte to_number to_single_byte translate true trunc uid upper user userenv varchar varchar2 variance varying vsize xml"), + operatorChars: /^[*+\-%<>!=~]/, + dateSQL: set("date time timestamp"), + support: set("doubleQuote nCharCast zerolessFloat binaryNumber hexNumber") + }); +}()); + +/* + How Properties of Mime Types are used by SQL Mode + ================================================= + + keywords: + A list of keywords you want to be highlighted. + functions: + A list of function names you want to be highlighted. + builtin: + A list of builtin types you want to be highlighted (if you want types to be of class "builtin" instead of "keyword"). + operatorChars: + All characters that must be handled as operators. + client: + Commands parsed and executed by the client (not the server). + support: + A list of supported syntaxes which are not common, but are supported by more than 1 DBMS. + * ODBCdotTable: .tableName + * zerolessFloat: .1 + * doubleQuote + * nCharCast: N'string' + * charsetCast: _utf8'string' + * commentHash: use # char for comments + * commentSlashSlash: use // for comments + * commentSpaceRequired: require a space after -- for comments + atoms: + Keywords that must be highlighted as atoms,. Some DBMS's support more atoms than others: + UNKNOWN, INFINITY, UNDERFLOW, NaN... + dateSQL: + Used for date/time SQL standard syntax, because not all DBMS's support same temporal types. +*/ diff --git a/fhem/www/codemirror/mode/xml/index.html b/fhem/www/codemirror/mode/xml/index.html new file mode 100644 index 000000000..873099aab --- /dev/null +++ b/fhem/www/codemirror/mode/xml/index.html @@ -0,0 +1,57 @@ + + +CodeMirror: XML mode + + + + + + + + + +
+

XML mode

+
+ +

The XML mode supports two configuration parameters:

+
+
htmlMode (boolean)
+
This switches the mode to parse HTML instead of XML. This + means attributes do not have to be quoted, and some elements + (such as br) do not require a closing tag.
+
alignCDATA (boolean)
+
Setting this to true will force the opening tag of CDATA + blocks to not be indented.
+
+ +

MIME types defined: application/xml, text/html.

+
diff --git a/fhem/www/codemirror/mode/xml/xml.js b/fhem/www/codemirror/mode/xml/xml.js new file mode 100644 index 000000000..96b51ff97 --- /dev/null +++ b/fhem/www/codemirror/mode/xml/xml.js @@ -0,0 +1,332 @@ +CodeMirror.defineMode("xml", function(config, parserConfig) { + var indentUnit = config.indentUnit; + var multilineTagIndentFactor = parserConfig.multilineTagIndentFactor || 1; + var multilineTagIndentPastTag = parserConfig.multilineTagIndentPastTag || true; + + var Kludges = parserConfig.htmlMode ? { + autoSelfClosers: {'area': true, 'base': true, 'br': true, 'col': true, 'command': true, + 'embed': true, 'frame': true, 'hr': true, 'img': true, 'input': true, + 'keygen': true, 'link': true, 'meta': true, 'param': true, 'source': true, + 'track': true, 'wbr': true}, + implicitlyClosed: {'dd': true, 'li': true, 'optgroup': true, 'option': true, 'p': true, + 'rp': true, 'rt': true, 'tbody': true, 'td': true, 'tfoot': true, + 'th': true, 'tr': true}, + contextGrabbers: { + 'dd': {'dd': true, 'dt': true}, + 'dt': {'dd': true, 'dt': true}, + 'li': {'li': true}, + 'option': {'option': true, 'optgroup': true}, + 'optgroup': {'optgroup': true}, + 'p': {'address': true, 'article': true, 'aside': true, 'blockquote': true, 'dir': true, + 'div': true, 'dl': true, 'fieldset': true, 'footer': true, 'form': true, + 'h1': true, 'h2': true, 'h3': true, 'h4': true, 'h5': true, 'h6': true, + 'header': true, 'hgroup': true, 'hr': true, 'menu': true, 'nav': true, 'ol': true, + 'p': true, 'pre': true, 'section': true, 'table': true, 'ul': true}, + 'rp': {'rp': true, 'rt': true}, + 'rt': {'rp': true, 'rt': true}, + 'tbody': {'tbody': true, 'tfoot': true}, + 'td': {'td': true, 'th': true}, + 'tfoot': {'tbody': true}, + 'th': {'td': true, 'th': true}, + 'thead': {'tbody': true, 'tfoot': true}, + 'tr': {'tr': true} + }, + doNotIndent: {"pre": true}, + allowUnquoted: true, + allowMissing: true + } : { + autoSelfClosers: {}, + implicitlyClosed: {}, + contextGrabbers: {}, + doNotIndent: {}, + allowUnquoted: false, + allowMissing: false + }; + var alignCDATA = parserConfig.alignCDATA; + + // Return variables for tokenizers + var tagName, type, setStyle; + + function inText(stream, state) { + function chain(parser) { + state.tokenize = parser; + return parser(stream, state); + } + + var ch = stream.next(); + if (ch == "<") { + if (stream.eat("!")) { + if (stream.eat("[")) { + if (stream.match("CDATA[")) return chain(inBlock("atom", "]]>")); + else return null; + } else if (stream.match("--")) { + return chain(inBlock("comment", "-->")); + } else if (stream.match("DOCTYPE", true, true)) { + stream.eatWhile(/[\w\._\-]/); + return chain(doctype(1)); + } else { + return null; + } + } else if (stream.eat("?")) { + stream.eatWhile(/[\w\._\-]/); + state.tokenize = inBlock("meta", "?>"); + return "meta"; + } else { + var isClose = stream.eat("/"); + tagName = ""; + var c; + while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/))) tagName += c; + if (!tagName) return "tag error"; + type = isClose ? "closeTag" : "openTag"; + state.tokenize = inTag; + return "tag"; + } + } else if (ch == "&") { + var ok; + if (stream.eat("#")) { + if (stream.eat("x")) { + ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";"); + } else { + ok = stream.eatWhile(/[\d]/) && stream.eat(";"); + } + } else { + ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";"); + } + return ok ? "atom" : "error"; + } else { + stream.eatWhile(/[^&<]/); + return null; + } + } + + function inTag(stream, state) { + var ch = stream.next(); + if (ch == ">" || (ch == "/" && stream.eat(">"))) { + state.tokenize = inText; + type = ch == ">" ? "endTag" : "selfcloseTag"; + return "tag"; + } else if (ch == "=") { + type = "equals"; + return null; + } else if (ch == "<") { + state.tokenize = inText; + state.state = baseState; + state.tagName = state.tagStart = null; + var next = state.tokenize(stream, state); + return next ? next + " error" : "error"; + } else if (/[\'\"]/.test(ch)) { + state.tokenize = inAttribute(ch); + state.stringStartCol = stream.column(); + return state.tokenize(stream, state); + } else { + stream.eatWhile(/[^\s\u00a0=<>\"\']/); + return "word"; + } + } + + function inAttribute(quote) { + var closure = function(stream, state) { + while (!stream.eol()) { + if (stream.next() == quote) { + state.tokenize = inTag; + break; + } + } + return "string"; + }; + closure.isInAttribute = true; + return closure; + } + + function inBlock(style, terminator) { + return function(stream, state) { + while (!stream.eol()) { + if (stream.match(terminator)) { + state.tokenize = inText; + break; + } + stream.next(); + } + return style; + }; + } + function doctype(depth) { + return function(stream, state) { + var ch; + while ((ch = stream.next()) != null) { + if (ch == "<") { + state.tokenize = doctype(depth + 1); + return state.tokenize(stream, state); + } else if (ch == ">") { + if (depth == 1) { + state.tokenize = inText; + break; + } else { + state.tokenize = doctype(depth - 1); + return state.tokenize(stream, state); + } + } + } + return "meta"; + }; + } + + function Context(state, tagName, startOfLine) { + this.prev = state.context; + this.tagName = tagName; + this.indent = state.indented; + this.startOfLine = startOfLine; + if (Kludges.doNotIndent.hasOwnProperty(tagName) || (state.context && state.context.noIndent)) + this.noIndent = true; + } + function popContext(state) { + if (state.context) state.context = state.context.prev; + } + function maybePopContext(state, nextTagName) { + var parentTagName; + while (true) { + if (!state.context) { + return; + } + parentTagName = state.context.tagName.toLowerCase(); + if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName) || + !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) { + return; + } + popContext(state); + } + } + + function baseState(type, stream, state) { + if (type == "openTag") { + state.tagName = tagName; + state.tagStart = stream.column(); + return attrState; + } else if (type == "closeTag") { + var err = false; + if (state.context) { + if (state.context.tagName != tagName) { + if (Kludges.implicitlyClosed.hasOwnProperty(state.context.tagName.toLowerCase())) + popContext(state); + err = !state.context || state.context.tagName != tagName; + } + } else { + err = true; + } + if (err) setStyle = "error"; + return err ? closeStateErr : closeState; + } else { + return baseState; + } + } + function closeState(type, _stream, state) { + if (type != "endTag") { + setStyle = "error"; + return closeState; + } + popContext(state); + return baseState; + } + function closeStateErr(type, stream, state) { + setStyle = "error"; + return closeState(type, stream, state); + } + + function attrState(type, _stream, state) { + if (type == "word") { + setStyle = "attribute"; + return attrEqState; + } else if (type == "endTag" || type == "selfcloseTag") { + var tagName = state.tagName, tagStart = state.tagStart; + state.tagName = state.tagStart = null; + if (type == "selfcloseTag" || + Kludges.autoSelfClosers.hasOwnProperty(tagName.toLowerCase())) { + maybePopContext(state, tagName.toLowerCase()); + } else { + maybePopContext(state, tagName.toLowerCase()); + state.context = new Context(state, tagName, tagStart == state.indented); + } + return baseState; + } + setStyle = "error"; + return attrState; + } + function attrEqState(type, stream, state) { + if (type == "equals") return attrValueState; + if (!Kludges.allowMissing) setStyle = "error"; + return attrState(type, stream, state); + } + function attrValueState(type, stream, state) { + if (type == "string") return attrContinuedState; + if (type == "word" && Kludges.allowUnquoted) {setStyle = "string"; return attrState;} + setStyle = "error"; + return attrState(type, stream, state); + } + function attrContinuedState(type, stream, state) { + if (type == "string") return attrContinuedState; + return attrState(type, stream, state); + } + + return { + startState: function() { + return {tokenize: inText, + state: baseState, + indented: 0, + tagName: null, tagStart: null, + context: null}; + }, + + token: function(stream, state) { + if (!state.tagName && stream.sol()) + state.indented = stream.indentation(); + + if (stream.eatSpace()) return null; + tagName = type = null; + var style = state.tokenize(stream, state); + if ((style || type) && style != "comment") { + setStyle = null; + state.state = state.state(type || style, stream, state); + if (setStyle) + style = setStyle == "error" ? style + " error" : setStyle; + } + return style; + }, + + indent: function(state, textAfter, fullLine) { + var context = state.context; + // Indent multi-line strings (e.g. css). + if (state.tokenize.isInAttribute) { + return state.stringStartCol + 1; + } + if (context && context.noIndent) return CodeMirror.Pass; + if (state.tokenize != inTag && state.tokenize != inText) + return fullLine ? fullLine.match(/^(\s*)/)[0].length : 0; + // Indent the starts of attribute names. + if (state.tagName) { + if (multilineTagIndentPastTag) + return state.tagStart + state.tagName.length + 2; + else + return state.tagStart + indentUnit * multilineTagIndentFactor; + } + if (alignCDATA && /", + + configuration: parserConfig.htmlMode ? "html" : "xml", + helperType: parserConfig.htmlMode ? "html" : "xml" + }; +}); + +CodeMirror.defineMIME("text/xml", "xml"); +CodeMirror.defineMIME("application/xml", "xml"); +if (!CodeMirror.mimeModes.hasOwnProperty("text/html")) + CodeMirror.defineMIME("text/html", {name: "xml", htmlMode: true}); diff --git a/fhem/www/codemirror/theme/blackboard.css b/fhem/www/codemirror/theme/blackboard.css new file mode 100644 index 000000000..8b7608472 --- /dev/null +++ b/fhem/www/codemirror/theme/blackboard.css @@ -0,0 +1,28 @@ +/* Port of TextMate's Blackboard theme */ + +.cm-s-blackboard.CodeMirror { background: #0C1021; color: #F8F8F8; } +.cm-s-blackboard .CodeMirror-selected { background: #253B76 !important; } +.cm-s-blackboard .CodeMirror-gutters { background: #0C1021; border-right: 0; } +.cm-s-blackboard .CodeMirror-linenumber { color: #888; } +.cm-s-blackboard .CodeMirror-cursor { border-left: 1px solid #A7A7A7 !important; } + +.cm-s-blackboard .cm-keyword { color: #FBDE2D; } +.cm-s-blackboard .cm-atom { color: #D8FA3C; } +.cm-s-blackboard .cm-number { color: #D8FA3C; } +.cm-s-blackboard .cm-def { color: #8DA6CE; } +.cm-s-blackboard .cm-variable { color: #FF6400; } +.cm-s-blackboard .cm-operator { color: #FBDE2D;} +.cm-s-blackboard .cm-comment { color: #AEAEAE; } +.cm-s-blackboard .cm-string { color: #61CE3C; } +.cm-s-blackboard .cm-string-2 { color: #61CE3C; } +.cm-s-blackboard .cm-meta { color: #D8FA3C; } +.cm-s-blackboard .cm-builtin { color: #8DA6CE; } +.cm-s-blackboard .cm-tag { color: #8DA6CE; } +.cm-s-blackboard .cm-attribute { color: #8DA6CE; } +.cm-s-blackboard .cm-header { color: #FF6400; } +.cm-s-blackboard .cm-hr { color: #AEAEAE; } +.cm-s-blackboard .cm-link { color: #8DA6CE; } +.cm-s-blackboard .cm-error { background: #9D1E15; color: #F8F8F8; } + +.cm-s-blackboard .CodeMirror-activeline-background {background: #3C3636 !important;} +.cm-s-blackboard .CodeMirror-matchingbracket {outline:1px solid grey;color:white !important} \ No newline at end of file diff --git a/fhem/www/codemirror/theme/cobalt.css b/fhem/www/codemirror/theme/cobalt.css new file mode 100644 index 000000000..b4a917736 --- /dev/null +++ b/fhem/www/codemirror/theme/cobalt.css @@ -0,0 +1,21 @@ +.cm-s-cobalt.CodeMirror { background: #002240; color: white; } +.cm-s-cobalt div.CodeMirror-selected { background: #b36539 !important; } +.cm-s-cobalt .CodeMirror-gutters { background: #002240; border-right: 1px solid #aaa; } +.cm-s-cobalt .CodeMirror-linenumber { color: #d0d0d0; } +.cm-s-cobalt .CodeMirror-cursor { border-left: 1px solid white !important; } + +.cm-s-cobalt span.cm-comment { color: #08f; } +.cm-s-cobalt span.cm-atom { color: #845dc4; } +.cm-s-cobalt span.cm-number, .cm-s-cobalt span.cm-attribute { color: #ff80e1; } +.cm-s-cobalt span.cm-keyword { color: #ffee80; } +.cm-s-cobalt span.cm-string { color: #3ad900; } +.cm-s-cobalt span.cm-meta { color: #ff9d00; } +.cm-s-cobalt span.cm-variable-2, .cm-s-cobalt span.cm-tag { color: #9effff; } +.cm-s-cobalt span.cm-variable-3, .cm-s-cobalt span.cm-def { color: white; } +.cm-s-cobalt span.cm-bracket { color: #d8d8d8; } +.cm-s-cobalt span.cm-builtin, .cm-s-cobalt span.cm-special { color: #ff9e59; } +.cm-s-cobalt span.cm-link { color: #845dc4; } +.cm-s-cobalt span.cm-error { color: #9d1e15; } + +.cm-s-cobalt .CodeMirror-activeline-background {background: #002D57 !important;} +.cm-s-cobalt .CodeMirror-matchingbracket {outline:1px solid grey;color:white !important} diff --git a/fhem/www/codemirror/theme/eclipse.css b/fhem/www/codemirror/theme/eclipse.css new file mode 100644 index 000000000..317218e3d --- /dev/null +++ b/fhem/www/codemirror/theme/eclipse.css @@ -0,0 +1,23 @@ +.cm-s-eclipse span.cm-meta {color: #FF1717;} +.cm-s-eclipse span.cm-keyword { line-height: 1em; font-weight: bold; color: #7F0055; } +.cm-s-eclipse span.cm-atom {color: #219;} +.cm-s-eclipse span.cm-number {color: #164;} +.cm-s-eclipse span.cm-def {color: #00f;} +.cm-s-eclipse span.cm-variable {color: black;} +.cm-s-eclipse span.cm-variable-2 {color: #0000C0;} +.cm-s-eclipse span.cm-variable-3 {color: #0000C0;} +.cm-s-eclipse span.cm-property {color: black;} +.cm-s-eclipse span.cm-operator {color: black;} +.cm-s-eclipse span.cm-comment {color: #3F7F5F;} +.cm-s-eclipse span.cm-string {color: #2A00FF;} +.cm-s-eclipse span.cm-string-2 {color: #f50;} +.cm-s-eclipse span.cm-qualifier {color: #555;} +.cm-s-eclipse span.cm-builtin {color: #30a;} +.cm-s-eclipse span.cm-bracket {color: #cc7;} +.cm-s-eclipse span.cm-tag {color: #170;} +.cm-s-eclipse span.cm-attribute {color: #00c;} +.cm-s-eclipse span.cm-link {color: #219;} +.cm-s-eclipse span.cm-error {color: #f00;} + +.cm-s-eclipse .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-eclipse .CodeMirror-matchingbracket {outline:1px solid grey; color:black !important;} diff --git a/fhem/www/codemirror/theme/elegant.css b/fhem/www/codemirror/theme/elegant.css new file mode 100644 index 000000000..dd7df7b73 --- /dev/null +++ b/fhem/www/codemirror/theme/elegant.css @@ -0,0 +1,13 @@ +.cm-s-elegant span.cm-number, .cm-s-elegant span.cm-string, .cm-s-elegant span.cm-atom {color: #762;} +.cm-s-elegant span.cm-comment {color: #262; font-style: italic; line-height: 1em;} +.cm-s-elegant span.cm-meta {color: #555; font-style: italic; line-height: 1em;} +.cm-s-elegant span.cm-variable {color: black;} +.cm-s-elegant span.cm-variable-2 {color: #b11;} +.cm-s-elegant span.cm-qualifier {color: #555;} +.cm-s-elegant span.cm-keyword {color: #730;} +.cm-s-elegant span.cm-builtin {color: #30a;} +.cm-s-elegant span.cm-link {color: #762;} +.cm-s-elegant span.cm-error {background-color: #fdd;} + +.cm-s-elegant .CodeMirror-activeline-background {background: #e8f2ff !important;} +.cm-s-elegant .CodeMirror-matchingbracket {outline:1px solid grey; color:black !important;}