#@+leo
#@+node:0::@file plugins/mod_read_only_nodes.py
#@+body
"""read only nodes"""

# Contributed by Davide Salomoni <dsalomoni@yahoo.com>

from leoPlugins import *
from leoGlobals import *
import ftplib, urllib, urlparse, os, cStringIO, tkFileDialog
from formatter import AbstractFormatter, DumbWriter
from htmllib import HTMLParser


#@+others
#@+node:1::documentation for @read-only nodes
#@+body
#@+at
#  Dear Leo users,
# 
# Here's my first attempt at customizing leo. I wanted to have the ability to
# import files in "read-only" mode, that is, in a mode where files could only
# be read by leo (not tangled), and also kept in sync with the content on the
# drive.
# 
# The reason for this is for example that I have external programs that generate
# resource files. I want these files to be part of a leo outline, but I don't
# want leo to tangle or in any way modify them. At the same time, I want them
# to be up-to-date in the leo outline.
# 
# So I coded the directive plugin. It has the following characteristics:
# 
# - It reads the specified file and puts it into the node content.
# 
# - If the @read-only directive was in the leo outline already, and the file content
# on disk has changed from what is stored in the outline, it marks the node as
# changed and prints a "changed" message to the log window; if, on the other hand,
# the file content has _not_ changed, the file is simply read and the node is
# not marked as changed.
# 
# - When you write a @read-only directive, the file content is added to the node
# immediately, i.e. as soon as you press Enter (no need to call a menu
# entry to import the content).
# 
# - If you want to refresh/update the content of the file, just edit the headline
# and press Enter. The file is reloaded, and if in the meantime it has changed,
# a "change" message is sent to the log window.
# 
# - The body text of a @read-only file cannot be modified in leo.
# 
# Davide Salomoni

#@-at
#@-body
#@+node:1::ftp/http access
#@+body
#@+at
#  The syntax to access files in @read-only via ftp/http is the following:
# 
# @read-only http://www.ietf.org/rfc/rfc0791.txt
# @read-only ftp://ftp.someserver.org/filepath
# 
# If FTP authentication (username/password) is required, it can be specified 
# as follows:
# 
# @read-only ftp://username:password@ftp.someserver.org/filepath
# 
# For more details, see the doc string for the class FTPurl.

#@-at
#@-body
#@-node:1::ftp/http access
#@-node:1::documentation for @read-only nodes
#@+node:2::class FTPurl
#@+body
class FTPurl:
	"""An FTP wrapper class to store/retrieve files using an FTP URL.

    To create a connection, call the class with the constructor:

        FTPurl(url[, mode])

    The url should have the following syntax:
    
        ftp://[username:password@]remotehost/filename
    
    If username and password are left out, the connection is made using
    username=anonymous and password=realuser@host (for more information,
    see the documentation of module ftplib).
    
    The mode can be '' (default, for ASCII mode) or 'b' (for binary mode).
	This class raises an IOError exception if something goes wrong.
	"""
	

	#@+others
	#@+node:1::__init__
	#@+body
	def __init__(self, ftpURL, mode=''):
		parse = urlparse.urlparse(ftpURL)
		if parse[0] != 'ftp':
			raise IOError, "error reading %s: malformed ftp URL" % ftpURL
	
		# ftp URL; syntax: ftp://[username:password@]hostname/filename
		self.mode = mode
		authIndex = parse[1].find('@')
		if authIndex == -1:
			auth = None
			ftphost = parse[1]
		else:
			auth = parse[1][:authIndex]
			ftphost = parse[1][authIndex+1:]
		self.ftp = ftplib.FTP(ftphost)
		if auth == None:
			self.ftp.login()
		else:
			# the URL has username/password
			pwdIndex = auth.find(':')
			if pwdIndex == -1:
				raise IOError, "error reading %s: malformed ftp URL" % ftpURL
			user = auth[:pwdIndex]
			password = auth[pwdIndex+1:]
			self.ftp.login(user, password)
		self.path = parse[2][1:]
		self.filename = os.path.basename(self.path)
		self.dirname = os.path.dirname(self.path)
		self.isConnectionOpen = 1
		self.currentLine = 0
	
	#@-body
	#@-node:1::__init__
	#@+node:2::Getters
	#@+node:1::read
	#@+body
	def read(self):
		"""Read the filename specified in the constructor and return it as a string.
	    If the constructor specifies no filename, or if the URL ends with '/',
	    return the list of files in the URL directory.
		"""
		self.checkParams()
		if self.filename=='' or self.path[-1]=='/':
			return self.dir()
	
		try:
			if self.mode == '':  # mode='': ASCII mode
				slist = []
				self.ftp.retrlines('RETR %s' % self.path, slist.append)
				s = '\n'.join(slist)
			else: # mode='b': binary mode
				file = cStringIO.StringIO()
				self.ftp.retrbinary('RETR %s' % self.path, file.write)
				s = file.getvalue()
				file.close()
			return s
		except:
			exception, msg, tb = sys.exc_info()
			raise IOError, msg
	
	
	#@-body
	#@-node:1::read
	#@+node:2::readline
	#@+body
	def readline(self):
		"""Read one entire line from the remote file."""
		try:
			self.lst
		except AttributeError:
			self.lst = self.read().splitlines(1)
		
		if self.currentLine < len(self.lst):
			s = self.lst[self.currentLine]
			self.currentLine = self.currentLine + 1
			return s
		else:
			return ''
	
	#@-body
	#@-node:2::readline
	#@-node:2::Getters
	#@+node:3::Setters
	#@+node:1::write
	#@+body
	def write(self, s):
		"""write(s) stores the string s to the filename specified in the
	    constructor."""
		self.checkParams()
		if self.filename == '':
			raise IOError, 'filename not specified'
		
		try:
			file = cStringIO.StringIO(s)
			if self.mode == '':  # mode='': ASCII mode
				self.ftp.storlines('STOR %s' % self.path, file)
			else: # mode='b': binary mode
				self.ftp.storbinary('STOR %s' % self.path, file)
			file.close()
		except:
			exception, msg, tb = sys.exc_info()
			raise IOError, msg
	
	#@-body
	#@-node:1::write
	#@-node:3::Setters
	#@+node:4::Utilities
	#@+node:1::seek
	#@+body
	def seek(offset=0):
		self.currentLine = 0  # we don't support fancy seeking via FTP
	
	#@-body
	#@-node:1::seek
	#@+node:2::flush
	#@+body
	def flush():
		pass # no fancy stuff here.
	#@-body
	#@-node:2::flush
	#@+node:3::dir
	#@+body
	def dir(self, path=None):
		"""Issue a LIST command passing the specified argument and return output as a string."""
		s = []
	
		if path == None:
			path = self.dirname
		try:
			listcmd = 'LIST %s' % path
			self.ftp.retrlines(listcmd.rstrip(), s.append)
			return '\n'.join(s)
		except:
			exception, msg, tb = sys.exc_info()
			raise IOError, msg
	
	#@-body
	#@-node:3::dir
	#@+node:4::exists
	#@+body
	def exists(self, path=None):
		"""Return 1 if the specified path exists. If path is omitted, the current file name is tried."""
		if path == None:
			path = self.filename
	
		s = self.dir(path)
		if s.lower().find('no such file') == -1:
			return 1
		else:
			return 0
	
	#@-body
	#@-node:4::exists
	#@+node:5::checkParams
	#@+body
	def checkParams(self):
		if self.mode not in ('','b'):
			raise IOError, 'invalid mode: %s' % self.mode
		if not self.isConnectionOpen:
			raise IOError, 'ftp connection closed'
	
	#@-body
	#@-node:5::checkParams
	#@-node:4::Utilities
	#@+node:5::close
	#@+body
	def close(self):
		"""Close an existing FTPurl connection."""
		try:
			self.ftp.quit()
		except:
			self.ftp.close()
		del self.ftp
		self.isConnectionOpen = 0
	#@-body
	#@-node:5::close
	#@-others


#@-body
#@-node:2::class FTPurl
#@+node:3::enable/disable_body
#@+body
# Alas, these do not seem to work on XP:
# disabling the body text _permanently_ stops the cursor from blinking.

def enable_body(body):
	global insertOnTime,insertOffTime
	if body.cget("state") == "disabled":
		try:
			es("enable")
			print insertOffTime,insertOnTime
			body.configure(state="normal")
			body.configure(insertontime=insertOnTime,insertofftime=insertOffTime)
		except: es_exception()
			
def disable_body(body):
	global insertOnTime,insertOffTime
	if body.cget("state") == "normal":
		try:
			es("disable")
			insertOnTime = body.cget("insertontime")
			insertOffTime = body.cget("insertofftime")
			print insertOffTime,insertOnTime
			body.configure(state="disabled")
		except: es_exception()

#@-body
#@-node:3::enable/disable_body
#@+node:4::insert_read_only_node (FTP version)
#@+body
# Sets v's body text from the file with the given name.
# Returns true if the body text changed.
def insert_read_only_node (c,v,name):
	if name=="":
		name = tkFileDialog.askopenfilename(
			title="Open",
			filetypes=[("All files", "*")]
			)
		c.beginUpdate()
		v.setHeadString("@read-only %s" % name)
		c.endUpdate()
	parse = urlparse.urlparse(name)
	try:
		if parse[0] == 'ftp':
			file = FTPurl(name)  # FTP URL
		elif parse[0] == 'http':
			file = urllib.urlopen(name)  # HTTP URL
		else:
			file = open(name,"r")  # local file
		es("reading: @read-only %s" % name)
		new = file.read()
		file.close()
	except IOError,msg:
		es("error reading %s: %s" % (name, msg))
		v.setBodyStringOrPane("") # Clear the body text.
		return true # Mark the node as changed.
	else:
		ext = os.path.splitext(parse[2])[1]
		if ext.lower() in ['.htm', '.html']:
			
			#@<< convert HTML to text >>
			#@+node:1::<< convert HTML to text >>
			#@+body
			fh = cStringIO.StringIO()
			fmt = AbstractFormatter(DumbWriter(fh))
			# the parser stores parsed data into fh (file-like handle)
			parser = HTMLParser(fmt)
			
			# send the HTML text to the parser
			parser.feed(new)
			parser.close()
			
			# now replace the old string with the parsed text
			new = fh.getvalue()
			fh.close()
			
			# finally, get the list of hyperlinks and append to the end of the text
			hyperlinks = parser.anchorlist
			numlinks = len(hyperlinks)
			if numlinks > 0:
				hyperlist = ['\n\n--Hyperlink list follows--']
				for i in xrange(numlinks):
					hyperlist.append("\n[%d]: %s" % (i+1,hyperlinks[i])) # 3/26/03: was i.
				new = new + ''.join(hyperlist)
			#@-body
			#@-node:1::<< convert HTML to text >>

		previous = v.t.bodyString
		v.setBodyStringOrPane(new)
		changed = (toUnicode(new,'ascii') != toUnicode(previous,'ascii'))
		if changed and previous != "":
			es("changed: %s" % name) # A real change.
		return changed



#@-body
#@-node:4::insert_read_only_node (FTP version)
#@+node:5::on_open2
#@+body
#  scan the outline and process @read-only nodes.
def on_open2 (tag,keywords):

	if tag == "start2":
		c = top()
	else:
		c = keywords.get("new_c")
	v = c.rootVnode()
	es("scanning for @read-only nodes...")
	c.beginUpdate()
	while v:
		h = v.headString()
		if match_word(h,0,"@read-only"):
			changed = insert_read_only_node(c,v,h[11:])
			if changed:
				if not v.isDirty():
					v.setDirty()
				if not c.isChanged():
					c.setChanged(changed)
		v = v.threadNext()
	c.endUpdate()
#@-body
#@-node:5::on_open2
#@+node:6::on_bodykey1
#@+body
# override the body key handler if we are in an @read-only node.

def on_bodykey1 (tag,keywords):

	c = keywords.get("c")
	v = keywords.get("v")
	h = v.headString()
	if match_word(h,0,"@read-only"):
		# The following code causes problems with scrolling and syntax coloring.
		# Its advantage is that it makes clear that the text can't be changed,
		# but perhaps that is obvious anyway...
		if 0: # Davide Salomoni requests that this code be eliminated.
			# An @read-only node: do not change its text.
			body = c.frame.body
			body.delete("1.0","end")
			body.insert("1.0",v.bodyString())
		return 1 # Override the body key event handler.
#@-body
#@-node:6::on_bodykey1
#@+node:7::on_headkey2
#@+body
# update the body text when we press enter

def on_headkey2 (tag,keywords):

	c = keywords.get("c")
	v = keywords.get("v")
	h = v.headString()
	ch = keywords.get("ch")
	if ch == '\r' and match_word(h,0,"@read-only"):
		# on-the-fly update of @read-only directives
		changed = insert_read_only_node(c,v,h[11:])
		c.setChanged(changed)
#@-body
#@-node:7::on_headkey2
#@+node:8::on_select1
#@+body
def on_select1 (tag,keywords):

	# Doesn't work: the cursor doesn't start blinking.
	# Enable the body text so select will work properly.
	c = keywords.get("c")
	enable_body(c.frame.body)

#@-body
#@-node:8::on_select1
#@+node:9::on_select2
#@+body
def on_select2 (tag,keywords):

	c = keywords.get("c")
	v = c.currentVnode()
	h = v.headString()
	if match_word(h,0,"@read-only"):
		disable_body(c.frame.body)
	else:
		enable_body(c.frame.body)

#@-body
#@-node:9::on_select2
#@-others


if 0: # Register the handlers...
	registerHandler(("start2","open2"), on_open2)
	registerHandler("bodykey1", on_bodykey1)
	registerHandler("headkey2", on_headkey2)
	if 0: # doesn't work: the cursor stops blinking.
		registerHandler("select1", on_select1)
		registerHandler("select2", on_select2)
		
	import mod_read_only_nodes
	es("...read only nodes v1.2: " + plugin_date(mod_read_only_nodes))

#@-body
#@-node:0::@file plugins/mod_read_only_nodes.py
#@-leo
