LJ Inline Expander

By Ilya Dogolazky Last update Mar 3, 2008 — Installed 1,997 times. Daily Installs: 0, 2, 1, 2, 0, 1, 2, 2, 2, 1, 0, 1, 1, 0, 0, 0, 3, 0, 3, 2, 2, 1, 0, 3, 0, 2, 4, 1, 1, 0, 2, 0

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// This JavaScript file is automatically generated by js-tool.
// Do NOT read it! Do NOT edit it! Edit the source code instead.


// LJ Inline Expander 0.2.10 (beta) 2008-03-03
// ------------------------------------------
// Copyright (c) 2007-2008 Ilya Dogolazky
// Released under the GPL license, see http://www.gnu.org/copyleft/gpl.html for details
// ------------------------------------------
// ==UserScript==
// @name           LJ Inline Expander
// @namespace      http://www.math.uni-bonn.de/people/ilyad/lj/expander
// @description    Makes it possible to open lj-cuts, read comments, and unfold the comments threads directly on the friends list page.
// @include        http://*.livejournal.com/*
// ==/UserScript==

// use Dumper ; // This module can not be redistributed :-(
function Dumper(a) {return a;}

// File Debug.js
Debug.level = function(l) // 0:nothing, 1:errors only 2: errors and warnings 3:all messages
{
  Debug.current_level = l ;
}

// oops! GM_log accepts only one parameter!
// http://wiki.greasespot.net/index.php?title=GM_log&diff=1764&oldid=1762 :-(
Debug.gm = function(level, cl, msg)
{
  if(cl>level)
  {
    var ar = [] ;
    for(var i=0; i<msg.length; ++i)
      ar.push(msg[i]) ;
    GM_log(ar.join(""), 2-level) ;
  }
}

Debug.level(3) ;

Debug.log = function() { Debug.gm(2, Debug.current_level, arguments) ; }
Debug.warn = function() { Debug.gm(1, Debug.current_level, arguments) ; }
Debug.error = function() { Debug.gm(0, Debug.current_level, arguments) ; }

Debug.prototype.log = function() { this.log_member(2, arguments) ; }
Debug.prototype.warn = function() { this.log_member(1, arguments) ; }
Debug.prototype.error = function() { this.log_member(0, arguments) ; }

Debug.prototype.log_member = function(level, msg)
{
  Debug.gm(level, this.enabled(), msg) ;
}

Debug.prototype.enabled = function()
{
  if(this.absolute)
    return this.current_level ;
  else
    return Math.min(this.current_level, Debug.current_level) ;
}

function Debug(level)
{
  this.absolute = (level<0) ;
  if(this.absolute)
    level = -level ;
  this.current_level = level ;
}

// vim:tw=0:smartindent

// End of file Debug.js
// File Dialog.js
// use Debug: already included

Dialog.list = {} ;

Dialog.register = function(attr, init_cb, handler_cb)
{
  if(Dialog.list[attr]!=null)
    Debug.error("Dialog '", attr, "' already exists") ;
  Dialog.list[attr] = new Dialog(attr, init_cb, handler_cb) ;
  return Dialog.list[attr] ;
}

Dialog.prototype.display = function()
{
  // weird bug?
  try
  {
    var by_attr = Xpath.aNode([this.attr,"iframe"]) ;
  }
  catch(e)
  {
    Debug.error("oops! can't call Xpath.aNode([", this.attr, "])") ;
  }
  if(this.ifrm!=by_attr)
  {
    Debug.warning("by_attr != this.ifrm, fixing...") ;
    this.ifrm = by_attr ;
  }

  if(this.ifrm.visibility=="visible")
    return ;

  this.init(this) ;
  this.form.innerHTML = this.html ;
  for each(var button in Xpath.aList([this.actn,"*"], this.form))
    button.addEventListener("click", this, false) ;
  this.ifrm.style.visibility = "visible" ;
}

Dialog.prototype.hide = function()
{
  if(this.ifrm.style.visibility=="hidden")
    return ;
  this.ifrm.style.visibility="hidden" ;
  this.form.innerHTML = this.html = "" ;
}

Dialog.prototype.checkbox = function(o_name, label)
{
  var checked = (GM_getValue(o_name)=="yes") ? "checked" : null ;
  return this.tag("label") +
    this.tag("input", [this.gmvl,o_name, "type","checkbox", checked,null]) +
    label +
    this.tag("/label") ;
}
Dialog.prototype.radio = function(o_name, value, label)
{
  var checked = (GM_getValue(o_name)==value) ? "checked" : null ;
  if(label==null) label = value ;
  return this.tag("label") +
    this.tag("input", [this.gmvl,o_name, "name",o_name, "type","radio", "value",value, checked,null]) +
    label +
    this.tag("/label") ;
}

Dialog.prototype.text = function(o_name, cols, rows)
{
  if(rows==null) rows = 1 ;
  if(cols==null) cols = 2 ;
  return (rows==1) ? this.textLine(o_name, cols) : this.textArea(o_name, cols, rows) ;
}

Dialog.prototype.button = function(text, action)
{
  return this.tag("input", [this.actn,action, "type","button", "value",text]) ;
}

Dialog.prototype.textArea = function(o_name, cols, rows) // private member, don't use it :-)
{
  return this.tag("textarea",[this.gmvl,o_name, "rows",rows, "cols",cols]) +
    GM_getValue(o_name) +
    this.tag("/textarea") ;
}

Dialog.prototype.textLine = function(o_name, cols) // private member, don't use it :-)
{
  var old_value = GM_getValue(o_name) ;
  return this.tag("input",[this.gmvl,o_name, "type","text", "cols",cols, "value",old_value]) ;
}

Dialog.prototype.attrList = function(arg) // private member, don't use it :-)
{
  var res = "" ;
  while(arg.length)
  {
    var key = arg.shift() ;
    var value = arg.shift() ;
    if(key==null) continue ;
    res += ' ' + key ;
    if(value!=null)
      res += '="' + value + '"'
    res += ' ' ;
  }
  return res ;
}

Dialog.prototype.tag = function(name, arg) // private member, don't use it :-)
{
  if(arg==null) arg=[] ;
  return "<" + name.toUpperCase() + " " + this.attrList(arg) + ">" ;
} ;

Dialog.prototype.gm_save = function()
{
  for each(var ctl in Xpath.aList([this.gmvl], this.form))
  {
    var tag = ctl.nodeName.toLowerCase() ;
    if(tag=="input" && ctl.type=="checkbox")
      GM_setValue(ctl.getAttribute(this.gmvl), (ctl.checked)?"yes":"no") ;
    else if(tag=="input" && ctl.type=="radio")
    {
      if(ctl.checked)
        GM_setValue(ctl.getAttribute(this.gmvl), ctl.value+"") ;
    }
    else if((tag=="input" && ctl.type=="text") || tag=="textarea")
      GM_setValue(ctl.getAttribute(this.gmvl), ctl.value) ;
    else
      alert("Unknown input element: tag=" + tag + " value=" + ctl.value + " type=" + ctl.type) ;
  }
}

Dialog.prototype.handleEvent = function(event)
{
  var button = event.currentTarget ;
  var res = this.hdlr(this, button.getAttribute(this.actn), event) ;
  if(res)
    this.hide() ;
}

Dialog.show = function(attr, flag)
{
  if(!Dialog.list[attr])
    alert("Dialog '"+attr+"' doesn't exist") ;
  if(flag)
    Dialog.list[attr].display(attr) ;
  else
    Dialog.list[attr].hide(attr) ;
} ;

function Dialog(attr, init_cb, handler_cb)
{
  this.attr = attr ;
  this.init = init_cb ;
  this.hdlr = handler_cb ;

  this.actn = attr + '-action' ;
  this.gmvl = attr + '-gmvlue' ;

  this.form = null ;
  this.box = null ;
  this.html = "" ; // it's a public member

  this.ifrm = document.createElement("iframe") ;
  this.ifrm.style.visibility="hidden" ;
  Debug.log("irfame created") ;

  this.box = document.createElement("div") ;
  this.form = document.createElement("form") ;

  var that = this ;
  this.ifrm.addEventListener("load", function() {
    this.contentDocument.body.innerHTML = '<p style="visibility: hidden;">Empty page (about:blank)</p>' ;
    Debug.log("about:blank loaded") ;
    this.contentDocument.body.insertBefore(that.box, null) ;
    Debug.log("box inserted") ;
    this.contentDocument.body.insertBefore(that.form, null) ;
    Debug.log("form inserted") ;
  }, false);

  this.ifrm.src = "about:blank" ;
  document.body.appendChild(this.ifrm) ;


  this.ifrm.style.position="fixed" ;
  this.ifrm.style.zIndex = 239 ;
  this.ifrm.style.top = "0%" ;
  this.ifrm.style.left = "0%" ;
  this.ifrm.style.right = "0%" ;
  this.ifrm.style.bottom = "0%" ;
  this.ifrm.style.border = "0px solid" ;
  this.ifrm.setAttribute(this.attr, "iframe") ;

  this.box.style.position = "absolute" ;
  this.box.style.background = "black" ;
  this.box.style.top = "0%" ;
  this.box.style.left = "0%" ;
  this.box.style.right = "0%" ;
  this.box.style.bottom = "0%" ;
  this.box.style.border = "solid 0px" ;
  this.box.style.MozOpacity = "0.2" ;
  this.box.style.opacity = "0.2" ; // CSS-3 ready!

  this.form.style.position = "absolute" ;
  this.form.style.overflow = "auto" ;
  this.form.style.background = "wheat" ;
  this.form.style.top = "3%" ;
  this.form.style.left = "3%" ;
  this.form.style.right = "3%" ;
  this.form.style.bottom = "3%" ;
  this.form.style.border = "double 3px" ;
  this.form.style.padding = "4px" ;

}

Dialog.prototype.style = function()
{
  return this.form.style ;
}


// vim:tw=0:smartindent


// End of file Dialog.js
// File Dom.js
// given two elements 'a' and 'b'; and a common major 'top'
// (it means a<=top and b<=top)
// find minimal common major (supremum):
// sup = min{x|a<=x, b<=x}
function sup(a,b,top)
{
  var pa=path_to_ancestor(a,top), pb=path_to_ancestor(b,top) ;
  // GM_log("pa="+pa+" pb="+pb) ;
  var last_eq ;
  while(pa.length>0 && pb.length>0 && pa[0]==pb[0])
  {
    last_eq = pa[0] ;
    pa.shift() ;
    pb.shift() ;
  }
  return last_eq ;
}

function path_to_ancestor(a, top)
{
  var res = [] ;
  while(a!=top)
  {
    // GM_log("a="+a+" top"+top) ;
    res.unshift(a) ;
    a = a.parentNode ;
  }
  res.unshift(top) ;
  return res ;
}

function is_descendant(that, node)
{
  for(var x=that; x; x=x.parentNode)
    if(x==node)
      return true ;
  return false ;
}

function nbsp_html(count)
{
  if(count==null)
    count = 1 ;
  html = "" ;
  for(var i=0; i<count; ++i)
    html += "&nbsp;" ;
  return html ;
}

function nbsp_node(count)
{
  var span = document.createElement("span") ;
  span.innerHTML = nbsp_html(count) ;
  return span ;
}

// End of file Dom.js
// File GM_huge.js
GM_huge.setValue = function (option, value)
{
  GM_setValue(option, "xxx"+value) ;
}

GM_huge.getValue =function (option)
{
  var v = GM_getValue(option) ;
  if(v==null)
    return null ;
  return v.match(/^xxx(.*)/) [1] ;
}

function GM_huge()
{
}

// End of file GM_huge.js
// File Lj.js
// use Debug: already included
// File Xpath.js
Xpath.list = function(xpath, root, order)
{
  if(!root)
    root = window.document ;
  var result = [] ;
  var snapshot = document.evaluate(xpath, root, null, (order ? XPathResult.ORDERED_NODE_SNAPSHOT_TYPE : XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE), null) ;
  for(var i=0; i<snapshot.snapshotLength; ++i)
    result.push(snapshot.snapshotItem(i)) ;
  return result ;
}

Xpath.node = function(xpath, root)
{
  return Xpath.list(xpath, root, false)[0] ;
}

Xpath.aNode = function (hash, root)
{
  var list = Xpath.aList(hash, root) ;
  if(list.length==0)
    return null ;
  if(list.length>1)
    GM_log("Multiply nodes found in Xpath.aNode("+hash+")") ;
  return list[0] ;
}

Xpath.aList = function(list, root, order)
{
  var xpath = Xpath.aXpath(list) ;
  return Xpath.list(xpath, root, order) ;
}

Xpath.aXpath = function (list)
{
  var s = [] ;
  while(list.length>0)
  {
    var key = list.shift() ;
    var value = list.shift() ;
    var at = "@" + key ;
    s.push((value==null || value=="*") ? at : at+"='" + value + "'") ;
  }
  return "descendant::*["+s.join(" and ")+"]" ;
}

function Xpath()
{}

// End of file Xpath.js
// File Uri.js
function Uri(str)
{
  var t = str.match(/^(.*?)#(.*)$/) ;
  if(t)
  {
    str = t[1] ;
    this.fragment = t[2] ;
  }
  this.location = str ;
  t = str.match(/^(.*?)\?(.*)$/) ;
  if(t)
  {
    str = t[1] ;
    var q = t[2] ;
  }
  this.path = str ;
  this.query = new Object ;
  if(q)
  {
    for each(var qq in q.split('&'))
    {
      var i = qq.search("=") ;
      if(i==-1)
      {
        var key = qq ;
        var value = undefined
      }
      else
      {
        var key = qq.substr(0,i) ;
        var value = qq.substr(i+1) ;
      }
      this.query[key] = value ;
    }
  }
}

Uri.prototype.originalLocation = function()
{
  return this.location ;
}

Uri.prototype.toString = function()
{
  var res = this.path ;
  var qq = [] ;
  for(var key in this.query)
  {
    var q = key+'' ;
    var value = this.query[key] ;
    if(value!=undefined)
      q += '=' + value ;
    qq.push(q)
  }
  if(qq.length)
    res += "?" + qq.join("&") ;
  if(this.fragment)
    res += "#" + this.fragment ;
  return res ;
}

if(new Debug(0).enabled())
{
  var u = new Uri(window.location.toString()) ;
  alert(Dumper(u)+"\n----\n"+u.toString()) ;
}

// End of file Uri.js

// Searches for the LJ entry containing given document node
function find_lj_entry(node)
{
  const xp_list =
  [
    'ancestor::*[@class="post"]',
    'ancestor::*[@class="entry"]',
    'ancestor::*[@class="entrybox"]',
    'ancestor::*[@class="entryHolder"]',
    'ancestor::*[@class="comment_wrapper"]',
    'ancestor::*[name()="DIV" or name()="TD"][1]'
  ] ;
  for each(xp in xp_list)
  {
    var entry = Xpath.list(xp, node, true) ;
    if(entry.length==0)
      continue ;
    if(entry.length>1)
      GM_log("Oops, many entry nodes found in "+xp) ;
    return entry[0] ;
  }
  return null ;
}

// Searches for the comments document node of given LJ entry
function find_lj_comments(entry)
{
  const comments_xpath = 'descendant::*[@class="comments" or @class="entry-footer"]' ;
  var comments_node = Xpath.node(comments_xpath, entry);
  return comments_node ;
}

function parent_of_cut(link)
{
  if(link==null || !link.href.match(/#cutid\d+$/))
    return null ;
  var parent=link.parentNode ;
  if(!parent)
    return null ;
  if(parent.nodeName.toUpperCase()!="B")
    return null ;
  if(!parent.innerHTML.match(/^\(/))
    return null ;
  if(!parent.innerHTML.match(/\)$/))
    return null ;
  return parent ;
}

function count_comments_pages(text)
{
  var page = document.createElement('span') ;
  page.innerHTML = text ;
  var max_x = 0 ;
  const links_xpath = 'descendant::a[@href]' ;
  var links = document.evaluate(links_xpath,page,null,XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,null) ;
  // GM_log("links.snapshotLength="+links.snapshotLength+" length(text)="+text.length) ;
  for(var i=0; i<links.snapshotLength; ++i)
  {
    var href = links.snapshotItem(i).href ;
    var re = '/\\d+\\.html\\?format=light\\&page=(\\d+)#comments$' ;
    var a = href.match(re) ;
    if(a && links.snapshotItem(i).innerHTML=="["+a[1]+"]")
    {
      // GM_log(href+"..."+a+"-----"+(a?a[1]:"noprop")+links.snapshotItem(i).innerHTML) ;
      var x = a[1]-0 ; // I hope, "-0" will cause converting to int
      if(max_x<x)
        max_x=x ;
    }
  }
  // GM_log("max_x="+max_x)
  return max_x ;
}

// Extracts journal's name and the entry id from the entry URL
Lj.extract_id = function(url)
{
  var reg_exps = [
    "^http://community.[^/]+/([^/]+)/(\\d+)\\.html$",
    "^http://syndicated.[^/]+/([^/]+)/(\\d+)\\.html$",
    "^http://users.[^/]+/([^/]+)/(\\d+)\\.html$",
    "^http://[^/]+/users/([^/]+)/(\\d+)\\.html$",
    "^http://[^/]+/community/([^/]+)/(\\d+)\\.html$",
    "^http://(.*?)\\.[^/]+/(\\d+)\\.html$"
  ] ;
  for each(var re in reg_exps)
  {
    var a = url.match(re) ;
    if(a)
      return a ;
  }
}

function find_all_entries()
{
  if(GM_getValue(UO_ADD_TO_ENTRIES)!="yes")
    return ;
  var comment_boxes_xp = 'descendant::*[@class="comments" or @class="entry-footer" or @class="entrylinks" or @class="asset-meta" or @class="Comment" or @class="short_entry" or @class="full_entry" or @class="index"]' ;
  for each(var that in Xpath.list(comment_boxes_xp))
  {
    // var that = cn[i] ;
    var entry = find_lj_entry(that) ;
    var links = Xpath.list("descendant::*[@href]", that, true) ;
    // UGLY HACK :-( needed for some weird styles, like that of ru-linux.livejournal.com
    if(that.nodeName.toUpperCase()=="A" && that.href && that.getAttribute("class")=="comments")
      links.push(that) ;
    var found = false ;
    for(var j=0; !found && j<links.length; ++j)
    {
      var link = links[j] ;
      var entry_url = extract_entry_url(link.href) ;
      if(entry_url)
      {
        found = true ;
        var cntrl = create_control(V_COMMENTS, entry_url, 0) ;
        var ancestor = link.parentNode ;
        ancestor.insertBefore(nbsp_node(), link) ;
        ancestor.insertBefore(cntrl, link) ;
        // var bebe = new Bebe() ;
        // ancestor.insertBefore(bebe.node(), link) ;
        // bebe.e.addEventListener("click", bebe, false) ;
        var span = document.createElement('span') ;
        span.innerHTML = nbsp_html() + "|" + nbsp_html() ;
        ancestor.insertBefore(span, link) ;

        // GM_log("entry: "+entry.nodeName.toUpperCase()+entry.innerHTML) ;
        // GM_log("that : "+that .nodeName.toUpperCase()+ that.innerHTML) ;

        var anchor ;
        if(entry.parentNode.nodeName.toUpperCase()=="TR")
          var anchor = that ;
        else if(entry.parentNode.nodeName.toUpperCase()=="DIV")
          var anchor = entry ;
        else if(entry.parentNode.nodeName.toUpperCase()=="P")
          var anchor = entry ;
        else
          document.body.innerHTML = "<b>Ooops: entry.parentNode is a "+entry.parentNode.nodeName+"<br/>entry_url="+entry_url+"</b>" ;

        anchor.setAttribute(A_ANCHOR, 'yes') ;
        anchor.setAttribute(A_URL, entry_url) ;
        anchor.setAttribute(A_CUT, 0) ;

        update_state(V_NOBOX, entry_url, 0) ;
        var x=new Debug(0) ;
        if(x.enabled())
        {
          anchor.style.background = "blue" ; // debugging
          entry.style.background = "yellow" ;
          that.style.background = "green" ;
          var boxik = document.createElement("blockquote") ;
          boxik.style.padding = "20px" ;
          boxik.style.border = "1px solid rgb(255,0,255)" ;
          boxik.innerHTML = "<b>ai karamba: "+entry.parentNode.nodeName+"<BR>"+entry_url+"</b>" ;
          anchor.parentNode.insertBefore(boxik, anchor.nextSibling) ;
        }
      }
    }
  }
}

Lj.colorize = function()
{
  var found = false ;
  Debug.log("colorize") ;
  for each(var st in Lj.styles)
  {
    for each(var entry in Xpath.list(st.entryXpath))
    {
      found = true ;
      var user = Xpath.node(st.userXpath, entry) ;
      var text = Xpath.node(st.textXpath, entry) ;
      var tags = Xpath.list(st.tagXpath,  entry) ;
      if(text) text.style.background = "green" ;
      if(user) user.parentNode.style.background = "red" ;
      if(tags.length) for each(var t in tags) t.parentNode.style.background = "blue" ;
      Debug.log(user, text, tags) ;
      entry.style.background = "wheat" ;
    }
    if(found)
    {
      Debug.log("Style found! ", st.name) ;
      break ;
    }
  }
}

Lj.styles = [

      // Nautical Generator - checked (dimrub)
      {
          name: 'Nautical Generator',
          entryXpath: '//table[@class=\'entrybox\']',
          userXpath: './/a[@class = \"index\"]//font/text()',
          tagXpath: './/a[@rel=\'tag\']/text()',
          textXpath: './/td[@bgcolor="#ffffff"]',
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry.nextSibling);
              remember(par, entry);
          }
      },

      // Punquin Elegant
      {
          name: 'Punquin Elegant',
          entryXpath: '//table[@class=\'entry\']',
          userXpath: './/a[1]/text()',
          tagXpath: './/a[@rel=\'tag\']/text()',
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry.nextSibling);
              remember(par, entry);
          }
      },

      // Martial Blue - checked (lev_m)
      {
          name: 'Martial Blue',
          entryXpath: "//div[@class='box']",
          userXpath: ".//span[@class = 'ljuser']/a[2]/text()",
          tagXpath: ".//div[@class='ljtags']//a[@rel=\'tag\']/text()",
          textXpath: "./div[@class='entry']",
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry);
          }
      },

      // A Sturdy Gesture
      {
          name: 'A Sturdy Gesture',
          entryXpath: "//div[@class='box']",
          userXpath: ".//div[@class='entry']//a[2]/text()",
          tagXpath: ".//div[@class='entry']//a[@rel=\'tag\']/text()",
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry);
          }
      },

      // Smooth Sailing - checked (annyway)
      {
          name: 'Smooth Sailing',
          entryXpath: "//div[@class='entryHolder']",
          userXpath: ".//span[@class='ljuser']//b/text()",
          tagXpath: ".//span[@class='entryMetadata-content']/a[contains(@href, 'tag')]/text()",
          textXpath: ".//div[@class='entryText']",
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry);
          }
      },

      // Unearthed - checked (zhuzh)
      {
          name: 'Unearthed',
          entryXpath: "//table[@class='DropShadow']",
          userXpath: ".//span[@class='ljuser']//b/text()",
          tagXpath: ".//div[@class='ljtags']/a[contains(@href, 'tag')]/text()",
          textXpath: ".//td[@class='BoxContents']",
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry);
          }
      },

      // Flexible Squares - checked (sciuro)
      {
          name: 'Flexible Squares',
          entryXpath: "//div[@class='subcontent']",
          userXpath: ".//div[@class='userpicfriends']//a/font/text()",
          tagXpath: ".//a[@rel='tag']/text()",
          textXpath: ".//div[@class='entry_text']",
          removeFunction: function(entry) {
              par = entry.parentNode;
              remember(par, entry);
              remember(par, entry.nextSibling);
              remember(par, entry.nextSibling.nextSibling);
          }
      },

      // Component - checked (benjamin-vn)
      {
          name: 'Component',
          entryXpath: "//table[tbody/tr/td/@class='entryHolderBg']",
          userXpath: ".//span[@class='ljuser']//a/b/text()",
          tagXpath: ".//a[@rel='tag']/text()",
          textXpath: ".//td[@class='entryHolderBg'][3]//td[@class='entry']/div[not(contains(@class, 'entry'))]", //div",
          removeFunction: function(entry) {
              par = entry.parentNode;
              var curEntry = entry;
              remember(par, entry);
              do {
                  curEntry = curEntry.nextSibling;
                  remember(par, curEntry);
              } while (!curEntry.tagName || curEntry.tagName != 'TABLE');
          }
      },
];

function Lj()
{
}

// End of file Lj.js
// use Xpath: already included
// File Update.js
// File Utils.js
// removes one (if any) trailing "\n" from the string
// Schould it remove cntrl-M??
Utils.chomp = function (str)
{
  if(str.length>0 && str.charAt(str.length-1)=="\n")
    return str.substr(0,str.length-1) ;
  else
    return str ;
}

Utils.get_url = function(url, cb)
{
  GM_xmlhttpRequest({method: "GET", url: url, onload: function(xhr) { cb(xhr.responseText); } });
}

Utils.uniqueAttribute = function(script_name)
{
  return script_name.replace(/[^a-zA-Z]/g, "-").toLowerCase() ;
}

function Utils()
{
}

// End of file Utils.js
// use Dialog: already included
// use GM_huge: already included

Update.O_LAST_CHECK_TIME = "time-of-last-update-check" ;
Update.V_INSTALL = "install" ;
Update.V_CANCEL = "cancel" ;
Update.V_PREF = "pref" ;

function Update(name, url, version, version_url)
{
  this.script_name = name ;
  this.script_url = url ;
  this.current_version = version ;
  this.version_url = version_url ;
  this.dialog_name = Utils.uniqueAttribute(name) + "-update-dialog" ;
  this.new_version = null ;
  var d = Dialog.register(this.dialog_name, Update.init_dialog, Update.dialog_callback) ;
  d.Update = this ;
  s=d.style() ;
  s.background = "blue" ;
  s.color = "white" ;
  s.fontSize = "139%" ;
  s.top = "25%" ;
  s.left = "25%" ;
  s.right = "25%" ;
  s.bottom = "25%" ;
  s.border = "double 3px" ;
  s.padding = "14px" ;
}

Update.prototype.check = function(cf, preferences)
{
  // this.interval = interval ;
  this.pref_dialog = preferences ;
  if(cf==0)
    return ;
  cf = cf * 1000 ; // milli!!!-seconds
  var last = GM_huge.getValue(Update.O_LAST_CHECK_TIME) ;
  if(last && (Date.now()-last < cf))
    return ;
  ( function(that) { Utils.get_url(that.version_url, function(text) {
    Debug.log(text) ;
    text = Utils.chomp(text) ;
    var update = that.need_update(text) ;
    if(update==0) // successfully checked
      GM_huge.setValue(Update.O_LAST_CHECK_TIME, Date.now()) ;
    if(update>0) // script should be updated
    {
      that.new_version = text ;
      Dialog.show(that.dialog_name, true) ;
    }
  }) ; })(this) ;
}

Update.prototype.need_update = function(ver)
{
  const re = /^(\d+)\.(\d+)\.(\d+)$/ ;
  var local = this.current_version.match(re) ;
  var remote = ver.match(re) ;
  if(!local || !remote)
    return -1 ; // can't check
  for(var i=1; i<=3; ++i)
  {
    var r = parseInt(remote[i]) ;
    var l = parseInt(local[i]) ;
    if(r > l)
      return 1 ; // new!
    if(r < l)
      return 0 ; // older? wow!!!
  }
  return 0 ; // successfully checked, no new version
}


Update.init_dialog = function(that)
{
  var p = "" ;
  if(that.Update.pref_dialog)
    p = nbsp_html(10) + that.button("Edit preferences", Update.V_PREF) ;
  that.html =
    "<p>" +
    "The version "+that.Update.new_version+" of "+that.Update.script_name+" is now ready for download "+
    "(currently installed is version "+that.Update.current_version+"). "+
    "Install new version?</p>" +
    "<hr>" +
    '<p>' +
    '<center>' +
    that.button("Install now", Update.V_INSTALL) + nbsp_html(10) +
    that.button("Probably later", Update.V_CANCEL) + p ;
    '</center>' + '</p>' +
    "" ;
}

Update.dialog_callback = function(that, action)
{
  if(action==Update.V_INSTALL)
  {
    setTimeout(function(){Dialog.show(that.Update.dialog_name, false);}, 1000) ;
    GM_openInTab(that.Update.script_url) ;
  }
  if(action==Update.V_PREF)
  {
    Dialog.show(that.Update.pref_dialog, true) ;
  }
  GM_huge.setValue(Update.O_LAST_CHECK_TIME, Date.now()) ;
  return true ;
}

// vim:tw=0:smartindent

// End of file Update.js
// use Utils: already included
// use Uri: already included

Debug.level(0) ;

const SCRIPT_NAME="LJ Inline Expander" ;
const VERSION="0.2.10" ;
const VERSION_URL = "http://www.math.uni-bonn.de/people/ilyad/lj/expander/version.txt" ;
const DIRECTORY="http://www.math.uni-bonn.de/people/ilyad/lj/expander" ;
const SCRIPT_URL="http://userscripts.org/scripts/source/16758.user.js" ;

const SCRIPT_PREFIX = Utils.uniqueAttribute(SCRIPT_NAME) ;

const UO_CHECK_FREQUENCY      = "update-check-frequency" ;
const UO_ADD_CUT_CONTROL      = "add-cut-control" ;
const UO_ADD_COMMENT_CONTROL  = "add-comment-control" ;
const UO_ADD_COMMENT_INBOX    = "add-comment-control-inbox" ;
const UO_ADD_TO_ENTRIES       = "add-comment-control-to-postings" ;
const UO_USE_BLINK_HTML       = "use-blink-html-tag" ;
const UO_FORMAT_LIGHT_INBOX   = "use-param-format-inbox" ;
const UO_STYLE_MINE_INBOX     = "use-param-style-inbox" ;

const A_URL               = SCRIPT_PREFIX + "-attribute-url" ;
const A_CUT               = SCRIPT_PREFIX + "-attribute-cut" ;
const A_TODO              = SCRIPT_PREFIX + "-attribute-todo" ;
const A_ANCHOR            = SCRIPT_PREFIX + "-attribute-anchor" ;
const A_BOX               = SCRIPT_PREFIX + "-attribute-box" ;
const A_FOOTER            = SCRIPT_PREFIX + "-attribute-footer" ;
const A_CONTROL           = SCRIPT_PREFIX + "-attribute-control" ;
const A_CTL_STATUS        = SCRIPT_PREFIX + "-attribute-ctl-status" ;
const A_SAVED_LOCATION    = SCRIPT_PREFIX + "-saved-location" ;
const A_PREFERENCES       = SCRIPT_PREFIX + "-preferences" ;
const A_DOWNLOAD_IFRAME   = SCRIPT_PREFIX + "-download-iframe" ;
const A_DOWNLOAD_FORM     = SCRIPT_PREFIX + "-download-form" ;
const A_PARENT_FRAGMENT   = SCRIPT_PREFIX + "-parent-fragment" ;
const A_FOLDED_THREAD     = SCRIPT_PREFIX + "-folded-thread" ;
const A_SHIFT             = SCRIPT_PREFIX + "-shift-unfolded-comments" ;
const A_UNFOLD_URL        = SCRIPT_PREFIX + "-unfold-url" ;
const A_LOADING           = SCRIPT_PREFIX + "-loading" ;
const A_DUMMY             = SCRIPT_PREFIX + "-attribute-dummy" ;

const V_COMMENTS   = "comments" ;
const V_CUT        = "cut" ;
const V_NOBOX      = "nobox" ;
const V_STATUS_ON      = "on" ;
const V_STATUS_OFF     = "off" ;
const V_SAVE = "save preferences" ;
const V_CANCEL = "cancel and reset preferences" ;
const V_EDIT = "edit-preferences" ;
const V_FORM = "preferences form" ;

check_user_options() ;

var pref = Dialog.register(A_PREFERENCES, preferences_init, preferences_callback) ;
GM_registerMenuCommand(SCRIPT_NAME+" Preferences" , function(){Dialog.show(A_PREFERENCES,true);});

var u = new Update(SCRIPT_NAME, SCRIPT_URL, VERSION, VERSION_URL) ;
u.check(GM_getValue(UO_CHECK_FREQUENCY), A_PREFERENCES) ;

main() ;
// Lj.colorize() ;
find_all_entries() ;

unsafeWindow.onbeforeunload = function() {} ; // I do not understand, why do I need it :-(

function main()
{
  var links = Xpath.list('//a[contains(@href , "#cutid")]') ;
  for(var i=0; i<links.length; ++i)
  {
    var that = links[i] ;
    var m = that.href.match(/#cutid(\d+)$/) ;
    if(!m)
      continue ;
    var cutid = m[1] ;
    var entry_url = extract_entry_url(that.href) ;
    if(!entry_url)
      continue ;
    var anchor = parent_of_cut(that) ;
    if(!anchor)
      continue ;
    var ctl1 = create_control(V_CUT, entry_url, cutid) ;
    var ctl2 = create_control(V_COMMENTS, entry_url, cutid) ;
    if(GM_getValue(UO_ADD_CUT_CONTROL)=="yes")
    {
      anchor.insertBefore(ctl1, that) ;
      anchor.insertBefore(nbsp_node(), that) ;
    }
    if(GM_getValue(UO_ADD_COMMENT_CONTROL)=="yes")
    {
      anchor.insertBefore(ctl2, that.nextSibling) ;
      anchor.insertBefore(nbsp_node(), that.nextSibling) ;
    }

    anchor.setAttribute(A_ANCHOR, 'yes') ;
    anchor.setAttribute(A_URL, entry_url) ;
    anchor.setAttribute(A_CUT, cutid) ;
    update_state(V_NOBOX, entry_url, cutid) ;
  }
}



function DOIT(event)
{
  this.removeEventListener("click",DOIT,false) ;
  var entry_url = this.getAttribute(A_URL) ;
  var cut_id = this.getAttribute(A_CUT) ;
  var control = this.getAttribute(A_CONTROL) ;
  var status = this.getAttribute(A_CTL_STATUS) ;
  var todo = (status==V_STATUS_OFF) ? V_NOBOX : control ;
  var box = Xpath.aNode([A_BOX,'*', A_URL,entry_url, A_CUT,cut_id]) ;
  var box_status = box ? box.getAttribute(A_BOX) : V_NOBOX ;

  if(todo==V_NOBOX) // && box
  {
    if(cut_id>0)
    {
      var entry_node = find_lj_entry(this) ;
      unhideChildren(entry_node) ;
    }
    var need_scrolling = is_descendant(this, box) ;
    box.parentNode.removeChild(box) ;
    if(need_scrolling)
    {
      var scroll_to = Xpath.aNode([A_CONTROL,'*', A_URL,entry_url, A_CUT,cut_id]) ;
      var underlyingFoo = scroll_to.wrappedJSObject || scroll_to ;
      underlyingFoo.scrollIntoView() ;
    }
  }
  else
  {
    if(box_status==V_NOBOX)
    {
      box = create_box(entry_url, cut_id, todo) ;
      if(cut_id>0)
        set_header(box, 0, "[ Loading lj-cut"+dots()+"]", false) ;
      var anchor = Xpath.aNode([A_ANCHOR,'*', A_URL,entry_url, A_CUT,cut_id]) ;
      if(cut_id==0 && anchor.getAttribute("class")=="entrybox") // UGLY HACK! needed for S2-Generator
      {
        box.style.textAlign = "left" ;
        box.style.backgroundColor = "white" ;
        box.style.color = "black" ;
      }
      anchor.parentNode.insertBefore(box, anchor.nextSibling) ;
      if(cut_id>0)
      {
        var entry_node = find_lj_entry(this) ;
        var comments_node = find_lj_comments(entry_node) ;
        if(comments_node)
        {
          var supremum = sup(box, comments_node, entry_node) ;
          var p = comments_node ;
          var q = comments_node ;
          while(p && p!=supremum)
          {
            q = p ;
            p = p.parentNode ;
          }
          comments_node = q ;
          // GM_log("new comments_node = " + comments_node.innerHTML) ;
        }
        hideChildren(box, comments_node, entry_node) ;
      }
    }
    if(todo==V_COMMENTS)
    {
      enlarge_box(box, 1) ; // create space for comments
      set_header(box, 1, "[ Loading comments"+dots()+"]", false) ;
    }
    set_loading_attribute(box, box.childNodes[0], true) ;
    ( function(entry_url,cut_id,box,box_status,todo) { Utils.get_url(light(entry_url), function(html_text) {
      if(box_status==V_NOBOX && cut_id>0)
      {
        var cut_text = extract_cut(html_text, cut_id) ;
        // box.style.border = "1px solid rgb(0, 170, 170)" ;
        box.style.paddingTop = "10px" ;
        set_loading_attribute(box, box.childNodes[0], false) ;
        box.childNodes[0].innerHTML = cut_text ;
      }
      Xpath.aNode([A_FOOTER], box).style.display = "" ; // show footer, if hidden
      if(todo=="comments")
      {
        var pages = count_comments_pages(html_text) ;
        // GM_log(pages+" pages counted") ;
        var comments = extract_comments(html_text, entry_url, cut_id, 0) ;
        insert_comments(box, 1, comments, pages>1) ;
        set_loading_attribute(box, box.childNodes[0], false) ; // some magic :-)
        enlarge_box(box, pages) ;
        for(var pg=2; pg<=pages; ++pg)
        {
          //var loading = document.createElement("B") ;
          //loading.innerHTML = "[Loading comments (page #"+pg+")]" ;
          //insert_comments(box, pg, loading, true) ;
          set_loading_attribute(box, box.childNodes[pg], true) ;
          set_header(box, pg, "[ Loading comments (page #"+pg+")"+dots()+"]", false) ;
          ( function(entry_url,box,pg) { Utils.get_url(light(entry_url)+"&page="+pg, function(html_text) {
            var comments_page = extract_comments(html_text, entry_url, cut_id, 0) ;
            insert_comments(box, pg, comments_page, true) ;
          }) ; })(entry_url,box,pg) ;
        }
      }
    }) ; })(entry_url,cut_id,box,box_status,todo) ;
  }
  update_state(todo, entry_url, cut_id) ;
  var x = new Debug(0) ;
  if(x.enabled())
  {
    this.href= "http://www.livejournal.com" ;
  }
  else
    event.preventDefault() ;
  this.addEventListener("click",DOIT,false) ;
}

function find_footer(box)
{
  // XXX probably is'll not work because of race condition
  return box.childNodes[box.childNodes.length-1] ;
}

function create_footer(entry_url, cut_id)
{
  var footer = document.createElement("div") ;
  footer.style.display = "none" ;
  footer.setAttribute(A_FOOTER, "yes") ;
  footer.setAttribute(A_URL, entry_url) ;
  footer.setAttribute(A_CUT, cut_id) ;
  var td_nbsp = "<td>"+nbsp_html()+nbsp_html()+"</td>" ;
  var dummy = "<span "+A_DUMMY+"='yes'></span>"
  var link = "<a href='"+entry_url+"'>Link</a>" ;
  var comm = "<a href='"+entry_url+"?mode=reply'>Leave"+nbsp_html()+"a"+nbsp_html()+"comment</a>" ;
  footer.innerHTML =
    "<table><tbody><tr>" +
      "<td>"+dummy+"</td>" +
      td_nbsp +
      "<td width='50%'><hr /></td>" +
      "<td><b>"+nbsp_html()+link+nbsp_html()+"|"+nbsp_html()+comm+nbsp_html()+"</b></td>" +
      "<td width='50%'><hr /></td>" +
      td_nbsp +
      "<td>"+dummy+"</td>" +
    "</tr></tbody></table>" ;
  var dummy_nodes = Xpath.aList([A_DUMMY], footer) ;
  // GM_log(dummy_nodes) ;
  var ctl = [V_CUT, V_COMMENTS] ;
  for(var i=0; i<2 ; ++i)
  {
    var c = dummy_nodes[i];
    var a = create_control(ctl[i], entry_url, cut_id) ;
    c.parentNode.insertBefore(a, c) ;
    c.parentNode.removeChild(c) ;
  }
  return footer ;
}

function create_control(ctl_type, url, cutid)
{
  var tt = document.createElement("a") ;
  tt.href = url+ (cutid? "#cutid"+cutid: "") ;
  tt.innerHTML = "<tt>xxx</tt>" ;
  // tt.style.cursor = "pointer" ;
  tt.style.textDecoration= "none" ;
  tt.addEventListener("click", DOIT, false) ;
  tt.setAttribute(A_CONTROL, ctl_type) ;
  tt.setAttribute(A_URL, url) ;
  tt.setAttribute(A_CUT, cutid) ;
  if(false)
    tt.style.background = "yellow" ;
  return tt ;
}

function set_box_color(box, color)
{
  box.style.border = "1px solid "+color ;
}

function create_box(entry_url, cut_id, status)
{
  var box = document.createElement('blockquote') ;
  box.style.padding = "20px" ;
  box.style.align = "left" ;
//  box.style.background = "white" ;
  set_box_color(box, "rgb(0,0,255)") ;
  box.setAttribute(A_URL, entry_url) ;
  box.setAttribute(A_CUT, cut_id) ;
  box.setAttribute(A_BOX, status) ;
  var footer = create_footer(entry_url, cut_id) ;
  box.insertBefore(footer, null) ;
  enlarge_box(box, 0) ;
  return box ;
}

function clear_box_page(box, page)
{
  var div = box.childNodes[page] ;
  div.innerHTML = "<div></div><div></div>" ;
}

function enlarge_box(box, max_page)
{
  var N = max_page + 2 - box.childNodes.length ;
  for(var i=0; i<N; ++i)
  {
    var div = document.createElement('div') ;
    box.insertBefore(div, find_footer(box)) ; // before the footer
    clear_box_page(box, box.childNodes.length-2) ;
    // GM_log("enlarge_box: max_page="+max_page+", i="+i+", box contains: "+box.innerHTML) ;
  }
}

function update_state(state, entry_url, cut_id)
{
  var buttons = Xpath.aList([A_CONTROL,'*', A_URL,entry_url, A_CUT,cut_id]) ;
  for each(a in buttons)
  {
    b = a.firstChild ;
    var type = a.getAttribute(A_CONTROL) ;
    var need_comments_control = true ;
    if(state!=V_NOBOX && type==V_COMMENTS)
    {
      var box = Xpath.aNode([A_BOX,'*', A_URL,entry_url, A_CUT,cut_id]) ;
      var is_inbox_control = is_descendant(b, box) ;
      if(is_inbox_control && GM_getValue(UO_ADD_COMMENT_INBOX)!="yes")
        need_comments_control = false ;
    }
    if(state==V_NOBOX)
      b.textContent = (type==V_CUT) ? "+++" : "===" ;
    else if(state==V_CUT)
      b.textContent = (type==V_COMMENTS && need_comments_control) ? "===" : "---" ;
    else
      b.textContent = "---" ;
    a.setAttribute(A_CTL_STATUS, b.textContent=="---" ? V_STATUS_OFF : V_STATUS_ON) ;
    var title ;
    if(type==V_CUT)
      title = "Open lj-cut..." ;
    if(type==V_COMMENTS && cut_id>0)
      title = "Open lj-cut and comments..." ;
    if(cut_id==0)
      title = "Open comments..." ;
    if(b.textContent=="---")
      title = "Close box" ;
    b.title = title ;
  }
}



// 50% --- by the courtesy of lj-user "dadcaptain"
function set_header(box, page, text, hr)
{
  var div = box.childNodes[page].childNodes[0] ;
  if(hr)
    div.innerHTML = "<table width='100%'><tbody><tr><td width='50%'><hr></td><td><B>&nbsp;"+text+"&nbsp;</B></td><td width='50%'><hr></td></tr></tbody></table>"
  else
    div.innerHTML = "<b>" + text + "</b>" ;
}

function insert_comments(box, pageno, node, flag)
{
  var page_box = box.childNodes[pageno] ;
  while(page_box.hasChildNodes())
    page_box.removeChild(page_box.childNodes[0]) ;

  clear_box_page(box, pageno) ;
  const nbsp = "&nbsp;" ;
  set_header(box, pageno, "Comments"+(flag?nbsp+"[page"+nbsp+"#"+ pageno+"]":""), true) ;

  page_box.childNodes[1].insertBefore(node, null) ;
  set_loading_attribute(box, page_box, false)
}

function set_loading_attribute(box, child, yesno)
{
  if(yesno)
  {
    child.setAttribute(A_LOADING,'yes') ;
    set_box_color(box, "rgb(255,0,0)") ;
  }
  else
  {
    child.setAttribute(A_LOADING, 'no') ;
    if(Xpath.aList([A_LOADING, "yes"], box).length == 0)
      set_box_color(box, "rgb(170,170,170)") ;
  }
}

// This function hides recursively all children nodes of the node 'start' standing
// after the node 'ref'. The node 'skip' is skipped (together with all its children)

var FLAG ; // TODO must it be a global variable??? No! No! I'll transform it to some stupid class member
function hideChildren(ref, skip, start)
{
  FLAG = false ;
  hc(ref, skip, start) ;
}

function hc(ref, skip, current)
{
  if(current==ref)
    FLAG = true ;
  if(current==skip || current==ref)
    return ;

  if(FLAG)
  {
    if(current.style)
      current.style.display = 'none' ;
    else
    {
      if(current.parentNode.style.display!="none")
      {
        var span = document.createElement('span') ;
        span.innerHTML = current.textContent ;
        span.style.display = "none" ;
        current.parentNode.insertBefore(span, current) ;
        current.parentNode.removeChild(current) ;
      }
    }
  }

  var children = current.childNodes;
  for(var i=0; i<children.length; ++i)
    hc(ref, skip, children[i]) ;
}

// Recursively deletes the 'none' display style from document nodes

function unhideChildren(node)
{
  if(node.style && node.style.display=="none")
    node.style.display = "" ;
  var children = node.childNodes;
  for(var i=0; i<children.length; ++i)
    unhideChildren(children[i]) ;
}

function extract_cut(text, cut_no)
{
  var c1 = '<a name="cutid'+cut_no +'+"></a>' ;
  var co = "</div><br style='clear: both' /><hr width='100%' size='2' align='center' />" ;
  var pos1 = text.search(c1) ;
  var pos2 = text.search(co) ;
  // GM_log("pos1="+pos1+" pos2="+pos2) ;
  if(pos1==-1 || pos2==-1)
    return "<b>Oops! Can't extract cut #"+cut_no+" from this post.</b>" ;
  // GM_log("cut="+text.substr(pos1+c1.length-1, pos2-pos1-c1.length+1)) ;
  return text.substr(pos1+c1.length-1, pos2-pos1-c1.length+1) ;
}

function extract_comments(text, entry, cutid, delta_shift)
{
  var page = document.createElement('span') ;
  page.innerHTML = text ;
  const comment_xpath = 'descendant::span[@id]' ;
  var comments = document.evaluate(comment_xpath,page,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null) ;
  var result = document.createElement('span') ;
  for(var i=0; i<comments.snapshotLength; ++i)
  {
    var span = comments.snapshotItem(i) ;
    var table = span.firstChild ;
    var id = comment_to_tid(table) ;
    if(!id)
      continue ;
    if(!table.getAttribute("class")) // folded or screend/deleted comment
    {
      table.setAttribute(A_FOLDED_THREAD, id) ;
      // Debug.log("FOLDED ", id) ;
      var subj = Xpath.node("./tbody/tr/td[2]/a",table, true) ;
      if(subj)
      {
        var hyphen = document.createTextNode(" - ") ;
        var unfold = document.createElement("a") ;
        unfold.innerHTML = "Unfold" ;
        var target = new Uri(entry) ;
        target.query.format = "light" ;
        target.query.thread = id ;
        unfold.setAttribute(A_UNFOLD_URL, target.toString()) ;
        unfold.addEventListener("click", unfold_thread, false) ;
        if(GM_getValue(UO_FORMAT_LIGHT_INBOX)!="yes")
          delete target.query.format ;
        if(GM_getValue(UO_STYLE_MINE_INBOX)=="yes")
          target.query.style = "mine" ;
        target.fragment = "t" + id ;
        unfold.href = target ;
        subj.parentNode.insertBefore(unfold, subj.nextSibling) ;
        subj.parentNode.insertBefore(hyphen, unfold) ;
        var img = Xpath.node("./tbody/tr/td/img[@height=1 and @width]", table) ;
        var shift = 0 ;
        if(img)
          shift = parseInt(img.width) ;
        unfold.setAttribute(A_SHIFT, shift+delta_shift) ;
      }
    }
    else // usual comment box --- need to rewrite "link"
    {
      var a = Xpath.node("./tbody/tr[1]/td[2]/font[3]/a[@href]", table) ;
      var target = new Uri(a.href) ;
      if(GM_getValue(UO_FORMAT_LIGHT_INBOX)!="yes")
        delete target.query.format ;
      if(GM_getValue(UO_STYLE_MINE_INBOX)=="yes")
        target.query.style = "mine" ;
      a.href = target.toString() ;
    }
    result.insertBefore(span , null) ;
    var a = document.createElement("a") ;
    a.name = jump_name(entry, cutid, id);
    result.insertBefore(a, span ) ;
    for each(var lnk in Xpath.list("./tbody/tr[2]/td/p/font/a", table))
    {
      var u = new Uri(lnk.href) ;
      var x = new Debug(3) ;
      if(GM_getValue(UO_FORMAT_LIGHT_INBOX)!="yes")
        delete u.query.format ;
      if(GM_getValue(UO_STYLE_MINE_INBOX)=="yes")
        u.query.style = "mine" ;
      lnk.href = u.toString() ;
      var target = lnk.href.match(/#t(\d+)$/) ;
      if(!target)
      {
        if(lnk.href.match("replyto="+id) && x.enabled())
          lnk.innerHTML += " - reply" ;
      }
      else if(target[1]==id)
      {
        if(x.enabled())
          lnk.innerHTML += " - thread " + id ;
        var target = new Uri(entry) ;
        target.query.format = "light" ;
        target.query.thread = id ;
        lnk.setAttribute(A_UNFOLD_URL, target.toString()) ;
        var img = Xpath.node("./tbody/tr/td/img[@height=1 and @width]", table) ;
        var shift = 0 ;
        if(img)
          shift = parseInt(img.width) ;
        lnk.setAttribute(A_SHIFT, shift+delta_shift) ;
        lnk.addEventListener("click", unfold_thread, false) ;
      }
      else
      {
        if(x.enabled())
          lnk.innerHTML += " - parent - " + jump_name(entry, cutid, target[1]) ;
        lnk.setAttribute(A_PARENT_FRAGMENT, jump_name(entry, cutid, target[1])) ;
        lnk.addEventListener("click", jump_to_parent, false) ;
        lnk.title = "Jump to parent" ;
      }
    }

  }
  return result ;
}
function extract_comments_obsolete_2008_03_03(text, entry, cutid, delta_shift)
{
  var page = document.createElement('span') ;
  page.innerHTML = text ;
  const comment_xpath = 'descendant::table' ;
  var comments = document.evaluate(comment_xpath,page,null,XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,null) ;
  var result = document.createElement('span') ;
  for(var i=0; i<comments.snapshotLength; ++i)
  {
    var table = comments.snapshotItem(i) ;
    var id = comment_to_tid(table) ;
    if(!id)
      continue ;
    if(!table.id) // folded or screend/deleted comment
    {
      table.setAttribute(A_FOLDED_THREAD, id) ;
      // Debug.log("FOLDED ", id) ;
      var subj = Xpath.node("./tbody/tr/td[2]/a",table, true) ;
      if(subj)
      {
        var hyphen = document.createTextNode(" - ") ;
        var unfold = document.createElement("a") ;
        unfold.innerHTML = "Unfold" ;
        var target = new Uri(entry) ;
        target.query.format = "light" ;
        target.query.thread = id ;
        unfold.setAttribute(A_UNFOLD_URL, target.toString()) ;
        unfold.addEventListener("click", unfold_thread, false) ;
        if(GM_getValue(UO_FORMAT_LIGHT_INBOX)!="yes")
          delete target.query.format ;
        if(GM_getValue(UO_STYLE_MINE_INBOX)=="yes")
          target.query.style = "mine" ;
        target.fragment = "t" + id ;
        unfold.href = target ;
        subj.parentNode.insertBefore(unfold, subj.nextSibling) ;
        subj.parentNode.insertBefore(hyphen, unfold) ;
        var img = Xpath.node("./tbody/tr/td/img[@height=1 and @width]", table) ;
        var shift = 0 ;
        if(img)
          shift = parseInt(img.width) ;
        unfold.setAttribute(A_SHIFT, shift+delta_shift) ;
      }
    }
    else // usual comment box --- need to rewrite "link"
    {
      var a = Xpath.node("./tbody/tr[1]/td[2]/font[3]/a[@href]", table) ;
      var target = new Uri(a.href) ;
      if(GM_getValue(UO_FORMAT_LIGHT_INBOX)!="yes")
        delete target.query.format ;
      if(GM_getValue(UO_STYLE_MINE_INBOX)=="yes")
        target.query.style = "mine" ;
      a.href = target.toString() ;
    }
    result.insertBefore(table, null) ;
    var a = document.createElement("a") ;
    a.name = jump_name(entry, cutid, id);
    result.insertBefore(a, table) ;
    for each(var lnk in Xpath.list("./tbody/tr[2]/td/p/font/a", table))
    {
      var u = new Uri(lnk.href) ;
      var x = new Debug(3) ;
      if(GM_getValue(UO_FORMAT_LIGHT_INBOX)!="yes")
        delete u.query.format ;
      if(GM_getValue(UO_STYLE_MINE_INBOX)=="yes")
        u.query.style = "mine" ;
      lnk.href = u.toString() ;
      var target = lnk.href.match(/#t(\d+)$/) ;
      if(!target)
      {
        if(lnk.href.match("replyto="+id) && x.enabled())
          lnk.innerHTML += " - reply" ;
      }
      else if(target[1]==id)
      {
        if(x.enabled())
          lnk.innerHTML += " - thread " + id ;
        var target = new Uri(entry) ;
        target.query.format = "light" ;
        target.query.thread = id ;
        lnk.setAttribute(A_UNFOLD_URL, target.toString()) ;
        var img = Xpath.node("./tbody/tr/td/img[@height=1 and @width]", table) ;
        var shift = 0 ;
        if(img)
          shift = parseInt(img.width) ;
        lnk.setAttribute(A_SHIFT, shift+delta_shift) ;
        lnk.addEventListener("click", unfold_thread, false) ;
      }
      else
      {
        if(x.enabled())
          lnk.innerHTML += " - parent - " + jump_name(entry, cutid, target[1]) ;
        lnk.setAttribute(A_PARENT_FRAGMENT, jump_name(entry, cutid, target[1])) ;
        lnk.addEventListener("click", jump_to_parent, false) ;
        lnk.title = "Jump to parent" ;
      }
    }

  }
  return result ;
}

function unfold_thread(event)
{
  event.preventDefault() ;
  Debug.log("unfold_thread handler: ", this.getAttribute(A_UNFOLD_URL)) ;
  var shift = this.getAttribute(A_SHIFT) ;
  var box = Xpath.node("ancestor::*[@"+A_BOX+"]", this) ;
  var entry = box.getAttribute(A_URL) ;
  var cutid = box.getAttribute(A_CUT) ;
  set_loading_attribute(box, this, true) ;
  ( function(box,that,shift,entry,cutid) { Utils.get_url(that.getAttribute(A_UNFOLD_URL), function(html_text) {
    replace_folded_comments(that, html_text, box, shift,entry,cutid) ;
  }) ; }) (box,this, shift,entry,cutid) ;
  event.preventDefault() ;
}

function replace_folded_comments(that, text, box, shift,entry,cutid)
{
  Debug.log("replace_folded_comments") ;
  set_loading_attribute(box, that, false) ;
  var body = extract_comments(text, entry, cutid, parseInt(shift)) ;
  document.body.appendChild(body) ;
  for each(var t in Xpath.list(".//table[@class='talk-comment']", body))
  {
    Debug.log(1) ;
    Debug.log("parentNode: ", t.parentNode.nodeName) ;
    Debug.log("parentNode.id: ", t.parentNode.id) ;
    var id = t.parentNode.id.match(/ljcmt(\d+)/) ;
    if(!id)
      continue ;
    Debug.log(2) ;
    id = id[1] ;
    var folded = Xpath.aNode([A_FOLDED_THREAD, id], box) ;
    if(!folded)
      continue ;
    Debug.log(3) ;
    var img = Xpath.node("./tbody/tr/td/img[@height=1 and @width]", t) ;
    if(img)
      img.width += parseInt(shift) ;
    Debug.log(4) ;
    folded.parentNode.replaceChild(t, folded) ;
    Debug.log(5) ;
  }
}

function jump_to_parent(event)
{
  var fragment = this.getAttribute(A_PARENT_FRAGMENT) ;
  if(fragment)
  {
    var u = new Uri(window.location.toString()) ;
    window.location = u.originalLocation() + "#" + fragment ;
    event.preventDefault() ;
  }
}

function jump_name(entry_url, cutid, thread_id)
{
  var a = Lj.extract_id(entry_url) ;
  var c = (cutid!="0" ? cutid + "-" : "") ;
  return a[1]+"-"+a[2]+"-"+c+"t"+thread_id ;
}


// returns numerical comments ID (as 12345 in <a name="t12345"> tag)
// returns null unless it's a comment
// The 'node' argument should be a <table> tag
function comment_to_tid_obsolete_2008_03_03(node)
{
  // the following nodes are comments:
  // 1) <a name="t1234"></a><table>...</table> --- normal comment
  // 2) <p><a name="t1234></a></p> <table>...</table> --- deleted/screened comment
  var prev = node.previousSibling ;
  if(!prev || !prev.nodeName) return null ;
  var nodename = prev.nodeName.toUpperCase() ;
  if(nodename=="P")
  {
    if(!prev.childNodes || !prev.childNodes[0]) return null ;
    prev = prev.childNodes[0] ;
    if(!prev || !prev.nodeName) return null ;
    nodename = prev.nodeName.toUpperCase() ;
  }
  if(nodename!="A") return null ;
  if(!prev.name) return null ;
  var a = prev.name.match("^t(\\d+)$") ;
  if(!a) return null ;
  return a[1] ;
}

function comment_to_tid(node)
{
  // the following node is a comment:
  // 1) <span id="ljcmt0000000"><table>...</table> 
  var parent = node.parentNode ;
  // Debug.level(4);
  Debug.log(parent.nodeName) ;
  if(parent.nodeName.toLowerCase()!="span")
    return null ;
  var id = parent.getAttribute("id") ;
  Debug.log(node.id) ;
  if(!id)
    return null ;
  var a = id.match("^ljcmt(\\d+)$") ;
  if(!a) return null ;
  return a[1] ;
}

function extract_entry_url(url)
{
  var u = new Uri(url) ;
  return u.path ;
}
function extract_entry_url_obsolete(url)
{
  var a = url.match("^(http://[^/]+/\\d+\\.html)") ;
  // GM_log("url="+url+" a="+a) ;
  if(a) return a[1] ;
  a = url.match("^(http://community.[^/]+/[^/]+/\\d+\\.html)") ;
  if(a) return a[1] ;
  a = url.match("^(http://syndicated.[^/]+/[^/]+/\\d+\\.html)") ;
  if(a) return a[1] ;
  a = url.match("^(http://users.[^/]+/[^/]+/\\d+\\.html)") ;
  if(a) return a[1] ;
}

// Creates an URL for the retrieving of the given LJ posting in "light format"
function light(url)
{
  var u = new Uri(url) ;
  u.query.format = "light" ;
  return u.toString() ;
}
function light_obsolete(url)
{
  var ch ;
  if(url.search(/\?/)==-1)
    ch = '?' ;
  else
    ch ='&' ;
  var a = url.search(/#/) ;
  if(a!=-1)
    url = url.substr(0,a) ;
  // GM_log("returns: "+url + ch + "format=light") ;
  return url + ch + "format=light" ;
}

function dots()
{
  if(GM_getValue(UO_USE_BLINK_HTML)=="yes")
    return "&nbsp;<BLINK>...</BLINK>&nbsp;" ;
  else
    return "&nbsp;...&nbsp;" ;
}

function check_user_options()
{
  if(!GM_getValue(UO_CHECK_FREQUENCY))
    GM_setValue(UO_CHECK_FREQUENCY, 60*60*24+"") ; // Daily
  var opt_yes = [ UO_ADD_CUT_CONTROL, UO_ADD_COMMENT_INBOX, UO_USE_BLINK_HTML, UO_ADD_TO_ENTRIES, UO_FORMAT_LIGHT_INBOX ] ;
  for each(var o in opt_yes)
    if(!GM_getValue(o))
      GM_setValue(o, "yes") ;
  var opt_no = [ UO_ADD_COMMENT_CONTROL, UO_STYLE_MINE_INBOX ] ;
  for each(var oo in opt_no)
    if(!GM_getValue(oo))
      GM_setValue(oo, "no") ;
}

function preferences_init(that)
{
  that.html =
    '<p>'+'<fieldset>'+
    '<legend> Controls in Livejournal friend list </legend>' +
    that.checkbox(UO_ADD_CUT_CONTROL,     "Add <tt><b>+++</b></tt> control opening lj-cuts to every <b>(Read more...)</b> link") + '<br>' +
    that.checkbox(UO_ADD_COMMENT_CONTROL, "Add <tt><b>===</b></tt> control opening lj-cuts and comments to every <b>(Read more...)</b> link ") + '<br>' +
    that.checkbox(UO_ADD_COMMENT_INBOX,   "Add <tt><b>===</b></tt> control opening comments to every opened lj-cut box") + '<br>' +
    that.checkbox(UO_ADD_TO_ENTRIES,      "Add <tt><b>===</b></tt> control to every posting (experimental option)") + '<br>' +
    '</fieldset>'+
    '<fieldset>' +
    '<legend> In-box links </legend>' +
    that.checkbox(UO_FORMAT_LIGHT_INBOX, "Don't remove <tt>format=light</tt> parameter") + '<br>' +
    that.checkbox(UO_STYLE_MINE_INBOX, "Add <tt>style=mine</tt> parameter") + '<br>' +
    '</fieldset>'+
    '<fieldset>'+
    '<legend> Automatically download new versions of '+SCRIPT_NAME+' </legend>' +
    'Check for new version: <br>' +
    nbsp_html() + that.radio(UO_CHECK_FREQUENCY, 0, "Never (inadvisable)") + '<br>' +
    nbsp_html() + that.radio(UO_CHECK_FREQUENCY, 3600*24*14,"Once every fortnight") + '<br>' +
    nbsp_html() + that.radio(UO_CHECK_FREQUENCY, 3600*24, "Daily (recommended)") + '<br>' +
    nbsp_html() + that.radio(UO_CHECK_FREQUENCY, 3600, "Every hour") + '<br>' +
    nbsp_html() + that.radio(UO_CHECK_FREQUENCY, 60, "Every minute (why not?)") + '<br>' +
    '</fieldset>' +
    '<fieldset>' +
    '<legend> Appearance </legend>' +
    that.checkbox(UO_USE_BLINK_HTML, "Use deprecated <tt>&lt;BLINK&gt;</tt> HTML tag during downloads"+dots()) +
    '</fieldset>'+
    '</p><p>' + '<center>' +
    that.button("Save preferences", V_SAVE) + nbsp_html(10) +
    that.button("Cancel", V_CANCEL) + '</center>' + '</p>' +
    "" ;
}

function preferences_callback(that, action)
{
  if(action==V_SAVE)
  {
    that.gm_save() ;
    alert("Preferences saved.\nDon't forget to reload page.") ;
  }
  return true ;
}

// vim:tw=0:smartindent