#!/usr/bin/ruby
## Time-stamp: <2004-03-31 23:49:52 vk>

require "parsearg.rb" ## to parse command line arguments

## -------------------------------------------------------------------
## 2do:  (volunteers please apply ;-)
##   replace FIXXMEs with code
##   error-msg: "Class/Method: blablabla" using self.class and so on
##   more error checking (idiot proof? there are always better idiots ;-)
##   more parsers
##   more outputformatters
##   probably: "interface" for formatters and parsers using "module" (book p.275f)
##   probably: "intelligent" recognition of input file format
##   probably: $OUTFORMATNAMES and $OUTFORMATHANDLER replaced by class constants and classnames
##             (including format recognition method)
## -------------------------------------------------------------------

def usage
  puts <<-EOD

 PURPOSE:  converts bookmarks from various formats into others

           Some minor things (personalbar, ...) may get lost, please
           back-up your files before using this tool!
          
 AUTHOR:   Karl Voit (shellscript@Karl-Voit.at)
          
 LICENSE:  GPL
          
 VERSION:  0.1 2004-03-31  first version (only Opera Hotlist v2 and
                           FireFox v0.8 (ignoring linkbar, personalbar, 
                           trash)

 SYNOPSIS: bmconverter.rb [options]

           (OR if /usr/bin/ruby is not found:
           ruby bmconverter.rb [options])

 OPTIONS:  --informat FORMAT    : format of the input-file
           --outformat FORMAT   : format of the output-file
           --input FILE         : name of the input-file
           --output FILE        : name of the output-file
           --version            : print out this help and exits
           --help               : print out this help and exits
           --HT informat        : print out "How to add a new informat"
           --HT outformat       : print out "How to add a new outformat"

EOD
  printf "           input formats currently supported:\n             "
  puts $INFORMATNAMES.join(", ")
  printf "           output formats currently supported:\n             "
  puts $OUTFORMATNAMES.join(", ")
  puts "\n"

end
$USAGE = 'usage'
## -------------------------------------------------------------------
def htoutformat
  puts <<-EOD

  How to add a new formatter (aka output format)
  ==============================================

   1) write a class including the methods (that returns only strings)

        include Formatter ## please inherit from module "Formatter" to
                          ## ensure complete functionality
        def formatUrl(url)
        def formatFolder(folder)
        def formatFolderend(folderend)
        def formatSeperator(seperator)
        def getHeader()

      Use FireFox08Formatter as an example.

   2) add your class in the global arrays $OUTFORMATNAMES and $OUTFORMATHANDLER

          * make sure, that both entries have the SAME index in the arrays!

   3) test, debug and publish

EOD
end
$HTOUTFORMAT='htoutformat'
## -------------------------------------------------------------------
def htinformat
  puts <<-EOD

  How to add a new parser (aka input format)
  ==========================================

   1) write a parse-class including the methods

        include Parser ## please inherit from module "Parser" to
                       ## ensure complete functionality

        def initialize(thisstack, thisstatusmachine)

        def parse(inputfilename)
          * store all fields in the Bookmarkstack
          * entries in the Bookmarkstack are of type Url, Seperator, Folder and Folderend

      Use OperaHotlist2Parser as an example.

   2) add your parser in the global arrays $INFORMATNAMES and $INFORMATHANDLER

          * make sure, that both entries have the SAME index in the arrays!

   3) test, debug and publish

EOD
end
$HTINFORMAT='htinformat'
## -------------------------------------------------------------------




## -------------------------------------------------------------------
## purpose: defines a generic formatter
## -------------------------------------------------------------------
module Formatter

  ## reformats the beginning whitespaces according to folderdepth
  def addindentation(string, item)
    offsetwhitespace = "    " * item.depth
    return offsetwhitespace + string.sub(/\n/, "\n"+offsetwhitespace)+"\n"
  end

  ## formats an URL
  def formatUrl(url)
    raise "ERROR of a programmer: the used formatter does not implements its own \"formatUrl()\"-method from module \"Formatter\""
  end

  ## formats a folder
  def formatFolder(folder)
    raise "ERROR of a programmer: the used formatter does not implements its own \"formatFolder()\"-methods from module \"Formatter\""
  end

  ## formats a seperator
  def formatSeperator(seperator)
    raise "ERROR of a programmer: the used formatter does not implements its own \"formatSeperator()\"-methods from module \"Formatter\""
  end

  ## formats an end-tag for a folder
  def formatFolderend(folderend)
    raise "ERROR of a programmer: the used formatter does not implements its own \"formatFolderend()\"-methods from module \"Formatter\""
  end

  ## returns a string of a bookmark header
  def getHeader()
    raise "ERROR of a programmer: the used formatter does not implements its own \"getHeader()\"-methods from module \"Formatter\""
  end

end





## -------------------------------------------------------------------
## purpose: defines a generic parser
## -------------------------------------------------------------------
module Parser

  def initialize(thisstack, thisstatusmachine)
    raise "ERROR of a programmer: the used parser does not implements its own \"initialize()\"-methods from module \"Parser\""
  end

  def parse(inputfilename)
    raise "ERROR of a programmer: the used parser does not implements its own \"parse()\"-methods from module \"Parser\""
  end

end





## -------------------------------------------------------------------
## purpose: formats a given bookmark-stack for FireFox v0.8
## -------------------------------------------------------------------
class FireFox08Formatter

  include Formatter

  ## formats an URL
  def formatUrl(url)
    ##              <DT><A HREF="http://heise.de/newsticker/" ADD_DATE="1080134840" LAST_VISIT="1080157555" LAST_MODIFIED="1080161958" SHORTCUTURL="heisekeywdr" ICON="http://heise.de/favicon.ico" SCHEDULE="0123456|0-24|10|sound,open" LAST_PING="1080161580" LAST_CHARSET="ISO-8859-1" ID="rdf:#$ws1sC">Eitnrag 2 im subfolder1</A>
    ##  <DD>descr
    raise "ERROR: formatUrl(#{url}) was called with no URL as argument." if url.class != Url
    raise "ERROR: formatUrl(#{url}) was called where no URL is found." if url.url.empty?
    raise "ERROR: formatUrl(#{url}) was called where no NAME is found." if url.name.empty?

    ## ignored: id, depth, shortcuturl, inpersonalfolder, personalfolderposition
    folderstring = "<DT><A HREF=\"#{url.url}\""
    folderstring += " ADD_DATE=\"#{url.createdtimestamp}\"" if ! url.createdtimestamp.empty?
    folderstring += " LAST_VISIT=\"#{url.lastvisitedtimestamp}\"" if ! url.lastvisitedtimestamp.empty?
    folderstring += " LAST_MODIFIED=\"#{url.lastmodifiedtimestamp}\"" if ! url.lastmodifiedtimestamp.empty?
    folderstring += " SHORTCUTURL=\"#{url.nickname}\"" if ! url.nickname.empty?
    folderstring += " ICON=\"#{url.icon}\"" if ! url.icon.empty?
    folderstring += " LAST_CHARSET=\"#{url.charset}\"" if ! url.charset.empty?
    folderstring += ">#{url.name}</A>"
    folderstring += "\n<DD>#{url.description}" if ! url.description.empty?
    return addindentation(folderstring, url)
  end


  ## formats a folder
  def formatFolder(folder)
    #  <DT><H3 PERSONAL_TOOLBAR_FOLDER="true" ADD_DATE="1080161710">subfolder2</H3>
    #  <DD>descr
    #  <DL><p>
    raise "ERROR: formatFolder(#{folder}) was called with no Folder as argument." if folder.class != Folder

    # ignored: depth, id, lastmodifiedtimestamp, expanded
    folderstring = "<DT><H3"
    folderstring += " ADD_DATE=\"#{folder.createdtimestamp}\"" if ! folder.createdtimestamp.empty?
    folderstring += " PERSONAL_TOOLBAR_FOLDER=\"#{folder.ispersonalfolder}\"" if folder.ispersonalfolder == true
    folderstring += ">#{folder.name}</H3>\n<DD>"
    folderstring += "#{folder.description}" if ! folder.description.empty?
    folderstring += "\n<DL><p>"
    return addindentation(folderstring, folder)
  end


  ## formats an end-tag for a folder
  def formatFolderend(folderend)
    #  </DL><p>
    return addindentation("</DL><p>\n", folderend)
  end


  ## formats a seperator
  def formatSeperator(seperator)
    return addindentation("<HR>\n", seperator)
  end


  ## returns a string of a bookmark header
  def getHeader()
    ## <!DOCTYPE NETSCAPE-Bookmark-file-1>
    ## <!-- This is an automatically generated file.
    ##      It will be read and overwritten.
    ##      DO NOT EDIT! -->
    ## <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
    ## <TITLE>Bookmarks</TITLE>
    ## <H1>Bookmarks</H1>
    ## 
    return "<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<!-- This is generated file from bmconverter.rb.\n" +
           "     It will be read and overwritten.\n     DO NOT EDIT! -->\n" +
           "<META HTTP-EQUIV=\"Content-Type\" CONTENT=\"text/html; charset=UTF-8\">\n" +
           "<TITLE>Bookmarks</TITLE>\n<H1>Bookmarks</H1>\n\n"
  end

end# class FireFox08Formatter






## -------------------------------------------------------------------
## purpose: formats a given bookmark-stack for Opera Hotlist v2
## -------------------------------------------------------------------
class OperaHotlist2Formatter

  include Formatter

  ## formats an URL
  def formatUrl(url)
    ##  #URL
    ##          ID=3921
    ##          NAME=Eintrag 1 im subfolder vom subfolder1
    ##          URL=http://www.vc-graz.ac.at/vpn/vpnc/
    ##          CREATED=1080161248
    ##          DESCRIPTION=descr
    ##          SHORT NAME=nick
    raise "ERROR: formatUrl(#{url}) was called with no URL as argument." if url.class != Url
    raise "ERROR: formatUrl(#{url}) was called where no URL is found." if url.url.empty?
    raise "ERROR: formatUrl(#{url}) was called where no NAME is found." if url.name.empty?

    ## ignored: shortcuturl, id, lastmodifiedtimestamp, icon, charset, depth
    folderstring = "#URL\n"
    folderstring += "      NAME=#{url.name}\n"
    folderstring += "      URL=#{url.url}\n"
    folderstring += "      CREATED=#{url.createdtimestamp}\n" if ! url.createdtimestamp.empty?
    folderstring += "      VISITED=#{url.lastvisitedtimestamp}\n" if ! url.lastvisitedtimestamp.empty?
    folderstring += "      NICKNAME=#{url.nickname}\n" if ! url.nickname.empty?
    folderstring += "      SHORT NAME=#{url.nickname}\n" if ! url.nickname.empty?
    folderstring += "      ON PERSONALBAR=YES\n" if url.inpersonalfolder
    folderstring += "      PERSONALBAR_POS=#{url.personalfolderposition}\n" if url.inpersonalfolder && url.personalfolderposition!=-1
    folderstring += "      DESCRIPTION=#{url.description}\n" if ! url.description.empty?
    folderstring += "\n"
    return folderstring
  end


  ## formats a folder
  def formatFolder(folder)
    ## #FOLDER
    ##         ID=3918
    ##         NAME=subfolder von subfolder1
    ##         CREATED=1080161195
    ##         EXPANDED=YES
    raise "ERROR: formatFolder(#{folder}) was called with no Folder as argument." if folder.class != Folder
    raise "ERROR: formatFolder(#{folder}) was called where no NAME is found." if folder.name.empty?

    # ignored: depth, id, lastmodifiedtimestamp
    folderstring = "#FOLDER\n"
    folderstring += "        NAME=#{folder.name}\n"
    folderstring += "        CREATED=#{folder.createdtimestamp}\n" if ! folder.createdtimestamp.empty?
    folderstring += "        EXPANDED=YES\n" if folder.expanded
    folderstring += "        LINKBAR FOLDER=YES\n" if folder.ispersonalfolder
    folderstring += "        LINKBAR STOP=YES\n" if folder.ispersonalfolder
    folderstring += "        ON PERSONALBAR=YES\n" if folder.inpersonalfolder
    folderstring += "        PERSONALBAR_POS=#{folder.personalfolderposition}\n" if folder.inpersonalfolder && folder.personalfolderposition!=-1
    folderstring += "\n"
    return folderstring
  end


  ## formats an end-tag for a folder
  def formatFolderend(folderend)
    return "-\n\n"
  end


  ## formats a seperator
  def formatSeperator(seperator)
    ## ignore it, because IMHO there is no such thing in an Opera Hotlist v2
  end


  ## returns a string of a bookmark header
  def getHeader()
    return "Opera Hotlist version 2.0\nOptions: encoding = utf8, version=3\n\n"
  end

end# class OperaHotlist2Formatter





## -------------------------------------------------------------------
## purpose: parses Opera Hotlist v2
## -------------------------------------------------------------------
class FireFox08Parser

  include Parser

  def initialize(thisstack, thisstatusmachine)
    @stack=thisstack
    @state=thisstatusmachine
    @linenumber=0
  end

  def parse(inputfilename)

    @stack.push(Headerend.new())

    File.open(inputfilename).each { |line|

      @linenumber += 1

      case line
        
        ## Header:
        ## <!DOCTYPE NETSCAPE-Bookmark-file-1>
        ## <!-- This is an automatically generated file.
        ##      It will be read and overwritten.
        ##      DO NOT EDIT! -->
        ## <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
        ## <TITLE>Bookmarks</TITLE>
        ## <H1>Bookmarks</H1>
        ## 
        ## <DL><p>

        ## Folderdefinition:
        #  <DT><H3 ADD_DATE="1080161710">subfolder2</H3>
        #  <DD>descr
        #  <DL><p>
      when /^\s*<DT><H3.*>.*<\/H3>\s*$/
        case @state.get
        when $URLSTATE
          @stack.pop ## end of URL
        when $NOSTATE
          ## everything is OK
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
        end

        @stack.push(Folder.new())

        folderdefinition= /^\s*<DT><H3(.*)>(.*)<\/H3>\s*$/.match(line)

        @stack.top.name = folderdefinition[2]

        folderdefinition[1].strip.split("\" ").each { |optionandvalue|

          ## optionandvalue == "NAME=\"value"
          name, value  = optionandvalue.split("=\"",2)

          case name
          when "LAST_MODIFIED"
            @stack.top.lastmodifiedtimestamp = value
          when "ADD_DATE"
            @stack.top.createdtimestamp = value
          when "PERSONAL_TOOLBAR_FOLDER"
            @stack.top.ispersonalfolder = true if value == "true"
          when "ID"
            @stack.top.id = value
          else
            raise "Parse ERROR: unknown option name for a folder. name[#{name}] value[#{value}] @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
          end
        }
        


        ## description line (folder or URL)
      when /^\s*<DD>.*$/
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          value=line.split('<DD>',2)[1].strip
          @stack.top.description=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
        end


        ## end of folder definition
      when /^\s*<DL><p>\s*$/
        case @state.get
        when $HEADERSTATE
          @stack.pop ## end of header
        when $FOLDERSTATE
          @stack.pop
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
        end


        ## end of folder content
        #  </DL><p>
      when /^\s*<\/DL><p>\s*$/
        case @state.get
        when $NOSTATE
          @stack.push(Folderend.new())
          @stack.pop
        when $URLSTATE
          @stack.pop ## pop the last URL
          @stack.push(Folderend.new())
          @stack.pop
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
        end


        ## URL:
        ##              <DT><A HREF="http://heise.de/newsticker/" ADD_DATE="1080134840" LAST_VISIT="1080157555" LAST_MODIFIED="1080161958" SHORTCUTURL="heisekeywdr" ICON="http://heise.de/favicon.ico" SCHEDULE="0123456|0-24|10|sound,open" LAST_PING="1080161580" LAST_CHARSET="ISO-8859-1" ID="rdf:#$ws1sC">Eitnrag 2 im subfolder1</A>
        ##  <DD>descr

        ## an URL definition line
      when /^\s*<DT><A HREF.*>.*<\/A>\s*$/
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          @stack.pop
        end
        @stack.push(Url.new())

        urldefinition= /^\s*<DT><A (.*)>(.*)<\/A>\s*$/.match(line)

        @stack.top.name = urldefinition[2]

        urldefinition[1].strip.split("\" ").each { |optionandvalue|

          ## optionandvalue == "NAME=\"value"
          name, value  = optionandvalue.split("=\"",2)

          case name
          when "HREF"
            @stack.top.url = value
          when "ADD_DATE"
            @stack.top.createdtimestamp = value
          when "LAST_VISIT"
            @stack.top.lastvisitedtimestamp = value
          when "LAST_MODIFIED"
            @stack.top.lastmodifiedtimestamp = value
          when "SHORTCUTURL"
            @stack.top.shortcuturl = value
          when "ICON"
            @stack.top.icon = value
          when "SCHEDULE"
            ## ignored
          when "LAST_PING"
            ## ignored
          when "LAST_CHARSET"
            @stack.top.charset = value
          when "ID"
            @stack.top.id = value
          else
            raise "Parse ERROR: unknown option name for an URL. name[#{name}] value[#{value}] @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
          end
        }
        

        ## <HR>
      when /^\s*<HR>\s*$/
          @stack.push(Seperator.new())
          @stack.pop
        

        ## empty lines
      when /^\s+$/
        case @state.get
        when $NOSTATE, $HEADERSTATE
          # ignore
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] line[#{line}]" 
        end


        ## all lines with no catch-rule above
      else
        case @state.get
        when $HEADERSTATE
          # ignore lines of header with no special rule
        else
          raise "Parse error: unknown element: line[#{line.strip}]"
        end
      end

    }# each line
  end# parse

end# class FireFox08Parser





## -------------------------------------------------------------------
## purpose: parses Opera Hotlist v2
## -------------------------------------------------------------------
class OperaHotlist2Parser

  include Parser

  def initialize(thisstack, thisstatusmachine)
    @stack=thisstack
    @state=thisstatusmachine
    @linenumber=0
  end

  def parse(inputfilename)

    @stack.push(Headerend.new())

    File.open(inputfilename).each { |line|

      @linenumber+=1

      case line
        
        ## empty lines (i.e. end of URL or folder definition)
      when /^\s+$/
        case @state.get
        when $HEADERSTATE, $URLSTATE, $FOLDERSTATE
          @stack.pop
        when $NOSTATE
          # ignore
        end


        ## Opera Hotlist version header line: only check version number
      when /\s*?Opera Hotlist version.*/
        raise "Opera Header found [#{line}] but parser state (#{@state.get}) is not in header state." if @state.get != $HEADERSTATE
        value=line.split("version",2)[1].strip
        raise "Opera Hotlist Parser found Opera Hotlist version header which is not 2.0" if value != "2.0"


        ## Opera Hotlist Options (ignored)
      when /\s*?Options:.*/
        raise "Opera Header found [#{line}] but parser state (#{@state.get}) is not in header state." if @state.get != $HEADERSTATE


      when /\#FOLDER/
        @stack.push(Folder.new())


        ## only a dash (end of folder content)
      when /^\s*-\s*$/
        @stack.push(Folderend.new())
        @stack.pop


      when /\#URL/
        @stack.push(Url.new)


      when /\sID=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          @stack.top.id=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /^\s*NAME=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          @stack.top.name=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /^\s*SHORT NAME=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE
          @stack.top.shortcuturl=value
        when $FOLDERSTATE
          raise "ERROR: no short name for a folder implemented yet"
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /^\s*NICKNAME=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE
          @stack.top.nickname=value
        when $FOLDERSTATE
          raise "ERROR: no nickname for a folder implemented yet"
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sURL=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE
          @stack.top.url=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sCREATED=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          @stack.top.createdtimestamp=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sDESCRIPTION=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          @stack.top.description=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sVISITED=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE
          @stack.top.lastvisitedtimestamp=value
        when $FOLDERSTATE
          ## ignore
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sEXPANDED=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE
          raise "ERROR: there is no such thing like an expandable URL (yet)" 
        when $FOLDERSTATE
          case value
          when /YES/
            @stack.top.expanded=true
          when /NO/
            @stack.top.expanded=false
          else
            raise "PARSE ERROR: expanded is not YES not NO" 
          end
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end

      ## Ignored entries:

      when /\sACTIVE=.*/
        ## ignore

      when /\sACTIVE FOLDER=.*/
        ## ignore

      when /\sFIND FOLDER=.*/
        ## ignore

      when /\sLINKBAR FOLDER=YES/
        case @state.get
        when $URLSTATE
          raise "ERROR: there is no such thing like a linkbar folder in URL" 
        when $FOLDERSTATE
          @stack.top.ispersonalfolder=true
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sLINKBAR STOP=.*/
        ## ignore


      when /\sON PERSONALBAR=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          case value
          when /YES/
            @stack.top.inpersonalfolder=true
          when /NO/
            @stack.top.inpersonalfolder=false
          end
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sPERSONALBAR_POS=.*/
        value=line.split('=',2)[1].strip
        case @state.get
        when $URLSTATE, $FOLDERSTATE
          @stack.top.personalfolderposition=value
        else
          raise "Parse ERROR: @linenumber[#{@linenumber}] @state.get[#{@state.get.to_s}] value[#{value}] line[#{line}]" 
        end


      when /\sORDER=.*/
        ## ignore

      when /\sTRASH FOLDER=.*/
        ## ignore

      else
        raise "Parse error: unknown element: line[#{line.strip}]"
      end

    }# each line
  end# parse

end# OperaHotlist2Parser





## -------------------------------------------------------------------
## purpose: data object to store the parse tree of bookmarks and folders
## original source: http://www.approximity.com/rubybuch2/node183.html
## note: this stack is a special "intelligent" stack, that handles the
##       output and the states, so 'stack' might be a misleading name
## -------------------------------------------------------------------
class Bookmarkstack

  ## standard stack including additional statistics (see push)
  def initialize(thismax_size, thisstats, thisstate, thisformatter, thisoutput)
    @elements = []	
    @max_size = thismax_size
    @statistics = thisstats
    @state = thisstate
    @formatter = thisformatter
    @output = thisoutput
    @firsturl=true ## only to beautify output
  end


  def size; @elements.size; end


  def empty?; @elements.empty?; end


  def top
    return @elements.last
  end


  ## stack-push, setting the states and adding the items, printing status informations to the screen
  def push(elem)
    begin
      raise "push called when stack reached maximum number of entries(#{@max_size})" if @max_size == @size

      ## to collect statistical informations and print out current number of URLs
      case elem.class.to_s
      when "Url"

        if @firsturl
          printf "  %6d",@statistics.incUrl
          @firsturl=false
        else
          printf "  \b\b\b\b\b\b%4d",@statistics.incUrl
        end
        elem.depth = @statistics.folderdepth
        @state.set($URLSTATE)

      when "Folder"
        @statistics.incFolder
        elem.depth = @statistics.folderdepth
        @state.set($FOLDERSTATE)

      when "Seperator"
        elem.depth = @statistics.folderdepth

      when "Headerend"
        # do nothing

      when "Folderend"
        elem.depth = @statistics.folderdepth

      end

      @elements.push(elem)
    end
  end


  ## stack-pop including state resetting and output using the formatter
  def pop
    begin
      raise "pop called when stack is empty" if empty?

      ## to collect statistical informations and print out current number of URLs
      case @elements.last.class.to_s

      when "Folder"
        @output.println @formatter.formatFolder(@elements.last)

      when "Seperator"
        @output.println @formatter.formatSeperator(@elements.last)

      when "Url"
        @output.println @formatter.formatUrl(@elements.last)

      when "Folderend"
        @output.println @formatter.formatFolderend(@elements.last)
        @statistics.decFolder

      when "Headerend"
        @output.println @formatter.getHeader

      end

      @state.set($NOSTATE)

      return @elements.pop
    end
  end


  ## a debug dump of the current stack state
  def debugstate
    return "DEBUG Bookmarkstack: size=[#{size}], " +
          "top.class=[#{top.class}], " +
          "empty?=[#{empty?}], " +
          "@elements.empty?=[#{@elements.empty?}]"
  end

end# class Bookmarkstack




## -------------------------------------------------------------------
## purpose: data object to store all informations of a bookmark-folder
## -------------------------------------------------------------------
class Folder

  attr_accessor :name              ## name of the folder ("Homepage of a friend of mine")
  attr_accessor :depth             ## depth of this folder in the folder-hierarchy
  attr_accessor :id                ## id of the folder (only needed in a few browsers)
  attr_accessor :createdtimestamp  ## timestamp in unix-format, when the folder was created (1080161248)
  attr_accessor :lastmodifiedtimestamp ## timestamp in unix-format, when the entry was modified for the last time (1080161248)
  attr_accessor :expanded          ## true|false if folder is to be shown expanded or not
  attr_accessor :ispersonalfolder    ## true|false if folder is the personal toolbar folder
  attr_accessor :description       ## description of the folder (multiline)
  attr_accessor :inpersonalfolder      ## true|false if on personal toolbar
  attr_accessor :personalfolderposition  ## number of position in current folder

  def initialize()
    @name=""             
    @depth=0
    @id=""               
    @createdtimestamp="" 
    @lastmodifiedtimestamp=""
    @ispersonalfolder=false
    @expanded=""         
    @description=""      
    @inpersonalfolder=false
    @personalfolderposition=-1
  end

end# class Folder



## -------------------------------------------------------------------
## purpose: data object to store all informations of an URL
## -------------------------------------------------------------------
class Url

  attr_accessor :name                  ## name of the URL ("Homepage of a friend of mine")
  attr_accessor :url                   ## URL of the URL ;-)
  attr_accessor :shortcuturl           ## short name of the URL ("friendhp")
  attr_accessor :nickname              ## nickname of the URL ("friend")
  attr_accessor :id                    ## id of the URL (only needed in a few browsers)
  attr_accessor :createdtimestamp      ## timestamp in unix-format, when the entry was created (1080161248)
  attr_accessor :description           ## description of the URL (multiline)
  attr_accessor :lastvisitedtimestamp  ## timestamp in unix-format, when the entry was visited for the last time (1080161248)
  attr_accessor :lastmodifiedtimestamp ## timestamp in unix-format, when the entry was modified for the last time (1080161248)
  attr_accessor :icon                  ## URL of a favicon ("http://heise.de/favicon.ico")
  attr_accessor :charset               ## ("ISO-8859-1")
  attr_accessor :depth                 ## depth of the folder
  attr_accessor :inpersonalfolder      ## true|false if on personal toolbar
  attr_accessor :personalfolderposition  ## number of position in current folder

  def initialize()
    @name=""                  
    @url=""                   
    @shortcuturl=""           
    @nickname=""              
    @id=""                    
    @createdtimestamp=""      
    @description=""           
    @lastvisitedtimestamp=""  
    @lastmodifiedtimestamp="" 
    @icon=""                  
    @charset=""               
    @depth=0
    @inpersonalfolder=false
    @personalfolderposition=-1
  end

end# class Url




## -------------------------------------------------------------------
## purpose: data object to store the information of a folder-end
## -------------------------------------------------------------------
class Folderend

  attr_accessor :depth             ## depth of this folder in the folder-hierarchy

  def initialize()
    @depth=0
  end

end# class Folderend



## -------------------------------------------------------------------
## purpose: data object to store the information of a header-end
## -------------------------------------------------------------------
class Headerend

  def initialize()
    # ignore
  end

end# class Headerend



## -------------------------------------------------------------------
## purpose: data object to store the information of a seperator
## -------------------------------------------------------------------
class Seperator

  attr_accessor :depth             ## depth of this folder in the folder-hierarchy

  def initialize()
    @depth=0
  end

end# class Seperator





## -------------------------------------------------------------------
## purpose: handels the writing of the output file, debug output, ...
## -------------------------------------------------------------------
class Outputwriter

  def initialize(filename)
    @outputfile = File.open(filename,"w")
  end

  ## prints out using the correct indentation
  def println(string)
    return if string == nil
    raise "Outputwriter.println(#{string.to_s}) ERROR: parameter is not a string" if string.class != String
    @outputfile.print(string)
##    offsetwhitespace = "    " * $folderdepht
##    @outputfile.print(offsetwhitespace + string.sub(/\n/, "\n"+offsetwhitespace)+"\n")
  end

  ## prints out to the user interface (stdout)
  def screen(string)
    raise "Outputwriter.println(#{string.to_s}) ERROR: parameter is not a string" if string.class != String
    puts string
  end

  ## prints out debug infos
  def debug(string)
    raise "Outputwriter.println(#{string.to_s}) ERROR: parameter is not a string" if string.class != String
    puts string
##    offsetwhitespace = "    " * $folderdepht
##    @outputfile.print(offsetwhitespace + "DEBUG: " + string + "\n")
  end

end#class Outputwriter





## -------------------------------------------------------------------
## purpose: collect statistical informations
## -------------------------------------------------------------------
class Statistics

  attr_reader :folderdepth
  attr_reader :numofurls
  attr_reader :numoffolders

  def initialize()
    @numofurls=0
    @numoffolders=0
    @folderdepth=0
  end

  ## increments number of URLs
  def incUrl()
    @numofurls+=1
  end

  ## increments number of folders
  def incFolder()
    @numoffolders+=1
    @folderdepth+=1
  end

  ## decrements number of folders
  def decFolder()
    @folderdepth-=1
  end

end#class Statistics





## -------------------------------------------------------------------
## purpose: final state machine for not getting lost when parsing *g*
## note: before switching to a specific (non-$NONE) state, you have to
##       switch back to $NONE
## -------------------------------------------------------------------
class Status

  ## the states of the final state machine
  $NOSTATE=0
  $HEADERSTATE=1
  $FOLDERSTATE=2
  $URLSTATE=3

  def initialize(startingstate)
    @currentstate=startingstate
  end

  def set(value)
    ## allow status transitions only from or to $NOSTATE
    raise "ERROR: Status.set(#{value}) called but status was already in #{@currentstate}" if ( @currentstate != $NOSTATE && value != $NOSTATE )
    #$output.debug "<!-- setStatus(#{value}) -->" 
    @currentstate=value
  end

  def get()
    return @currentstate
  end

end# class Statistics





## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------
## ------------------------------------------------------------------------------------------------------


## BEGIN lists of supported formats ----------------------------------------

## NOTE: index of FORMAT and corrspondend HANDLER must be the same!
$OUTFORMATNAMES=["FireFox0.8","OperaHotlist2"]
$OUTFORMATHANDLER=[FireFox08Formatter,OperaHotlist2Formatter]

## NOTE: index of FORMAT and corrspondend HANDLER must be the same!
$INFORMATNAMES=["FireFox0.8","OperaHotlist2"]
$INFORMATHANDLER=[FireFox08Parser,OperaHotlist2Parser]

## END lists of supported formats ----------------------------------------


parseArgs(0, "(informat&outformat&input&output)|version|help|HT", nil, "informat:", "outformat:", "input:", "output:", "version", "help", "HT:")


if ($OPT_version||$OPT_help)

  ## show only help
  eval($USAGE)
  exit

elsif ($OPT_HT == "informat")
  eval($HTINFORMAT)
  exit

elsif ($OPT_HT == "outformat")
  eval($HTOUTFORMAT)
  exit

else
  ## check parameters
  #FIXXME: check if input file exists
  #FIXXME: check if output file does not exist

  outformatindexnumber = $OUTFORMATNAMES.index($OPT_outformat)

  if outformatindexnumber != nil
    currentformatter = $OUTFORMATHANDLER[outformatindexnumber].new()
    ## FIXXME: ensure, that currentformatter inherits from Formatter
  else
    puts "Sorry, the output file format \"#{$OPT_outformat}\" is not (yet) supported.\n(Are you interested in writing it?)"
    exit
  end    

  ## the output handling
  $output = Outputwriter.new($OPT_output)

  ## the status final state machine
  status = Status.new($HEADERSTATE)

  ## collects statistical informations
  bmstats=Statistics.new()

  ## the bookmark-stack
  bmstack=Bookmarkstack.new(999999, bmstats, status, currentformatter, $output)

  informatindexnumber = $INFORMATNAMES.index($OPT_informat)

  if informatindexnumber != nil
    $output.screen "begin parsing of \"#{$OPT_input}\"... (please be patient)\n\nNumber of URLs converted:"
    currentparser = $INFORMATHANDLER[informatindexnumber].new(bmstack, status).parse($OPT_input)
    ## FIXXME: ensure, that currentparser inherits from parser
  else
    puts "Sorry, the input file format \"#{$OPT_informat}\" is not (yet) supported.\n(Are you interested in writing it?)"
    exit
  end    

  $output.screen "\nNumber of folders: "+bmstats.numoffolders.to_s+"\n\nBookmark conversion successfully finished."

end


## ------------------------------------------------------------------------------------------------------
##end
