Collection of themes/skins for the Fossil SCM

โŒˆโŒ‹ โŽ‡ branch:  Fossil Skins Extra


Artifact [5debf965c0]

Artifact 5debf965c095001629ffb9c1365d6c4dc6221e4c:

  • File parts/github/github.th1-setup — part of check-in [d933f24359] at 2021-04-04 22:51:56 on branch trunk — Enable upvar 1 for fx_stats query (user: mario size: 12801)


#-- Pre-increment  [++ varname]
proc ++ {varname} {
   upvar 1 $varname i
   return [uplevel 1 "set {$varname} [expr 1+$i]"]
}

#-- ternary / if-shorthand (cond/then/else may be literals, or {[expressions]} themselves)
proc ?: {cond then else} {
   uplevel 1 "if {$cond} { return $then; } else { return $else; }"
}

#-- info exists shorthand
proc isset {varname} {
   return [uplevel 1 "info exists {$varname}"]
}

#-- string equality shorthand
proc eq {str1 str2} {
   return [expr {$str1 eq $str2}]
}

#-- while loop
proc while {condition code} {
   return [uplevel 1 "for {} {$condition} {} {$code}"]
}

#-- foreach VAR "abc xyz 123" { puts "($VAR) " }
proc foreach {varname list code} {
   upvar 1 $varname val
   for {set i 0}  {$i < [llength $list]}  {++ i} {
      set val [lindex $list $i]
      uplevel 1 "$code"
   }
}

#-- A switch statement.
#
# switch "val" {
#        "cmp1" {code1}
#        "cmp2" {code2}
#        "cmp3" {code3}
#   {{default}} {codeN}
# }
#
proc switch {compare_value val_code_pairs} {
   set len [llength $val_code_pairs]
   # loop over compare values + code pairs
   for  {set n 0}  {$n < $len}  {++ n} {
      set cmp [lindex $val_code_pairs $n];
      if {[expr $cmp eq $compare_value || $cmp eq {{default}} ]} {
         return [uplevel 1 [lindex $val_code_pairs [++ n]]];
      }
   }
}

#-- set a couple of variables
proc ui::vars {} {
   #-- import
   set current_page $::current_page
   set baseurl $::baseurl

   #-- export variables
   upvar 1 pagecat pagecat            # index | wcontent | wiki | tree | timeline
   upvar 1 pagename pagename          # src/data
   upvar 1 basedomain basedomain
   upvar 1 stats_description stats_description
   upvar 1 stats_social stats_social
   upvar 1 stats_forks stats_forks

   #-- Determine current page type
   set pagename ""
   if {[regexp {^(index|home)[?]?} $current_page]} {
      set pagecat "index"
   } else { if {[regexp {^wiki\?name=} $current_page]} {
         set pagecat "wiki"
         set pagename [string range $current_page 10 2048]
   } else {
      set pagecat $current_page
   } }

   #-- Split domain name from baseurl
   set basedomain [string range $baseurl [expr 7+[expr {[string range $baseurl 4 4] eq "s"}]] [expr 7+[string first "/" [string range "$baseurl/" 8 50]]]]

   #-- Project stats
   set stats_description [setting project-description]
   set stats_social 0
   set stats_forks 1
   catch { query { SELECT name,value FROM fx_stats WHERE name GLOB 'stats_*' } {
      set "$name" "$value"
      upvar 1 $name $name
   } }
}


#-- For outputting class=current in #sidebar
proc current {name} {
   upvar 1 pagecat pagecat
   if [regexp "^($name)" $pagecat] { puts { class=current} }
}


#-- Turn config:sitemap-* urls into button links
proc ui::sitemap_links {} {
   set color "green"
   foreach name "download docidx license contact changelog news freshcode repository doap project.json" {
      set url [setting sitemap-$name]
      if {[string length $url]} {
         html "<a href='$url' class='button $color proj-sitemap'>$name</a>"
      }
      set color "white"
   }
}

#-- Ordered list of project statistics (will populate global $stats() array)
proc ui::stats {} {
   uplevel 1 { query {SELECT
     (SELECT count(objid) FROM event WHERE type='ci' LIMIT 1) AS `stats_checkins`,
     (SELECT count(name) FROM filename LIMIT 1) AS `stats_files`,
     (SELECT count(status) FROM ticket LIMIT 1) AS `stats_tickets`,
     (SELECT count(DISTINCT user) FROM event LIMIT 1) AS `stats_developers`,
     (SELECT count(DISTINCT value) FROM tagxref WHERE tagid=8) AS `stats_branches`,
     (SELECT count(tagname) FROM tag WHERE tagname LIKE 'sym-%') AS `stats_tags`,
     (SELECT count(tagname) FROM tag WHERE tagname REGEXP '^sym[-a-z0-9_.]+\d+\.\d+') AS `stats_releases`
   } {} }
}

#-- Language/Content statistics (outputs colored bar graph)
proc ui::lang_stats {} {
   # fetch $lang(js/...), $lang_color(js), $lang_list, $total_size
   query {SELECT name, value FROM fx_stats ORDER by VALUE DESC} {
      set $name $value
   }
   # output color bar for language proportions
   #html "<div class=language-bar style='width:100%; height:3pt; box-sizing:border-box;'>"
   foreach name $lang_list {
      set percent "[expr $lang($name)*100]%"
      html "<span class=code-rate-$name style='height:100%; width:$percent; display:inline-block; background-color:#$lang_color($name)' title='$percent $name'></span>";
   }
   #html "</div>";
}

#-- print two table rows for last commit
proc ui::last_commit {} {
   query {
       SELECT *, CAST(julianday('now')-mtime AS INT) AS age, substr(comment,0,199) AS msg, substr(uuid, 0, 10) AS short_uuid
       FROM event JOIN blob ON blob.rid=event.objid
       WHERE type='ci' ORDER BY mtime DESC LIMIT 1
   } {
      html "  <tr><th colspan=3>$msg</th></tr>";
      html "  <tr><th colspan=3 style=background:#fff><a href='timeline?u=$user' class=user>$user</a> authored $age days ago
            <span style=float:right>last checkin <a href='ci/$uuid'>$short_uuid <span class=glyph>&#x2398;</span></a></span></th></tr>";
   }
}

#-- outputs table rows containing top-level filenames and recent checkin comments
proc ui::recent_files {dirname} {
   set seen "(.gitignore)"
   
   # search files using directory as base path
   set branch "trunk"
   set cutname 0
   set AND_DIR ""
   if {[string length $dirname]} {
       set dirname "$dirname/"
       set cutname [string length $dirname]
       set AND_DIR " AND substr(name, 0, \$cutname+1) = \$dirname "
   }

   # files   // vcache.rid=mlink.fid would be easier to skip deleted files, but access is prohibited(?)
   query "
       SELECT DISTINCT
          m.fnid, m.fid, MAX(m.mid),
          INSTR(SUBSTR(name,\$cutname+1),'/')>0  AS  is_dir,
          name                                AS  pathname,
          bf.rid   AS  fn_rid,     bf.uuid    AS  fn_uuid,
          bm.rid   AS  ci_rid,     bm.uuid    AS  ci_uuid,
          SUBSTR(comment, 0, 70)              AS  comment,
          CAST(JULIANDAY('now')-e.mtime AS INT) AS  age
       FROM
          filename
          LEFT JOIN mlink m ON m.fnid = filename.fnid
          LEFT JOIN tagxref ON m.mid = tagxref.rid
          LEFT JOIN blob bf ON bf.rid = m.fid
          LEFT JOIN blob bm ON bm.rid = m.mid
          LEFT JOIN event e ON e.objid = m.mid
       WHERE
          tagxref.value = \$branch
          $AND_DIR
       GROUP BY
          name
       HAVING
          fid <> 0
       ORDER BY
          is_dir DESC, name ASC, e.mtime DESC
   " {

      # separate directories and files
      set name [string range $pathname $cutname 2048]
      set dir [string first "/" $name]
      if {$dir>0} { set name [string range $name 0 [expr $dir-1]] }

      # skip seen files
      if [str::contains "($name)" $seen] { continue } else { set seen "($name),$seen" }
      
      # output table entries
      html "               <tr><td>";
      if {$dir>0} {
          # if there is an equivalent wiki page for a directory, then we mix filebox + wiki
          set display "wiki"
          #set display [?: [sql::page_exists "$dirname$name"] "wiki" "tree"]
          html "<a class=dir href='$display/[htmlize $dirname$name]'><b class=glyph>๐Ÿ“‚</b> [htmlize $name]</a>";
      } else {
          html "<a class=file href='artifact/$fn_uuid'><b class=glyph>๐Ÿ“„</b> [htmlize $name]</a>";
      }
      html "</td> <td>[htmlize $comment]<a href='ci/$ci_uuid'>โ€นโ€บ</a></td> <td>[htmlize $age] days ago</td></tr>\n";
   }
}

#-- social media share links
proc ui::social_links {baseurl} {
  html "
   <a class=sml-go href='https://plus.google.com/share?url=$baseurl' title=google+>g+</a> &middot;
   <a class=sml-fb href='https://www.facebook.com/sharer/sharer.php?u=$baseurl' title=facebook>fb</a> &middot;
   <a class=sml-tw href='https://twitter.com/intent/tweet?url=$baseurl' title=twitter>tw</a> &middot;
   <a class=sml-rd href='http://reddit.com/submit?url=$baseurl' title=reddit>rd</a> &middot;
   <a class=sml-in href='https://www.linkedin.com/shareArticle?mini=true&amp;url=$baseurl' title=linkedin>in</a> &middot;
   <a class=sml-su href='https://www.stumbleupon.com/submit?url=$baseurl' title=stumbleupon>su</a> &middot;
   <a class=sml-dl href='https://del.icio.us/post?url=$baseurl' title=delicious>dl</a>
  ";
}


# Outputs a textual /changelog
proc webpage_changelog {} {
  html "<!-- NEWS-style timeline --> <meta http-equiv=\"Content-Type\" content=\"text/plain\"> <pre>\n\n";
  set version "trunk"
  puts "$version (unreleased)\n";
  query {
     SELECT event.mtime, tag.tagname, MAX(tag.tagid), DATE(event.mtime) AS d,
            REPLACE(TRIM(REPLACE(event.comment, char(10,10), char(10)), char(8,10,13,32)), char(10), char(10,32,32,32)) AS comment
     FROM event
      LEFT JOIN tagxref ON event.objid=tagxref.rid
      LEFT JOIN tag ON tagxref.tagid=tag.tagid
     WHERE type='ci'
     GROUP BY objid
     ORDER BY event.mtime DESC
     LIMIT 750
  } {
     if {[regexp {^sym-.*\d+\.\d+} $tagname]} {
        for {} {[string length $tagname] >= 3 && [regexp {^\d+\.} $tagname] == 0} {} {
          set tagname [string range $tagname 1 100]
        }
        puts "\n$tagname ($d)\n";
     }
     puts " * $comment\n";
  }
  puts "\n\n";
}

# Alternative to /raw trunk file access without ?name=uuid,
# Doesn't work with CONTENT() yet.
proc webpage_cat {} {
  set name [getParameter name ""]
  if {![string length $name]} { puts "No filename given."; break; }
  query {
     SELECT uuid
     FROM blob LEFT JOIN mlink ON blob.rid=mlink.fid
               LEFT JOIN filename ON mlink.fnid=filename.fnid
     WHERE name = $name
     ORDER BY rid DESC LIMIT 1
  } { html [artifact "$uuid"]; }
}

# Generate a text/uri-list for available files
proc webpage_uri-list {} {
  html "# Fossil latest file references <pre>\r\n"
  query {
     SELECT filename.name, uuid
     FROM blob LEFT JOIN mlink ON blob.rid=mlink.fid LEFT JOIN filename ON mlink.fnid=filename.fnid
     GROUP BY filename.name  ORDER BY rid DESC
  } { html "$name?name=$uuid\r\n" }
}

# Invokes web request page procs
proc webpage_hook {} {
  #if {! [anycap ro]} { break }
  catch { "webpage_$::web_name"; return -code 2 found; } rc
  if {"$rc" eq "found"} { break continue }
}


#-- Whitelist for SQL params
# Just realized this is redundant; because query {} accepts
# uninterpolated \$varnames as parameter placeholders.
proc sql::allowed {str} {
   return [regexp {^[-a-zA-Z0-9 !$&/(){}=<>,.;:_+#*@]+$} $str]
}
#-- Also prohibit regex special chars
proc sql::allowed_regexp {str} {
   return [regexp {^[-a-zA-Z0-9 !$&/    =<>,.;:_ # @]+$} $str]
}


#-- Check for existence of wiki page
proc sql::page_exists {name} {
   query {SELECT 1 FROM tag WHERE tagname = ('wiki-' || $name)} { return 1 }
   return 0
}


#-- Check if exact file name (including path) exists in repository
proc sql::file_exists {name} {
   query {SELECT 1 FROM filename WHERE name = $name} { return 1 }
   return 0
}


#-- Find file by basename
proc sql::find_file {path} {
   if {![sql::allowed_regexp $path]} { return 0 }
   query {SELECT name FROM filename WHERE name REGEXP ('(^|/)' || $path || '\$')} { return $name }
   return ""
}


#-- Check if directory exists
proc sql::dir_exists {path} {
   if {![sql::allowed_regexp $path]} { return 0 }
   query {SELECT name FROM filename WHERE name REGEXP ('^' || $path || '/.+')} { return 1 }
   return 0
}

   
#-- returns true if string contained in another string
proc str::contains {needle haystack} {
   return [expr {-1 != [string first $needle $haystack]}]
}

#-- wrapper for [string first ...] to support startindex
proc str::next {search content start} {
   # cut out $content at $start before searching
   set p [string first $search [string range $content $start [string length $content]]]
   if [expr $p>=0] {
      set p [expr $start+$p]
   }
   return $p
}

#-- enclose string in e.g. html tags
proc str::wrap {content search before after} {
   set len [string length $search]
   set p 0
   while {[expr [set p [str::next $search $content $p]]>=0]} {
      set content "[string range $content 0 [expr $p-1]]$before$search$after[string range $content [expr $p+$len] 2000]";
      set p [expr $p+[string length "$before+$search+$after"]]; # skip a little further
   }
   return $content
}

#-- Split string into list on delimiter character
# (basically just turns delimiter into space)
#
proc str::explode {delim str} {
   set r ""
   set len [string length $str]
   while {-1 != [set p [string first $delim $str]]} {
      set r "$r [string range $str 0 [expr $p-1]]"
      set str [string range $str [++ p] $len]
   }
   return [list [string trim "$r $str"]]
}   

#-- Extract dirname from path/file/name
proc str::dirname {path} {
   return [string range $path 0 [expr [string last "/" $path]-1]]
}