Posted to tcl by bairui at Thu Feb 02 03:40:49 GMT 2017view pretty

#!/usr/bin/env wish

package require Tk
package require opt
package require Img
package require Tkhtml

# set app_dir [file dirname [info script]]
# source [file join $app_dir czrss.tcl]
# package provide czrss 0.11

package require snit
package require tdom
package require http

if {![catch {package require autoproxy}]} {
  autoproxy::init
}

namespace eval ::net {
  proc get_url url {
    set token [::http::geturl $url -timeout 10000]
    set data  [::http::data $token]
    ::http::cleanup $token
    return $data
  }

  proc get_image uri {
    #if the 'url' passed is an image name
    if { [lsearch [image names]  $uri] > -1 } {
      return $uri
    }

    # if the 'url' passed is a file on disk
    if { [file exists $uri] } {
      #create image using file
      image create photo $uri -file $uri
      return $uri
    }

    #if the 'url' is an http url.
    if { [string equal -length 7 $uri http://] } {
      image create photo $uri -data [get_url $uri]
      return $uri
    }
  }
}

# This is the class representing an RSS document
snit::type ::czrss::doc {
  variable xpath
  variable channel
  variable items
  variable url

  # Konstruktor for a given URI
  constructor { uri } {
    set url $uri
    $self load
  }

  method load { } {
    set xml ""
    set tries 0
    while {! [string match "*?xml*" $xml]} {
      set xml [::net::get_url $url]
      after 1000 incr tries
      if {$tries > 5} {return -code error}
    }

    # load xml into dom from temporary file
    set doc [ dom parse $xml ]
    set _root [ $doc documentElement ]

    set root [$doc documentElement]
    switch [getRSSVersion $doc] {
      0.91 - 0.92 - 0.93 - 2.0 {
        set xpath(titleXpath)        {/rss/channel/title/text()}
        set xpath(linkXpath)         {/rss/channel/link/text()}
        set xpath(imgNodeXpath)      {/rss/channel/image/title}
        set xpath(imgTitleXpath)     {/rss/channel/image/title/text()}
        set xpath(imgLinkXpath)      {/rss/channel/image/url/text()}
        set xpath(imgWidthXpath)     {/rss/channel/image/width/text()}
        set xpath(imgHeightXpath)    {/rss/channel/image/height/text()}
        set xpath(storiesXpath)      {/rss/channel/item}
        set xpath(itemTitleXpath)    {title/text()}
        set xpath(itemLinkXpath)     {link/text()}
        set xpath(itemPubDateXpath)  {pubDate/text()}
        set xpath(itemDescXpath)     {description/text()}
      }
      1.0 {
        set xpath(titleXpath)        {/rdf:RDF/*[local-name()='channel']/*[local-name()='title']/text()}
        set xpath(linkXpath)         {/rdf:RDF/*[local-name()='channel']/*[local-name()='link']/text()}
        set xpath(imgNodeXpath)      {/rdf:RDF/*[local-name()='image']}
        set xpath(imgTitleXpath)     {/rdf:RDF/*[local-name()='image']/*[local-name()='title']/text()}
        set xpath(imgLinkXpath)      {/rdf:RDF/*[local-name()='image']/*[local-name()='url']/text()}
        set xpath(imgWidthXpath)     {/rdf:RDF/*[local-name()='image']/*[local-name()='width']/text()}
        set xpath(imgHeightXpath)    {/rdf:RDF/*[local-name()='image']/*[local-name()='height']/text()}
        set xpath(storiesXpath)      {/rdf:RDF/*[local-name()='item']}
        set xpath(itemTitleXpath)    {*[local-name()='title']/text()}
        set xpath(itemLinkXpath)     {*[local-name()='link']/text()}
        set xpath(itemPubDateXpath)  {*[local-name()='pubDate']/text()}
        set xpath(itemDescXpath)     {*[local-name()='description']/text()}
      }
      default {
        error "Unssupported schema [getRSSVersion $doc]"
      }
    }

    # Channel
    set cN [ $_root child 1 channel ]
    set channel [::czrss::channel create %AUTO% $self $cN]

    # Items
    set items {}
    set stories [$_root selectNodes $xpath(storiesXpath) ]
    foreach iN $stories {
      lappend items [ ::czrss::item  create %AUTO% $self $iN ]
    }
  }

  # returns the XPath Query for a given type
  method xpath { key } {
    return $xpath($key)
  }

  # returns the channel object
  method channel {} {
    return $channel
  }

  # returns a list of items
  method items {} {
    return $items
  }

  # detects the RSS version of the document
  proc getRSSVersion {doc} {
    set root [$doc documentElement]
    switch [$root nodeName] {
      rss {
        if {[$root hasAttribute version]} {
          return [$root getAttribute version]
        }
        # Best guess as most stuff is optional...
        return 0.92
      }
      rdf:RDF {
        return 1.0
      }
      default {
        return 0
      }
    }
  }
}

# this class is used to contain rss items
snit::type ::czrss::item {
  variable _node
  variable _doc

  constructor {doc node } {
    variable history
    set _doc $doc
    set _node $node
  }
  # get the title
  method title { } {
    set xpath [$_doc xpath itemTitleXpath]
    return [ ::czrss::nodeTxt $_node $xpath]
  }
  # get the link
  method link {} {
    set xpath [$_doc xpath itemLinkXpath]
    return [ ::czrss::nodeUri $_node $xpath]
  }
  # get the description
  method description {} {
    set xpath [$_doc xpath itemDescXpath]
    return [ ::czrss::nodeTxt $_node $xpath]
  }
  # return the publication date as string
  method pubDate {} {
    set xpath [$_doc xpath itemPubDateXpath]
    return [ ::czrss::nodeTxt $_node $xpath]
  }
}

# this class contains information on the channel
snit::type ::czrss::channel {
  variable _doc
  variable _root

  constructor { doc root} {
    set _doc $doc
    set _root $root
  }
  # get the title
  method title { } {
    set xpath [$_doc xpath titleXpath]
    return [ ::czrss::nodeTxt $_root $xpath]
  }
  # get the image link
  method imgLink {} {
    set xpath [$_doc xpath imgLinkXpath]
    return [ ::czrss::nodeUri $_root $xpath]
  }
  # get the image title
  method imgTitle {} {
    set xpath [$_doc xpath imgTitleXpath]
    return [ ::czrss::nodeUri $_root $xpath]
  }

  # get the image width
  method imgWidth {} {
    set xpath [$_doc xpath imgWidthXpath]
    return [ ::czrss::nodeTxt $_root $xpath]
  }
  # get the image height
  method imgHeight {} {
    set xpath [$_doc xpath imgHeightXpath]
    return [ ::czrss::nodeTxt $_root $xpath]
  }
}

# this namespace contains some utility methods
namespace eval ::czrss {

  proc encUri {uri} {
    set res {}
    foreach c [split $uri {}] {
      append res [
        if {[string match {[-A-Za-z.0-9!()'*_~:/]} $c]} {
          set c
        } else {
          format %%%02X [scan $c %c]
        }]
    }
    set res
  }

  proc encTxt {txt} {
    return [string map { & &amp; < &lt; > &gt; } $txt]
  }

  proc nodeUri {node xpath} {
    if {[$node selectNode $xpath] != ""} {
      # Only if there is a lonely &, quote it back to an entity.
      return [encUri [[$node selectNode $xpath] nodeValue]]
    } else {
      return ""
    }
  }

  proc nodeTxt {node xpath} {
    if {[$node selectNode $xpath] != ""} {
      return [[$node selectNode $xpath] nodeValue]
    } else {
      return ""
    }
  }

}

namespace eval ::rssgui {
  variable counter

  proc init {} {
    variable counter
    set counter 0

    . configure -background white
    option add *background white

    ttk::frame .t

    text .t.text  -relief flat -wrap word \
      -selectbackground blue \
      -yscrollcommand {.t.sby set}  \
      -highlightthickness 0

    scrollbar .t.sby -orient vert -command {.t.text yview} \
      -highlightthickness 0

    # Configure text widget
    .t.text tag configure title -foreground steelblue -font {Helvetica  16} \
        -spacing1 5 -spacing3 5
    .t.text tag configure description -foreground black \
        -spacing1 10 -lmargin1 20 -lmargin2 10 -spacing3 5 -elide true
    .t.text tag configure even -background whitesmoke
    .t.text tag configure odd -background white
    .t.text tag configure visible -elide false
    .t.text configure -state disabled

    html .browser -height 20 -imagecmd ::net::get_image

    ttk::frame .buttons
    ttk::frame .buttons.padding

    set off_data {
      R0lGODlhIAAgAOcAAP////7+/vv7++rq6uPj4/X19eXl5ZCQkHBwcMXFxfn5+f39/crKykJCQhoa
      GpGRkfLy8vz8/MDAwDMzMxEREYGBge/v78HBwTQ0NBISEoODg97e3s3Nzfb29sPDwzg4OBYWFoWF
      hfDw8PPz87+/v+Dg4NnZ2Wtra1tbW9TU1DU1NcnJyUZGRm1tbdvb2+vr63p6ehMTEyoqKrGxsYKC
      gvj4+J2dnXZ2dunp6bS0tCUlJQsLC19fXzIyMhAQENPT01dXVwoKCiMjI7Ozs+zs7GxsbAkJCby8
      vMLCwoSEhDExMdXV1To6OnJycu3t7Tc3NxQUFIaGhmlpaVFRUeLi4sbGxi4uLh0dHaioqJaWlszM
      zCgoKLq6urKysiEhIScnJx4eHtfX1zY2NiAgICkpKZSUlPT09Pf398/Pz+Hh4U5OTggICFlZWVpa
      WlJSUouLi6SkpA0NDQcHB0hISPr6+sjIyI+PjwwMDE9PT7e3t0pKShwcHJWVlXd3dy8vL3t7e7W1
      tampqSsrK+bm5iIiIjAwMBsbG4CAgK6url1dXS0tLRgYGDs7O6Wlpaamprm5uf//////////////
      ////////////////////////////////////////////////////////////////////////////
      ////////////////////////////////////////////////////////////////////////////
      ////////////////////////////////////////////////////////////////////////////
      ////////////////////////////////////////////////////////////////////////////
      ////////////////////////////////////////////////////////////////////////////
      /////////////////////////////////////////////////////yH+FUNyZWF0ZWQgd2l0aCBU
      aGUgR0lNUAAh+QQBCgD/ACwAAAAAIAAgAAAI/gABCBxIsKDBgwgTKlzIUGCAhhAFChhAoEBEhgEM
      HECQQMFFhQsYNHDwAMJHhAEiSJhAoYKFhycdLgiQ8gKGDBpeBhBwcsEGDh1SevgAIoQIACNIlFgQ
      MYCJEyhSLIhwQQVOCwtWsGjhAibDFzBiyJihgOpNGhZq2HCQ4QaOhhFy6NjBw8WCBRJ6+HC54AeQ
      IEKGRGBIpIiRHkd4LkCiIkaSoxGOKNkBw+TCJUx2NHEicIGHJ1CiHAXwQkqQKVQWBqhi5QoWngAW
      2MxAY3SNLDEmaPF6cAGJLTq4MI39OfToBV28fCExHOWFL2C6DA8QhocYLB06dxlD5gLvgxx6t2Qo
      U2OgGS1HDAw/8yCDCjQM06hZwyYNQQEKvFJps8YNAYYFvOHDGHCUd9AZcHgRRxQWqaYFE3LMkQAd
      BtFBQgNyfFDHdwiZYYcDd+CRB1YSvZCDHkHswUeDDRHQBwhG+PEHIFrUEUgLghgBAgyDXBRAGjTo
      IEcchExQiCFBrPHFIWlwqJoTiCSiCBiLLEIIIy3k4ISTDdExSB05NOLIIykQMVhMBAWwgAACNIfm
      m3DGKeecdCoUEAA7
    }

    image create photo off -data $off_data
    ttk::button .buttons.exit -command {exit 0} -image off

    grid .t.text  -row 0 -column 0 -sticky news
    grid .t.sby   -row 0 -column 1 -sticky ns

    grid .buttons.padding -row 0 -column 0 -sticky ew
    grid .buttons.exit    -row 0 -column 1 -sticky e

    grid .t       -row 0 -column 0 -sticky news
    grid .browser -row 1 -column 0 -sticky news
    grid .buttons -row 2 -column 0 -sticky ew

    grid rowconfigure    .  .t       -weight 2
    grid columnconfigure .  .t       -weight 2
    grid rowconfigure    .  .browser -weight 1
    grid columnconfigure .  .browser -weight 1
    grid rowconfigure    .t .t.text    -weight 1
    grid columnconfigure .t .t.text    -weight 1
    grid columnconfigure .buttons .buttons.padding  -weight 1
  }

  proc setFeed { url } {
    set doc [::czrss::doc create %AUTO% $url ]
    set channel [$doc channel]
    wm title . [$channel title]
    .t.text configure -state normal
    .t.text delete 0.0 end
    foreach item [ $doc items ] {
      insertMessage [$item title] [$item description] [$item link]
    }
    .t.text configure -state disabled
  }

  # insert a message into the text widget
  proc insertMessage { title description url } {
    variable counter
    set id [ clock clicks]

    if {[expr [incr counter] % 2 ]} {
      set row even
    } else {
      set row odd
    }

    .t.text insert end "$title\n" [ list $row title $id]
    .t.text tag bind $id <Enter> [list ::rssgui::enterTag $id]
    .t.text tag bind $id <Double-1> [list ::rssgui::startBrowser [string map { % %% } $url]]

    incr id
    .t.text insert end "$description\n" [ list $row description $id]
  }

  # called when entering a taged item with the mouse in the text widget
  proc enterTag { id } {
    set start [ lindex [.t.text tag nextrange $id 0.0 end] 0]
    set end   [ lindex [.t.text tag nextrange [expr $id +1 ] 0.0 end] 1]
    set text [.t.text get $start $end]
    browse $text
  }

  proc browse {text} {
    .browser reset
    .browser parse $text
  }

  # start a HTML browser on the local machine
  proc startBrowser {url} {
    set binary [array get env BROWSER]
    if {$binary eq ""} {
      foreach browser [list google-chrome firefox mozilla galeon konqueror netscape opera] {
        set binary [lindex [auto_execok $browser] 0]
        if {[string length $binary]} {
          break
        }
      }
    } else {
      set binary [lindex $binary 1]
    }
    catch {exec $binary $url &}
  }
}

proc main {{url "http://news.yahoo.com/rss/world" }} {
  rssgui::init
  rssgui::setFeed $url
}

if {[catch { eval main $argv } res]} {
  puts stderr $res
} else {
  puts stdout $res
}