| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629 | #!/usr/bin/env python# ============================================================================# Blogit.py is free software; you can redistribute it and/or modify# it under the terms of the GNU General Public License, version 3# as published by the Free Software Foundation;## Blogit.py is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with Blogit.py; if not, write to the Free Software# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA# ============================================================================# Copyright (C) 2013 Oz Nahum Tiram <nahumoz@gmail.com># ============================================================================# Note about Summary# has to be 1 line, no '\n' allowed!"""Summary: |   some summary ...Your post""""""Everything the Header can't have ":" or "..." in it, you can't have titlewith ":" it makes markdown break!""""""The content directory can contain only mardown or txt files, no imagesallowed!"""import osimport reimport datetimeimport argparseimport sysimport operatorfrom distutils import dir_utilimport shutilfrom StringIO import StringIOimport codecsimport subprocess as spimport SimpleHTTPServerimport BaseHTTPServerimport socketimport SocketServerimport threadtry:    import yaml  # in debian python-yaml    from jinja2 import Environment, FileSystemLoader  # in debian python-jinja2except ImportError, e:    print e    print "On Debian based system you can install the dependencies with: "    print "apt-get install python-yaml python-jinja2"    sys.exit(1)try:    import markdown2    renderer = 'md2'except ImportError, e:    try:        import markdown        renderer = 'md1'    except ImportError, e:        print e        print "try: sudo pip install markdown2"        sys.exit(1)from tinydb import Querysys.path.insert(0, os.getcwdu())from conf import CONFIG, ARCHIVE_SIZE, GLOBAL_TEMPLATE_CONTEXT, KINDS, DBjinja_env = Environment(loader=FileSystemLoader(CONFIG['templates']))class Tag(object):    def __init__(self, name):        self.name = name        self.prepare()        self.permalink = GLOBAL_TEMPLATE_CONTEXT["site_url"]        self.table = DB['tags']        # todo: fix this        #try:        #    os.makedirs(destination)        #except:        #    pass        Tags = Query()        tag = self.table.get(Tags.name == self.name)        if not tag:            self.table.insert({'name': self.name, 'post_ids': []})    def prepare(self):        _slug = self.name.lower()        _slug = re.sub(r'[;;,. ]', '-', _slug)        self.slug = _slug    @property    def posts(self):        """        return a list of posts tagged with Tag        """        Tags = Query()        tag = self.table.get(Tags.name == self.name)        return tag['post_ids']    @posts.setter    def posts(self, post_ids):        if not isinstance(post_ids, list):            raise ValueError("post_ids must be of type list")        Tags = Query()        tag = self.table.get(Tags.name == self.name)        if tag:            new = set(post_ids) - set(tag['post_ids'])            tag['post_ids'].extend(list(new))            self.table.update({'post_ids': tag['post_ids']}, eids=[tag.eid])        else:            self.table.insert({'name': self.name, 'post_ids': post_ids})    @property    def entries(self):        _entries = []        Posts = Query()        for id in self.posts:            post = DB['posts'].get(eid=id)            entry = Entry(os.path.join(CONFIG['content_root'],                                       post['filename']))            _entries.append(entry)        return _entries    def render(self):        """Render html page and atom feed"""        self.destination = "%s/tags/%s" % (CONFIG['output_to'],                self.slug)        template = jinja_env.get_template('tag_index.html')        try:            os.makedirs(self.destination)        except OSError:            pass        context = GLOBAL_TEMPLATE_CONTEXT.copy()        context['tag'] = self        context['entries'] = _sort_entries(self.entries)        sorted_entries = _sort_entries(self.entries)        encoding = CONFIG['content_encoding']        render_to = "%s/tags/%s" % (CONFIG['output_to'], self.slug)        jobs = [{'tname': 'tag_index.html',                'output': codecs.open("%s/index.html" % render_to, 'w', encoding),                'entries': sorted_entries},                {'tname': 'atom.xml',                 'output': codecs.open("%s/atom.xml" % render_to, 'w', encoding),                 'entries': sorted_entries[:10]}                ]        for j in jobs:            template = jinja_env.get_template(j['tname'])            context['entries'] = j['entries']            html = template.render(context)            j['output'].write(html)            j['output'].close()        return Trueclass Entry(object):    def __init__(self, path):        super(Entry, self).__init__()        path = path.split('content/')[-1]        self.path = path        self.entry_template = jinja_env.get_template("entry.html")        self.prepare()    def __str__(self):        return self.path    def __repr__(self):        return self.path    @property    def name(self):        return os.path.splitext(os.path.basename(self.path))[0]    @property    def abspath(self):        return os.path.abspath(os.path.join(CONFIG['content_root'], self.path))    @property    def destination(self):        dest = "%s/%s/index.html" % (KINDS[                                     self.kind]['name_plural'], self.name)        print dest        return os.path.join(CONFIG['output_to'], dest)    @property    def title(self):        return self.header['title']    @property    def summary_html(self):        return "%s" % markdown2.markdown(self.header['summary'].strip())    @property    def credits_html(self):        return "%s" % markdown2.markdown(self.header['credits'].strip())    @property    def summary_atom(self):        summarya = markdown2.markdown(self.header['summary'].strip())        summarya = re.sub("<p>|</p>", "", summarya)        more = '<a href="%s"> continue reading...</a>' % (self.permalink)        return summarya+more    @property    def published_html(self):        if self.kind in ['link', 'note', 'photo']:            return self.header['published'].strftime("%B %d, %Y %I:%M %p")        return self.header['published'].strftime("%B %d, %Y")    @property    def published_atom(self):        return self.published.strftime("%Y-%m-%dT%H:%M:%SZ")    @property    def atom_id(self):        return "tag:%s,%s:%s" % \            (                self.published.strftime("%Y-%m-%d"),                self.permalink,                GLOBAL_TEMPLATE_CONTEXT["site_url"]            )    @property    def body_html(self):        if renderer == 'md2':            return markdown2.markdown(self.body, extras=['fenced-code-blocks',                                                         'hilite',                                                         "tables"])        if renderer == 'md1':            return markdown.markdown(self.body,                                     extensions=['fenced_code',                                                 'codehilite(linenums=False)',                                                  'tables'])    @property    def permalink(self):        return "/%s/%s" % (KINDS[self.kind]['name_plural'], self.name)    @property    def tags(self):        return [Tag(t) for t in self.header['tags']]    def _read_header(self, file):        header = ['---']        while True:            line = file.readline()            line = line.rstrip()            if not line:                break            header.append(line)        header = yaml.load(StringIO('\n'.join(header)))        # todo: dispatch header to attribute        # todo: parse date from string to a datetime object        return header    def prepare(self):        file = codecs.open(self.abspath, 'r')        self.header = self._read_header(file)        self.date = self.header['published']        for k, v in self.header.items():            try:                setattr(self, k, v)            except:                pass        body = file.readlines()        self.body = ''.join(body)        file.close()        if self.kind == 'link':            from urlparse import urlparse            self.domain_name = urlparse(self.url).netloc        elif self.kind == 'photo':            pass        elif self.kind == 'note':            pass        elif self.kind == 'writing':            pass    def render(self):        if not self.header['public']:            return False        try:            os.makedirs(os.path.dirname(self.destination))        except:            pass        context = GLOBAL_TEMPLATE_CONTEXT.copy()        context['entry'] = self        try:            html = self.entry_template.render(context)        except Exception as e:            print context            print self.path            print e            sys.exit()        destination = codecs.open(            self.destination, 'w', CONFIG['content_encoding'])        destination.write(html)        destination.close()        # before returning write log to csv        # file name, date first seen, date rendered        # self.path , date-first-seen, if rendered datetime.now        return Trueclass Link(Entry):    def __init__(self, path):        super(Link, self).__init__(path)    @property    def permalink(self):        print "self.url", self.url        return self.urldef _sort_entries(entries):    """Sort all entries by date and reverse the list"""    return list(reversed(sorted(entries, key=operator.attrgetter('date'))))def render_archive(entries, render_to=None):    """    this function creates the archive page    """    context = GLOBAL_TEMPLATE_CONTEXT.copy()    context['entries'] = entries[ARCHIVE_SIZE:]    template = jinja_env.get_template('archive_index.html')    html = template.render(context)    if not render_to:        render_to = "%s/archive/index.html" % CONFIG['output_to']        dir_util.mkpath("%s/archive" % CONFIG['output_to'])    destination = codecs.open("%s/archive/index.html" % CONFIG[                              'output_to'], 'w', CONFIG['content_encoding'])    destination.write(html)    destination.close()def find_new_posts(posts_table):    """    Walk content dir, put each post in the database    """    Posts = Query()    for root, dirs, files in os.walk(CONFIG['content_root']):        for filename in files:            if filename.endswith(('md', 'markdown')):                if not posts_table.contains(Posts.filename == filename):                    post_id = posts_table.insert({'filename': filename})                    yield post_id, filenamedef _get_last_entries():    eids = [post.eid for post in DB['posts'].all()]    eids = sorted(eids, reverse=True)[-10:]    entries = [Entry(DB['posts'].get(eid=eid)['filename']) for eid in eids]    return entriesdef update_index():    """find the last 10 entries in the database and create the main    page.    Each entry in has an eid, so we only get the last 10 eids.    This method also update the ATOM feed.    """    entries = _get_last_entries()    context = GLOBAL_TEMPLATE_CONTEXT.copy()    context['entries'] = entries    for name, out in {'entry_index.html': 'index.html',                      'atom.xml': 'atom.xml'}.items():        template = jinja_env.get_template(name)        html = template.render(context)        destination = codecs.open("%s/%s" % (CONFIG['output_to'], out),                                  'w', CONFIG['content_encoding'])        destination.write(html)        destination.close()def new_build():    """        a. For each new post:        1. render html        2. find post tags        3. update atom feeds for old tags        4. create new atom feeds for new tags    b. update index page    c. update archive page    """    print    print "Rendering website now..."    print    print " entries:"    entries = list()    tags = dict()    root = CONFIG['content_root']    for post_id, post in find_new_posts(DB['posts']):        try:            entry = Entry(os.path.join(root, post))            if entry.render():                entries.append(entry)                for tag in entry.tags:                    tag.posts = [post_id]                    tags[tag.name] = tag            print "     %s" % entry.path        except Exception as e:            print "Found some problem in: ", post            print e            print "Please correct this problem ..."            sys.exit(1)    for name, to in tags.iteritems():        print "updating tag %s" % name        to.render()    # update index    print "updating index"    update_index()    # update archive    # TODOdef build():    print    print "Rendering website now..."    print    print " entries:"    entries = list()    tags = dict()    for root, dirs, files in os.walk(CONFIG['content_root']):        for filename in files:            try:                if filename.endswith(('md', 'markdown')):                    entry = Entry(os.path.join(root, filename))                    if entry.render():                        entries.append(entry)                        for tag in entry.tags:                            if tag.name not in tags:                                tags[tag.name] = {                                    'tag': tag,                                    'entries': list(),                                }                            tags[tag.name]['entries'].append(entry)                    print "     %s" % entry.path            except Exception as e:                print "Found some problem in: ", filename                print e                print "Please correct this problem ..."                sys.exit()    print " :done"    print    print " tag pages & their atom feeds:"    render_tag_pages(tags)    print " :done"    print    print " site wide index"    entries = _sort_entries(entries)    render_index(entries)    print "................done"    print " archive index"    render_archive(entries)    print "................done"    print " site wide atom feeds"    render_atom_feed(entries)    print "...........done"    print    print "All done "class StoppableHTTPServer(BaseHTTPServer.HTTPServer):    def server_bind(self):        BaseHTTPServer.HTTPServer.server_bind(self)        self.socket.settimeout(1)        self.run = True    def get_request(self):        while self.run:            try:                sock, addr = self.socket.accept()                sock.settimeout(None)                return (sock, addr)            except socket.timeout:                pass    def stop(self):        self.run = False    def serve(self):        while self.run:            self.handle_request()def preview():    """    launch an HTTP to preview the website    """    Handler = SimpleHTTPServer.SimpleHTTPRequestHandler    SocketServer.TCPServer.allow_reuse_address = True    port = CONFIG['http_port']    httpd = SocketServer.TCPServer(("", port), Handler)    os.chdir(CONFIG['output_to'])    print "and ready to test at http://127.0.0.1:%d" % CONFIG['http_port']    print "Hit Ctrl+C to exit"    try:        httpd.serve_forever()    except KeyboardInterrupt:        httpd.shutdown()def publish(GITDIRECTORY=CONFIG['output_to']):    sp.call('git push', cwd=GITDIRECTORY, shell=True)def new_post(GITDIRECTORY=CONFIG['output_to'],             kind=KINDS['writing']):    """    This function should create a template for a new post with a title    read from the user input.    Most other fields should be defaults.    """    title = raw_input("Give the title of the post: ")    while ':' in title:        title = raw_input("Give the title of the post (':' not allowed): ")    author = CONFIG['author']    date = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d')    tags = '[' + raw_input("Give the tags, separated by ', ':") + ']'    published = 'yes'    chronological = 'yes'    summary = ("summary: |\n    Type your summary here.\n    Do not change the "               "indentation"               "to the left\n    ...\n\nStart writing your post here!")    # make file name    fname = os.path.join(os.getcwd(), 'content', kind['name_plural'],                         datetime.datetime.strftime(datetime.datetime.now(),                                                    '%Y'),                         date+'-'+title.replace(' ', '-')+'.markdown')    with open(fname, 'w') as npost:        npost.write('title: %s\n' % title)        npost.write('author: %s\n' % author)        npost.write('published: %s\n' % date)        npost.write('tags: %s\n' % tags)        npost.write('public: %s\n' % published)        npost.write('chronological: %s\n' % chronological)        npost.write('kind: %s\n' % kind['name'])        npost.write('%s' % summary)    print '%s %s' % (CONFIG['editor'], repr(fname))    os.system('%s %s' % (CONFIG['editor'], fname))def clean(GITDIRECTORY=CONFIG['output_to']):    directoriestoclean = ["writings", "notes", "links", "tags", "archive"]    os.chdir(GITDIRECTORY)    for directory in directoriestoclean:        shutil.rmtree(directory)def dist(SOURCEDIR=os.getcwd()+"/content/",         DESTDIR=CONFIG['raw_content']):    """    sync raw files from SOURCE to DEST    """    sp.call(["rsync", "-avP", SOURCEDIR, DESTDIR], shell=False,            cwd=os.getcwd())if __name__ == '__main__':    parser = argparse.ArgumentParser(        description='blogit - a tool to blog on github.')    parser.add_argument('-b', '--build', action="store_true",                        help='convert the markdown files to HTML')    parser.add_argument('-p', '--preview', action="store_true",                        help='Launch HTTP server to preview the website')    parser.add_argument('-c', '--clean', action="store_true",                        help='clean output files')    parser.add_argument('-n', '--new', action="store_true",                        help='create new post')    parser.add_argument('-d', '--dist', action="store_true",                        help='sync raw files from SOURCE to DEST')    parser.add_argument('--publish', action="store_true",                        help='push built HTML to git upstream')    args = parser.parse_args()    if len(sys.argv) < 2:        parser.print_help()        sys.exit()    if args.clean:        clean()    if args.build:        new_build()    if args.dist:        dist()    if args.preview:        preview()    if args.new:        new_post()    if args.publish:        publish()
 |