# -*- coding: utf-8 -*- # # Copyright (c) 2003 Eugeny Korekin # Copyright (c) 2005-2007 Basil Shubin import os import sys import re import shutil import urllib import errno import string import mimetypes from HTMLParser import HTMLParser, HTMLParseError, piclose from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler # import PyCHM bindings from chm import chmlib from archmod import message from archmod.htmltotext import htmltotext # what config file to use - local or global? user_config = os.path.join(os.path.expanduser('~'), '.arch.conf') if os.path.exists(user_config): config = user_config else: config = '/usr/local/etc/archmage/arch.conf' def listdir_r(dir): def f(res, dir, files): for e in files: d = '/'.join(dir.split('/')[1:]) if d: d += '/' res.append(d + e) res = [] os.path.walk(dir, f, res) return res class CHMDir(object): """ Class that represent CHM content from directory """ def __init__(self, name): # Name of source directory with CHM content self.sourcename = name # Import variables from config file into namespace execfile(config, self.__dict__) # Get all entries self.entries = self.get_entries(name) # Get template files self.templates = self.get_templates() # Get 'Table of Contents' for e in self.entries: if e.lower().endswith('.hhc'): self.hhc = e if e.lower().endswith('.hhk'): self.hhk = e hhclines = self.get_entry_by_name(self.hhc) self.contents, self.deftopic = SitemapFile(hhclines).parse() def get_entries(self, name): """ Get all entries """ entries = [] for fname in listdir_r(name): name = '/' + fname if os.path.isdir(self.sourcename + name): name += '/' entries.append(name) return entries def get_entry_by_name(self, name): """ Get CHM entry by it's name """ # show index page or any other substitute if name == '/': name = '/index.html' if name in self.templates: return self.get_template_by_name(name) if name.lower() in [ os.path.join('/icons', icon.lower()) for icon in os.listdir(self.icons_dir) ]: return open(os.path.join(self.icons_dir, os.path.basename(name))).read() for e in self.entries: if e.lower() == name.lower(): return CHMEntry(self, e).get() else: raise NameError, 'There is no %s' % name def sub_mytag(self,re): """docstring should be here""" try: res = eval('self.' + re.group(1)) except: res = eval(re.group(1)) return res def get_templates(self): """ Get list of all template files """ return [ os.path.join('/', file) for file in os.listdir(self.templates_dir) if os.path.isfile(os.path.join(self.templates_dir, file)) ] def get_template_by_name(self, name): """ Get template file by it's name """ tpl = open(os.path.join(self.templates_dir, os.path.basename(name))).read() return re.sub('\<%(.+?)%\>', self.sub_mytag, tpl) def process_templ(self): """ Process templates """ for template in self.templates: open(os.path.basename(template), 'w').write(self.get_template_by_name(template)) if not os.path.exists('icons/'): shutil.copytree(os.path.join(self.icons_dir), 'icons/') def raw_extract(self): """ Extract raw CHM entries into the files """ # build regex from the list of auxillary files aux_re = '|'.join([ re.escape(s) for s in self.auxes ]) for e in self.entries: # if entry is auxillary file, than skip it if re.match(aux_re, e): continue # process entry fname = string.lower(e[1:]) # get dirname for file fname if any dname = os.path.dirname(fname) # if dname is a directory and it's not exist, than build it if dname and not os.path.exists(dname): os.makedirs(dname) # otherwise write a file from CHM entry else: # filename enconding conversion if self.fs_encoding: fname = fname.decode('utf-8').encode(self.fs_encoding) # write CHM entry content into the file open(fname, 'w').writelines(CHMEntry(self, e).get()) def extract(self, dir): """ Extract CHM file content into FS """ try: # directory to extract CHM file content os.mkdir(dir) os.chdir(dir) # make raw content extraction self.raw_extract() # process templates self.process_templ() except OSError, error: if error[0] == errno.EEXIST: error_msg("Decompilation error: Directory '%s' already exists!" % dir) def raw_dump(self, ext=['*']): """ Dump CHM entries into the stdout """ # build regex from the list of auxillary files aux_re = '|'.join([ re.escape(s) for s in self.auxes ]) ext_re = '|'.join([ '.*' + '\.' + s + '$' for s in ext]) for e in self.entries: # if entry is auxillary file, than skip it if re.match(aux_re, e) or not re.match(ext_re, e): continue # to use this function you should have 'lynx' or 'elinks' installed htmltotext(input=CHMEntry(self, e).get(), cmd=self.htmltotext) def dump_html(self): """ Dump HTML content from CHM file into stdout """ # make html content dumping self.raw_dump(['html', 'htm']) class CHMFile(CHMDir): """ CHM file class derived from CHMDir """ def get_entries(self, name): """ Overrided CHMDir.get_entries() method """ entries = [] # open CHM file and get handler self._handler = chmlib.chm_open(name) # get CHM file content and process it for name in self.get_names(self._handler): if (name == '/'): continue entries.append(name) return entries def get_names(self, chmfile): """ Get object's names inside CHM file """ def _get_name(chmfile, ui, content): content.append(ui.path) return chmlib.CHM_ENUMERATOR_CONTINUE chmdir = [] if (chmlib.chm_enumerate(chmfile, chmlib.CHM_ENUMERATE_ALL, _get_name, chmdir)) == 0: error_msg('UNKNOWN ERROR: CHMLIB or PyCHM issue') return chmdir def __del__(self): """ Closes CHM file handler on class destroing """ chmlib.chm_close(self._handler) class CHMEntry(object): """ Class for CHM file entry """ def __init__(self, parent, name): # parent CHM file self.parent = parent # object inside CHM file self.name = name def read(self): """ Read CHM entry content """ # Check where parent instance is CHMFile or CHMDir if isinstance(self.parent, CHMFile): result, ui = chmlib.chm_resolve_object(self.parent._handler, self.name) if (result != chmlib.CHM_RESOLVE_SUCCESS): return None size, content = chmlib.chm_retrieve_object(self.parent._handler, ui, 0L, ui.length) if (size == 0): return None return content else: return open(self.parent.sourcename + self.name).read() def lower_links(self, text): """ Links to lower case """ return re.sub('(?i)(href|src)\s*=\s*([^\s|>]+)', lambda m:m.group(0).lower(), text) def add_restoreframing_js(self, name, text): name = re.sub('/+', '/', name) depth = name.count('/') js = """""" % ( '../'*depth, name ) return re.sub('(?i)<\s*body\s*>', js, text) def get(self): """ Get CHM entry content""" # read entry content data = self.read() # If entry is a html page? if re.search('(?i)\.html?$', self.name): # lower-casing links if needed if self.parent.filename_case: data = self.lower_links(data) # restore framing if that option is set in config file if self.parent.restore_framing: data = self.add_restoreframing_js(self.name[1:], data) if data is not None: return data else: return '' class SitemapFile(object): """ Sitemap file class """ def __init__(self, lines): self.lines = lines def parse(self): p = SitemapParser() p.feed(self.lines) return (p.parsed + '\n]', p.deftopic) class TagStack(list): """ from book of David Mertz 'Text Processing in Python' """ def append(self, tag): # Remove every paragraph-level tag if this is one if tag.lower() in ('p', 'blockquote'): self = TagStack([ t for t in super if t not in ('p', 'blockquote') ]) super(TagStack, self).append(tag) def pop(self, tag): # 'Pop' by tag from nearest pos, not only last item self.reverse() try: pos = self.index(tag) except ValueError: raise HTMLParser.HTMLParseError, 'Tag not on stack' self[:] = self[pos + 1:] self.reverse() class SitemapParser(HTMLParser): """ Class for parsing files in SiteMap format, such as .hhc """ def __init__(self): self.tagstack = TagStack() self.params = {} self.parsed = '' self.deftopic = '' HTMLParser.__init__(self) def handle_starttag(self, tag, attrs): # first ul, start processing from here if tag == 'ul' and not self.tagstack: self.tagstack.append(tag) self.parsed += '\n[' # if inside ul elif self.tagstack: if tag == 'li': if self.tagstack[-1] != 'ul': self.parsed += ']' self.tagstack.pop('li') indent = ' ' * len(self.tagstack) if self.parsed != '\n[': self.parsed += ',' self.parsed += '\n' + indent + '[' if tag == 'param': self.params[dict(attrs)['name']] = dict(attrs)['value'] self.tagstack.append(tag) def handle_endtag(self, tag): # if inside ul if self.tagstack: if tag == 'ul': self.parsed += ']' if tag == 'object': if not self.params.has_key('ImageNumber'): self.params['ImageNumber'] = 1 if not self.params.has_key('Local'): self.params['Local'] = '' if not self.params.has_key('Name'): self.params['Name'] = '' # use first page as deftopic if not self.deftopic: self.deftopic = self.params['Local'].lower() # # otherwise if there index.htm inside CHM file use it instead # if 'index.htm' in self.params['Local'].lower(): # self.deftopic = self.params['Local'].lower() self.params['Name'] = self.params['Name'].replace('\r\n', '\\n').replace('\n', '\\n') self.params['Local'] = self.params['Local'].replace('..\\', '') if '"' in self.params['Local']: lstr = "'%s'" self.params['Local'] = self.params['Local'].replace("'", '\\\'') else: lstr = '"%s"' self.params['Local'] = self.params['Local'].replace('"', "\\\"") if '"' in self.params['Name']: nstr = "'%s'" self.params['Name'] = self.params['Name'].replace("'", '\\\'') else: nstr = '"%s"' self.params['Name'] = self.params['Name'].replace('"', "\\\"") fstr = nstr + ',' + lstr + ',' + '"%s"' self.parsed += fstr % ( self.params['Name'], self.params['Local'].lower(), self.params['ImageNumber']) self.params = {} if tag != 'li': self.tagstack.pop(tag) def parse_starttag(self, i): try: return HTMLParser.parse_starttag(self, i) except HTMLParseError: try: return piclose.search(self.rawdata, i).end() except AttributeError: return -1 def parse_endtag(self, i): try: return HTMLParser.parse_endtag(self, i) except HTMLParseError: try: return piclose.search(self.rawdata, i).end() except: return -1 class CHMServer(HTTPServer): """ HTTP Server that handle Compressed HTML """ def __init__(self, CHM, name='', port=8000): self.address = (name, port) self.httpd = HTTPServer(self.address, CHMRequestHandler) self.httpd.CHM = CHM self.address = (name, port) def run(self): self.httpd.serve_forever() class CHMRequestHandler(BaseHTTPRequestHandler): """ This class handle HTTP request for CHMServer """ def do_GET(self): pagename = urllib.unquote(self.path.split('?')[0]) if pagename == '/': mimetype = 'text/html' else: mimetype = mimetypes.guess_type(pagename)[0] self.send_response(200) self.send_header('Content-type', mimetype) self.end_headers() # get html data from CHM instance and write it into output self.wfile.write(self.server.CHM.get_entry_by_name(pagename))