#use wml::debian::ctime #use wml::debian::common_tags #use wml::debian::legal_tags #use wml::debian::openrecode No items for this year. proposed in discussion voting open finished withdrawn Future events Past events (new revision) # get_top_news() # display the top news selected by homepagenews.data # each line in homepagenews.data contains one file filename (without .wml) of some news # we use the frontpage format for a nice layout sub get_top_news() { open DATAFILE, "$(ENGLISHDIR)/homepagenews.data" or warn "couldn't open $(ENGLISHDIR)/$homepagenews.data: $!\n"; # read list of homepagenews foreach my $id () { chomp $id; next unless $id =~ /^\d+$/; print get_recent_list('News/2y', '1', '$(ENGLISHDIR)', 'bydate frontpage', $id); } close DATAFILE; } # get_recent_list( $year, $number, $eng_dir, $format, $match) # # get_recent_list grabs the date and title of the last $number special # files from directory $year. # The special files can be news, events, security # advisories and vote summaries. # # The format parameter specifies the type of output: numbered, definition # and bulleted list, a table, or a RDF file (rdf, rdflong, or rfsseq). # RDF output is currently only supported for DSAs. # # The match parameter is a regexp used to determine if the file is special, # i.e. that it should be parsed. # # When you specify 'frontpage' in the format string (you can mix it with # all the other strings allowed there), the entries will only use a # different output format. # # When you specify 'bydate' in the format string (you can mix it with # all the other strings allowed there), the behavior is slightly # changed: You can specify instead of a year a time, since when # items should be shown. The format of this time specification # is \d+(d|w|m|y) for \d+ days/weeks/months/years. $number is then # only the minimum number of shown items. # This also enables parsing the report_date tag of security advisories # as a comma delimited list, so one advisory can have different # dates for its different revisions. # # The interface of this function has several flaws. Some tips: # As first parameter you must specify the relative path from the site # on which the list should occour to the directory, that contains # the $year directories. After the last slash can either follow a year # or a time specification for bydate (see above). # As third parameter you must specify the path to the english directory # of the file you are in. # # example usage: # 1) /index.wml # get_recent_list( 'security/1m', 10, '$(ENGLISHDIR)', 'bydate', '(2000\d+\w+|dsa-\d+)' ) # 2) News/index.wml # get_recent_list ('$(CUR_YEAR)', '0', '$(ENGLISHDIR)/News', '', '\d+\w*') # 3) News/2002/index.wml # get_recent_list ('.', '0', '$(ENGLISHDIR)/News/2002', '', '\d+\w*') use Encode; my ($listhead, $listfoot, $elemhead, $elemdate, $elemfoot, $elememptyfoot, $elemrealfoot); my $is_events = 0; my $is_by_date = 0; sub get_recent_list { #clear up $is_events = 0; $is_by_date = 0; my ($time, $minnum, $eng_dir, $format, $match) = @_; # djpig: this is a hack for backward compatibility # djpig: I would prefer a new argument $rel_path # djpig: additionally to $eng_dir my $rel_path = ($time =~ s|^(?:\./)?([[:alpha:]]+)/?||) ? $1 : ''; $rel_path .= '/' unless $rel_path eq ''; #warn "$rel_path\n"; my $open_by_path = 0; my ($year, $since_year, $since_date); # set open_by_path, if no year is specified at all unless (($time eq '.') || (($time =~ /\d{4}/) && !$minnum)) { $year = $time; $since_year = $year; $since_date = timegm(0, 0, 0, 1, 0, $year) if $year =~ /\d+/; if ($format =~ /bydate/) { $year = $(CUR_YEAR) unless $year =~ /\d{4}/; $since_date = parse_time($time); $since_year = (gmtime($since_date))[5] + 1900; if ($since_year > $year) { warn "since_year > year ($since_year > $year)\n"; } $is_by_date = 1; } warn "don't know, what to do: year=$year time=$time\n" unless $since_date =~ /\d+/; } else { $open_by_path = 1; } #warn scalar gmtime($since_date) . " - " . scalar gmtime() . "\n"; if ($format =~ order) { $listhead = '
    '; $listfoot = '
'; $elemhead = '
  • '; $elemfoot = '- '; $elememptyfoot = '
  • '; $elemrealfoot = ''; } elsif ($format =~ bullet) { $listhead = '
      '; $listfoot = '
    '; $elemhead = '
  • '; $elemfoot = '- '; $elememptyfoot = '
  • '; $elemrealfoot = ''; } elsif ($format =~ list) { $listhead = '
    '; $listhead = '
    ' if $format eq "cdeflist"; $listfoot = '
    '; $elemhead = '
    '; $elemfoot = '
    '; $elememptyfoot = ''; $elemrealfoot = '
    '; } elsif ($format =~ table) { $listhead = ''; $listfoot = '
    '; $elemhead = ''; $elemdate = ''; $elemfoot = ''; $elememptyfoot = ' '; $elemrealfoot = ''; } elsif ($format =~ /frontpage/) { $listhead = ''; $listfoot = ''; $elemhead = '
    '; $elemfoot = '
    '; $elememptyfoot = ''; $elemrealfoot = ''; } else { $listhead = ''; $listfoot = ''; $elemhead = ''; $elemfoot = '- '; $elememptyfoot = ''; $elemrealfoot = ''; } my @files; # djpig: take $since_year-1, perhaps better define an $oldest_year? # djpig: but there should be no more updates to an item after a year # djpig: we're saving time so. my @years; if ( $open_by_path ) { push @years, $time; } else { foreach (($since_year-1) .. $year) { push @years, $_; } } for my $act_year (@years) { opendir DIR, "$eng_dir/$rel_path$act_year" or do { # we could not warn here regularly # f.e. the actual year can be to new #warn "couldn't open dir $eng_dir/$rel_path/$act_year: $!\n"; next; }; my @new_files = grep { /^$match.wml$/ && -f "$eng_dir/$rel_path$act_year/$_" && ($_="$rel_path$act_year/$_") } readdir(DIR); closedir DIR; @files = ( @files, @new_files ); } @files = sort { ($a =~ m,(?:^|/)dsa-(\d+),)[0] <=> ($b =~ m,(?:^|/)dsa-(\d+),)[0] || $a cmp $b } @files; #warn "files: " . join " ", @files; warn "\n"; #warn "years: " . join " ", @years; warn "\n"; #warn "time=$time minnum=$minnum"; #warn "eng_dir=$eng_dir rel_path=$rel_path format=$format match=$match\n"; #warn "is_by_date=$is_by_date is_events=$is_events\n"; #warn "open_by_path=$open_by_path\n"; my $str = grab_titles( $since_date, $minnum, $eng_dir, $format, $match , @files); if ( !$is_events ) { # events already have their head and foot $str = "$listhead$str$listfoot"; } #warn "$str\n"; return $str; } # grab_titles is an auxiliary function to get_recent_list, which actually # does the real work. sub grab_titles { my ($since_date, $minnum, $eng_dir, $format, $match, @files) = @_; my ($over, $current, $str); my (%str, %count, %over, %current); my $count = 0; foreach $file (@files) { (my $trans_title = $file) =~ s/wml/title/; $count++; my $str1 = ""; my @str1 = (); # for bydate my $base = $1 if ($file =~ m|^([[:alpha:]./]*(\d{4})?/(?:$match)).wml|); #warn "$file -> $base\n"; # read file in my $fh = openrecode($file, $trans_title, "$eng_dir/$file") or die "couldn't open $eng_dir/$file: $!\n"; my $content; { local $/; $content = Encode::decode_utf8(<$fh>); } close $fh; my ($title, $date, $rdate, $rvdate, $hdate, $desc, $status, $where, $moreinfo, $startdate, $enddate, $repfile, $just, $release, $codename, $revision); my (@hdate, @rdate, @isodate) = (); if ($content =~ /^\s*(.*?)\s*<\/define-tag>/ms) { $title = Encode::encode_utf8(qq/$1/); } # all if ($content =~ /^(.*?)<\/define-tag>/ms) { $date = Encode::encode_utf8(qq/$1/); } # News, news.XX.rdf if ($content =~ /^

    (.*?)<\/p>/ms) { $moreinfo = Encode::encode_utf8(qq/$1/); } # news.XX.rdf Should catch the first paragraph if ($content =~ /^(.*?)<\/define-tag>/ms) { $where = Encode::encode_utf8(qq/$1/); } # events if ($content =~ /^(.*?)<\/define-tag>/ms) { $date = Encode::encode_utf8(qq/$1/); } # events if ($content =~ /^(.*?)<\/define-tag>/ms) { $desc = Encode::encode_utf8(qq/$1/); } # security if ($content =~ /^\s*(.*?)\s*<\/define-tag>/ms) { $just = Encode::encode_utf8(qq/$1/); } # licenses if ($content =~ /^(?:(.*?<\/p>)|(.*?)<\/define-tag>)/ms) { $moreinfo = Encode::encode_utf8(qq/$1/); } # dsa-long.XX.rdf if ($content =~ /^(.*?)<\/define-tag>/ms) { $status = Encode::encode_utf8(qq/$1/); } # vote if ($content =~ /^(.*?)<\/define-tag>/ms) { $startdate = Encode::encode_utf8(qq/$1/); } # startdate (events) if ($content =~ /^(.*?)<\/define-tag>/ms) { $enddate = Encode::encode_utf8($1); } # enddate (events) my ($shortfile) = $file =~ /^(?:.*\/)?(.*)\.wml$/; if ($content =~ //ms) { $repfile = Encode::encode_utf8($1); } # Report (events) if ($content =~ //ms) { #" $repfile = Encode::encode_utf8($1); } # Report (events) if ($content =~ /^(.*?)<\/define-tag>/ms) { $release = Encode::encode_utf8(qq/$1/); } # News if ($content =~ /^(.*?)<\/define-tag>/ms) { $codename = Encode::encode_utf8(qq/$1/); } # News if ($content =~ /^(.*?)<\/define-tag>/ms) { $revision = Encode::encode_utf8(qq/$1/); } # News $hdate = $date || $startdate; #warn "file=$file hdate=$hdate date=$date rdate=$rdate\n"; if ($startdate && $enddate) { # Convert date range into printable string $date = &daterange($startdate, $enddate); } if ($title && $date && $where) { # for events/ if ($date !~ /^[\d-\s]+$/ ) { # old events dates $hdate = ""; } my $report = ($repfile) ? " [Report]" : ''; $str1 = "$elemhead[$date]$elemdate $title, $elemfoot$where$report
    $elemrealfoot\n"; } elsif ($desc) { # for security/ # read in datafile open DATAFILE, "$eng_dir/$base.data" or do { warn "couldn't open $eng_dir/$base.data: $!\n"; next; }; foreach my $l () { if ($l =~ /^(.*)<\/define-tag>/) { $title = qq/$1/; } elsif ($l =~ /^(.*)<\/define-tag>/) { $rdate = qq/$1/; } } close DATAFILE; @hdate = split ',', $rdate; $hdate = $hdate[0]; $isodate = sprintf("%04d-%02d-%02d", split '-', $hdate[-1]); $rdate = newsdate($hdate); @rdate = map( newsdate($_), @hdate ); @isodate = map( sprintf("%04d-%02d-%02d", split '-', $_), @hdate ); $title =~ s/(D[SL]A-\d{3,})-\d{1}/$1/; # strip off the revision in the DSA/DLA number # we need absolute paths for rdf (my $rdfbase = $base) =~ s/^security\///; my $prefix = '/lts' if $eng_dir =~ /\/lts\//; if ($format =~ /\brdf\b/) { foreach (@isodate) { push @str1, "\n" ."\n" ." $title\n" ." https://www.debian.org" ."$prefix/security/$rdfbase\n" ." \n" ." $desc\n" ." \n" ." $_\n" ."\n"; } $str1 = $str1[0]; } elsif ($format =~ /\brdflong\b/) { # $moreinfo is WML/HTML; we need to strip tags here # HTML entities $moreinfo =~ s/(&[^#;]+;)/&decodehtmlentity($1)/ge; # $moreinfo =~ s#"]+)"?>#$1#g; $moreinfo =~ s#]+)">#$1#g; # HTML tags $moreinfo =~ s//>/g; $moreinfo =~ s/"/"/g; #" # WML continuation $moreinfo =~ s/\\\n//g; foreach (@isodate) { push @str1, "\n" ."\n" ." $title - $desc\n" ." https://www.debian.org" ."$prefix/security/$rdfbase\n" ." \n" ." $moreinfo\n" ." \n" ." $_\n" ."\n"; } $str1 = @str1[0]; } elsif ($format =~ /\brdfseq/){ foreach (@isodate) { push @str1, "\n"; } $str1 = $str1[0]; } else { foreach( @rdate ) { push @str1, "$elemhead[$_] " ."$title " ."$elemfoot$desc NEW_REVISION
    $elemrealfoot\n"; } $str1 = $str1[0]; } } elsif ($just) { # for licenses/ # read in datafile open DATAFILE, "$eng_dir/$base.data" or do { warn "couldn't open $eng_dir/$base.data: $!\n"; next; }; my ( $free, $l_name ); foreach my $l () { if ($l =~ /^(.*)<\/define-tag>/) { $title = qq/$1/; } if ($l =~ /^(.*)<\/define-tag>/) { $l_name = qq/$1/; } elsif ($l =~ /^(.*)<\/define-tag>/) { $rdate = qq/$1/; } elsif ($l =~ /^(.*)<\/define-tag>/) { $free = qq/$1/; } } close DATAFILE; $rdate = newsdate($rdate); if ($free eq 'yes') { $free = ''; } elsif ($free eq 'no') { $free = ''; } elsif ($free eq 'non-dist') { $free = ''; } $str1 = "$elemhead[$rdate] " ."$title$elemfoot " ."$l_name – $free
    $elemrealfoot\n"; } elsif ($title && $date && !$is_events) { # for News/ and not events/ # read in datafile open DATAFILE, "$eng_dir/$base.wml" or do { warn "couldn't open $eng_dir/$base.wml: $!\n"; next; }; foreach my $l () { if ($l =~ /^(.*)<\/define-tag>/) { $rdate = qq/$1/; } } close DATAFILE; @hdate = split ',', $rdate; $hdate = $hdate[0]; $isodate = sprintf("%04d-%02d-%02d", split '-', $hdate[-1]); $rdate = newsdate($hdate); @rdate = map( newsdate($_), @hdate ); @isodate = map( sprintf("%04d-%02d-%02d", split '-', $_), @hdate ); # we need absolute paths for rdf (my $rdfbase = $base) =~ s/^News\///; if ($format =~ /\brdflong\b/) { # $moreinfo is WML/HTML; we need to strip tags here # HTML entities $moreinfo =~ s/(&[^#;]+;)/&decodehtmlentity($1)/ge; # $moreinfo =~ s#"]+)"?>#$1#g; $moreinfo =~ s#]+)">#$1#g; $moreinfo =~ s#\$\(HOME\)#https://www.debian.org#g; $moreinfo =~ s##$release#g; $moreinfo =~ s##$codename#g; $moreinfo =~ s##$revision#g; # HTML tags $moreinfo =~ s//>/g; $moreinfo =~ s/"/"/g; #" $title =~ s/<\/?q>/"/g; $title =~ s/<\/?sup>//g; $title =~ s//>/g; $title =~ s/"/"/g; #" # WML continuation $moreinfo =~ s/\\\n//g; foreach (@isodate) { push @str1, "\n" ."\n" ." $title\n" ." https://www.debian.org" ."/News/$rdfbase\n" ." \n" ." $moreinfo\n" ." \n" ." $_\n" ."\n"; } $str1 = @str1[0]; } elsif ($format =~ /\brdfseq/){ foreach (@isodate) { push @str1, "\n"; } $str1 = $str1[0]; $str1 = @str1[0]; } elsif ($format =~ /\bfrontpage\b/ ) { my ($cyear, $cmon, $cmday) = split /-/, $date; my $mon_str = $longmoy[$cmon - 1]; $datetime =$date; $date = newsdate($date); $str1 = "$elemhead" ."

    " ."$title" ."
    " ."\n"; } else { $date = newsdate($date); $str1 = "$elemhead[$date] " ."$title
    " ."$elemrealfoot\n"; } } elsif ($title && $status) { # for vote/ $str1 = "$elemhead$title — "; if ( $status eq "P" ) { $str1 .= ""; } elsif ( $status eq "D" ) { $str1 .= ""; } elsif ( $status eq "V" ) { $str1 .= ""; } elsif ( $status eq "F" ) { $str1 .= ""; } elsif ( $status eq "W" ) { $str1 .= ""; } else { $str1 .= "$status"; } $str1 .= "
    $elememptyfoot\n"; } if (!$is_by_date || ($hdate eq "")) { $hdate = $count; } else { $hdate = iso2stamp( $hdate ); @hdate = map(iso2stamp($_), @hdate); } #warn "$file: hdate=$hdate (".join( ":", @hdate ).")\n"; if ($event ne "") { # this file was for an event if ( $event eq "past" ) { $over{$hdate} .= $str1; $count{$hdate}++; } elsif ( $event eq "current" ) { $current{$hdate} .= $str1; $count{$hdate}++; } } else { #warn "$file: no event?" if $is_events; unless ( ($#hdate > 0) && $is_by_date ) { ($str{$hdate} = $str1 . $str{$hdate}) =~ s/NEW_REVISION//; $count{$hdate}++; } else { if ( $#hdate != $#str1 ) { warn '$#hdate != $#str1\n'; } ($str{$hdate[0]} = $str1[0] . $str{$hdate[0]}) =~ s/NEW_REVISION//; for (1..$#str1) { ($str{$hdate[$_]} = $str1[$_] . $str{$hdate[$_]}) =~ s,NEW_REVISION,,; $count{$hdate[$_]}++; } } } } # for each file if ($is_events) { $str = ""; $over = pick_recent( $since_date, $minnum, \%over, \%count); $current = pick_recent( $since_date, $minnum, \%current, \%count, "reverse:quietnull" ); if ($current) { $str = "

    \n$listhead\n$current\n$listfoot\n"; } if ($over) { $str .= "

    \n$listhead\n$over\n$listfoot\n"; } } else { $str = pick_recent( $since_date, $minnum, \%str, \%count); } return $str; } # pick_recent picks the item from an hash and concatenates them to a string. # Input: $since_date - integer timestamp # $minnum - at least this number of items are picked # (if the hash is big enough) # %str - hash with timestamps as keys and strings as values # Output: string with the concatenated values of the picked items # # pick_recent sorts the keys of the array newest to oldest and then picks # the first $minnum one and then until a key is lower then $since_date. sub pick_recent { my ($since_date, $minnum, $str, $str_count, $sort) = @_; my $out_str = ''; my $count = 0; my @keys; if (defined($sort) && ($sort =~ /reverse/)) { @keys = sort { $a <=> $b } keys %$str; } else { @keys = sort { $b <=> $a } keys %$str; } #warn "since_date: ".scalar gmtime($since_date)." minnum: $minnum\n"; foreach (@keys) { #warn "date: ".scalar gmtime($_)." ($count >= $minnum) && ($_ < $since_date)\n"; if ($count >= $minnum) { if( (!$is_by_date && $minnum) || (($is_by_date || !$minnum) && ($_ < $since_date)) ) { last; } } $out_str .= ${$str}{$_}; $count += ${$str_count}{$_}; #warn "infinite loop?\n" unless $count % 1000; } if (($count eq 0) && ($sort !~ /quietnull/)) { $out_str = "\n"; $out_str = $elemhead . $out_str . $elemrealfoot if $is_events; } #warn "count: $count\n"; #warn "$out_str\n"; return $out_str; } # parse_time gets as argument a string and returns a unix timestamp # Input: $time_str - String with the following format # $time_str ::= (d|w|m|y) # Output: integer timestamp # # parse_time subtracts days/weeks/months from the actual time and # returns the corresponding timestamp. Years are handled special: 1y means # "since January, 1st of actual year", 2y means "since January, 1st of # last year", etc. sub parse_time { my $time_str = shift; my $year = (gmtime())[5] + 1900; my $time = time(); my $res; for ($time_str) { /\d{4}/ && do { $res = timegm(0,0,0,1,0,$year); last; }; /(\d+)d/ && do { $res = $time - 86400 * $1; last; }; /(\d+)w/ && do { $res = $time - 86400 * 7 * $1; last; }; /(\d+)m/ && do { # All months have 30 days, # all other would be far more complicated $res = $time - 86400 * 30 * $1; last; }; /(\d+)y/ && do { # years are handled special my $ryear = $year - $1 + 1; # the actual year count as a whole one $res = timegm(0,0,0,1,0,$ryear); # 01.01.$ryear 00:00:00 last; }; } return $res; } # iso2stamp converts a date in ISO format (YYYY-MM-DD) to an # unix timestamp for 23:59:59 on the specified day # Input: $time - String with the ISO date # Output: integer timestamp sub iso2stamp { my $time = shift; if ($time =~ /undated/) { return 0; } my ($year, $month, $day) = ($time =~ /(\d{4})-(\d{1,2})-(\d{1,2})/); unless ($year && $month && $day) { warn "not an ISO date: $time\n"; } return timegm( 59, 59, 23, $day, $month-1, $year); } # decode_html_entity is used in the RDF outputs to convert the predefined # HTML/SGML entities to NCRs, as they are not predefined for XML formats. sub decodehtmlentity { my $ent = shift; # ISO 8859-1 entities @entities = ( ' ', '¡', '¢', '£', '¤', '¥', '¦', '§', '¨', '©', 'ª', '«', '¬', '­', '®', '¯', '°', '±', '²', '³', '´', 'µ', '¶', '·', '¸', '¹', 'º', '»', '¼', '½', '¾', '¿', 'À', 'Á', 'Â', 'Ã', 'Ä', 'Å', 'Æ', 'Ç', 'È', 'É', 'Ê', 'Ë', 'Ì', 'Í', 'Î', 'Ï', 'Ð', 'Ñ', 'Ò', 'Ó', 'Ô', 'Õ', 'Ö', '×', 'Ø', 'Ù', 'Ú', 'Û', 'Ü', 'Ý', 'Þ', 'ß', 'à', 'á', 'â', 'ã', 'ä', 'å', 'æ', 'ç', 'è', 'é', 'ê', 'ë', 'ì', 'í', 'î', 'ï', 'ð', 'ñ', 'ò', 'ó', 'ô', 'õ', 'ö', '÷', 'ø', 'ù', 'ú', 'û', 'ü', 'ý', 'þ', 'ÿ' ); for (my $i = 0; $i < $#entities; ++ $i) { return "&#".($i+160).";" if $entities[$i] eq $ent; } # Non-ISO 8859-1 entities %entities = ( # Specials 'Œ' => 'Œ', 'œ' => 'œ', 'Š' => 'Š', 'š' => 'š', 'Ÿ' => 'Ÿ', 'ˆ' => 'ˆ', '˜' => '˜', ' ' => ' ', ' ' => ' ', ' ' => ' ', '‌' => '‌', '‍' => '‍', '‎' => '‎', '‏' => '‏', '–' => '–', '—' => '—', '‘' => '‘', '’' => '’', '‚' => '‚', '“' => '“', '”' => '”', '„' => '„', '†' => '†', '‡' => '‡', '‰' => '‰', '‹' => '‹', '›' => '›', '€' => '€', # Symbols 'ƒ' => 'ƒ', 'Α' => 'Α', 'Β' => 'Β', 'Γ' => 'Γ', 'Δ' => 'Δ', 'Ε' => 'Ε', 'Ζ' => 'Ζ', 'Η' => 'Η', 'Θ' => 'Θ', 'Ι' => 'Ι', 'Κ' => 'Κ', 'Λ' => 'Λ', 'Μ' => 'Μ', 'Ν' => 'Ν', 'Ξ' => 'Ξ', 'Ο' => 'Ο', 'Π' => 'Π', 'Ρ' => 'Ρ', 'Σ' => 'Σ', 'Τ' => 'Τ', 'Υ' => 'Υ', 'Φ' => 'Φ', 'Χ' => 'Χ', 'Ψ' => 'Ψ', 'Ω' => 'Ω', 'α' => 'α', 'β' => 'β', 'γ' => 'γ', 'δ' => 'δ', 'ε' => 'ε', 'ζ' => 'ζ', 'η' => 'η', 'θ' => 'θ', 'ι' => 'ι', 'κ' => 'κ', 'λ' => 'λ', 'μ' => 'μ', 'ν' => 'ν', 'ξ' => 'ξ', 'ο' => 'ο', 'π' => 'π', 'ρ' => 'ρ', 'ς' => 'ς', 'σ' => 'σ', 'τ' => 'τ', 'υ' => 'υ', 'φ' => 'φ', 'χ' => 'χ', 'ψ' => 'ψ', 'ω' => 'ω', 'ϑ' => 'ϑ', 'ϒ' => 'ϒ', 'ϖ' => 'ϖ', '•' => '•', '…' => '…', '′' => '′', '″' => '″', '‾' => '‾', '⁄' => '⁄', '℘' => '℘', 'ℑ' => 'ℑ', 'ℜ' => 'ℜ', '™' => '™', 'ℵ' => 'ℵ', '←' => '←', '↑' => '↑', '→' => '→', '↓' => '↓', '↔' => '↔', '↵' => '↵', '⇐' => '⇐', '⇑' => '⇑', '⇒' => '⇒', '⇓' => '⇓', '⇔' => '⇔', '∀' => '∀', '∂' => '∂', '∃' => '∃', '∅' => '∅', '∇' => '∇', '∈' => '∈', '∉' => '∉', '∋' => '∋', '∏' => '∏', '∑' => '∑', '−' => '−', '∗' => '∗', '√' => '√', '∝' => '∝', '∞' => '∞', '∠' => '∠', '∧' => '∧', '∨' => '∨', '∩' => '∩', '∪' => '∪', '∫' => '∫', '∴' => '∴', '∼' => '∼', '≅' => '≅', '≈' => '≈', '≠' => '≠', '≡' => '≡', '≤' => '≤', '≥' => '≥', '⊂' => '⊂', '⊃' => '⊃', '⊄' => '⊄', '⊆' => '⊆', '⊇' => '⊇', '⊕' => '⊕', '⊗' => '⊗', '⊥' => '⊥', '⋅' => '⋅', '⌈' => '⌈', '⌉' => '⌉', '⌊' => '⌊', '⌋' => '⌋', '⟨' => '〈', '⟩' => '〉', '◊' => '◊', '♠' => '♠', '♣' => '♣', '♥' => '♥', '♦' => '♦', ); return $entities{$ent} if defined $entities{$ent}; return '?'; # Say what? } # # vim:ts=8:sw=4: #