blogit2.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667
  1. #!/usr/bin/env python
  2. # ============================================================================
  3. # Blogit.py is free software; you can redistribute it and/or modify
  4. # it under the terms of the GNU General Public License, version 3
  5. # as published by the Free Software Foundation;
  6. #
  7. # Blogit.py is distributed in the hope that it will be useful,
  8. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. # GNU General Public License for more details.
  11. #
  12. # You should have received a copy of the GNU General Public License
  13. # along with Blogit.py; if not, write to the Free Software
  14. # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  15. # ============================================================================
  16. # Copyright (C) 2013 Oz Nahum Tiram <nahumoz@gmail.com>
  17. # ============================================================================
  18. # Note about Summary
  19. # has to be 1 line, no '\n' allowed!
  20. """
  21. Summary: |
  22. some summary ...
  23. Your post
  24. """
  25. """
  26. Everything the Header can't have ":" or "..." in it, you can't have title
  27. with ":" it makes markdown break!
  28. """
  29. """
  30. The content directory can contain only mardown or txt files, no images
  31. allowed!
  32. """
  33. import os
  34. import re
  35. import datetime
  36. import argparse
  37. import sys
  38. import operator
  39. from distutils import dir_util
  40. import shutil
  41. from StringIO import StringIO
  42. import codecs
  43. import subprocess as sp
  44. import SimpleHTTPServer
  45. import BaseHTTPServer
  46. import socket
  47. import thread
  48. try:
  49. import yaml # in debian python-yaml
  50. from jinja2 import Environment, FileSystemLoader # in debian python-jinja2
  51. except ImportError, e:
  52. print e
  53. print "On Debian based system you can install the dependencies with: "
  54. print "apt-get install python-yaml python-jinja2"
  55. sys.exit(1)
  56. try:
  57. import markdown2
  58. renderer = 'md2'
  59. except ImportError, e:
  60. try:
  61. import markdown
  62. renderer = 'md1'
  63. except ImportError, e:
  64. print e
  65. print "try: sudo pip install markdown2"
  66. sys.exit(1)
  67. from tinydb import Query
  68. sys.path.insert(0, os.getcwdu())
  69. from conf import CONFIG, ARCHIVE_SIZE, GLOBAL_TEMPLATE_CONTEXT, KINDS, DB
  70. jinja_env = Environment(loader=FileSystemLoader(CONFIG['templates']))
  71. class Tag(object):
  72. def __init__(self, name):
  73. self.name = name
  74. self.prepare()
  75. self.permalink = GLOBAL_TEMPLATE_CONTEXT["site_url"]
  76. self.table = DB['tags']
  77. # todo: fix this
  78. #try:
  79. # os.makedirs(destination)
  80. #except:
  81. # pass
  82. Tags = Query()
  83. tag = self.table.get(Tags.name == self.name)
  84. if not tag:
  85. self.table.insert({'name': self.name, 'post_ids': []})
  86. def prepare(self):
  87. _slug = self.name.lower()
  88. _slug = re.sub(r'[;;,. ]', '-', _slug)
  89. self.slug = _slug
  90. @property
  91. def posts(self):
  92. """
  93. return a list of posts tagged with Tag
  94. """
  95. Tags = Query()
  96. tag = self.table.get(Tags.name == self.name)
  97. return tag['post_ids']
  98. @posts.setter
  99. def posts(self, post_ids):
  100. if not isinstance(post_ids, list):
  101. raise ValueError("post_ids must be of type list")
  102. Tags = Query()
  103. tag = self.table.get(Tags.name == self.name)
  104. if tag:
  105. new = set(post_ids) - set(tag['post_ids'])
  106. tag['post_ids'].extend(list(new))
  107. self.table.update({'post_ids': tag['post_ids']}, eids=[tag.eid])
  108. else:
  109. self.table.insert({'name': self.name, 'post_ids': post_ids})
  110. @property
  111. def entries(self):
  112. _entries = []
  113. Posts = Query()
  114. for id in self.posts:
  115. post = DB['posts'].get(eid=id)
  116. entry = Entry(os.path.join(CONFIG['content_root'],
  117. post['filename']))
  118. _entries.append(entry)
  119. return _entries
  120. def render(self):
  121. self.destination = "%s/tags/%s" % (CONFIG['output_to'],
  122. self.slug)
  123. template = jinja_env.get_template('tag_index.html')
  124. try:
  125. os.makedirs(self.destination)
  126. except OSError:
  127. pass
  128. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  129. context['tag'] = self.name
  130. context['entries'] = self.entries
  131. context['entries'] = _sort_entries(self.entries)
  132. destination = "%s/tags/%s" % (CONFIG['output_to'], self.slug)
  133. template = jinja_env.get_template('tag_index.html')
  134. html = template.render(context)
  135. file = codecs.open("%s/index.html" % destination,
  136. 'w', CONFIG['content_encoding'])
  137. file.write(html)
  138. file.close()
  139. #render_atom_feed(context['entries'],
  140. # render_to="%s/atom.xml" % destination)
  141. # before returning write log to csv
  142. # file name, date first seen, date rendered
  143. # self.path , date-first-seen, if rendered datetime.now
  144. #return True
  145. class Entry(object):
  146. def __init__(self, path):
  147. super(Entry, self).__init__()
  148. path = path.split('content/')[-1]
  149. self.path = path
  150. self.entry_template = jinja_env.get_template("entry.html")
  151. self.prepare()
  152. def __str__(self):
  153. return self.path
  154. def __repr__(self):
  155. return self.path
  156. @property
  157. def name(self):
  158. return os.path.splitext(os.path.basename(self.path))[0]
  159. @property
  160. def abspath(self):
  161. return os.path.abspath(os.path.join(CONFIG['content_root'], self.path))
  162. @property
  163. def destination(self):
  164. dest = "%s/%s/index.html" % (KINDS[
  165. self.kind]['name_plural'], self.name)
  166. print dest
  167. return os.path.join(CONFIG['output_to'], dest)
  168. @property
  169. def title(self):
  170. return self.header['title']
  171. @property
  172. def summary_html(self):
  173. return "%s" % markdown2.markdown(self.header['summary'].strip())
  174. @property
  175. def credits_html(self):
  176. return "%s" % markdown2.markdown(self.header['credits'].strip())
  177. @property
  178. def summary_atom(self):
  179. summarya = markdown2.markdown(self.header['summary'].strip())
  180. summarya = re.sub("<p>|</p>", "", summarya)
  181. more = '<a href="%s"> continue reading...</a>' % (self.permalink)
  182. return summarya+more
  183. @property
  184. def published_html(self):
  185. if self.kind in ['link', 'note', 'photo']:
  186. return self.header['published'].strftime("%B %d, %Y %I:%M %p")
  187. return self.header['published'].strftime("%B %d, %Y")
  188. @property
  189. def published_atom(self):
  190. return self.published.strftime("%Y-%m-%dT%H:%M:%SZ")
  191. @property
  192. def atom_id(self):
  193. return "tag:%s,%s:%s" % \
  194. (
  195. self.published.strftime("%Y-%m-%d"),
  196. self.permalink,
  197. GLOBAL_TEMPLATE_CONTEXT["site_url"]
  198. )
  199. @property
  200. def body_html(self):
  201. if renderer == 'md2':
  202. return markdown2.markdown(self.body, extras=['fenced-code-blocks',
  203. 'hilite',
  204. "tables"])
  205. if renderer == 'md1':
  206. return markdown.markdown(self.body,
  207. extensions=['fenced_code',
  208. 'codehilite(linenums=False)',
  209. 'tables'])
  210. @property
  211. def permalink(self):
  212. return "/%s/%s" % (KINDS[self.kind]['name_plural'], self.name)
  213. @property
  214. def tags(self):
  215. return [Tag(t) for t in self.header['tags']]
  216. def _read_header(self, file):
  217. header = ['---']
  218. while True:
  219. line = file.readline()
  220. line = line.rstrip()
  221. if not line:
  222. break
  223. header.append(line)
  224. header = yaml.load(StringIO('\n'.join(header)))
  225. # todo: dispatch header to attribute
  226. # todo: parse date from string to a datetime object
  227. return header
  228. def prepare(self):
  229. file = codecs.open(self.abspath, 'r')
  230. self.header = self._read_header(file)
  231. self.date = self.header['published']
  232. for k, v in self.header.items():
  233. try:
  234. setattr(self, k, v)
  235. except:
  236. pass
  237. body = file.readlines()
  238. self.body = ''.join(body)
  239. file.close()
  240. if self.kind == 'link':
  241. from urlparse import urlparse
  242. self.domain_name = urlparse(self.url).netloc
  243. elif self.kind == 'photo':
  244. pass
  245. elif self.kind == 'note':
  246. pass
  247. elif self.kind == 'writing':
  248. pass
  249. def render(self):
  250. if not self.header['public']:
  251. return False
  252. try:
  253. os.makedirs(os.path.dirname(self.destination))
  254. except:
  255. pass
  256. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  257. context['entry'] = self
  258. try:
  259. html = self.entry_template.render(context)
  260. except Exception as e:
  261. print context
  262. print self.path
  263. print e
  264. sys.exit()
  265. destination = codecs.open(
  266. self.destination, 'w', CONFIG['content_encoding'])
  267. destination.write(html)
  268. destination.close()
  269. # before returning write log to csv
  270. # file name, date first seen, date rendered
  271. # self.path , date-first-seen, if rendered datetime.now
  272. return True
  273. class Link(Entry):
  274. def __init__(self, path):
  275. super(Link, self).__init__(path)
  276. @property
  277. def permalink(self):
  278. print "self.url", self.url
  279. return self.url
  280. def entry_factory():
  281. pass
  282. def _sort_entries(entries):
  283. """Sort all entries by date and reverse the list"""
  284. return list(reversed(sorted(entries, key=operator.attrgetter('date'))))
  285. def render_index(entries):
  286. """
  287. this function renders the main page located at index.html
  288. under oz123.github.com
  289. """
  290. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  291. context['entries'] = entries[:10]
  292. template = jinja_env.get_template('entry_index.html')
  293. html = template.render(context)
  294. destination = codecs.open("%s/index.html" % CONFIG[
  295. 'output_to'], 'w', CONFIG['content_encoding'])
  296. destination.write(html)
  297. destination.close()
  298. def render_archive(entries, render_to=None):
  299. """
  300. this function creates the archive page
  301. """
  302. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  303. context['entries'] = entries[ARCHIVE_SIZE:]
  304. template = jinja_env.get_template('archive_index.html')
  305. html = template.render(context)
  306. if not render_to:
  307. render_to = "%s/archive/index.html" % CONFIG['output_to']
  308. dir_util.mkpath("%s/archive" % CONFIG['output_to'])
  309. destination = codecs.open("%s/archive/index.html" % CONFIG[
  310. 'output_to'], 'w', CONFIG['content_encoding'])
  311. destination.write(html)
  312. destination.close()
  313. def render_atom_feed(entries, render_to=None):
  314. #context = GLOBAL_TEMPLATE_CONTEXT.copy()
  315. #context['entries'] = entries[:10]
  316. #template = jinja_env.get_template('atom.xml')
  317. #html = template.render(context)
  318. #if not render_to:
  319. # render_to = "%s/atom.xml" % CONFIG['output_to']
  320. #destination = codecs.open(render_to, 'w', CONFIG['content_encoding'])
  321. #destination.write(html)
  322. #destination.close()
  323. pass
  324. def render_tag_page(tag):
  325. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  326. context['entries'] = _sort_entries(v['entries'])
  327. destination = "%s/tags/%s" % (CONFIG['output_to'], context['tag'].slug)
  328. def render_tag_pages(tag_tree):
  329. """
  330. tag_tree is a dictionary witht the following structure:
  331. {'python': {'tag': <__main__.Tag object at 0x7f0e56200ed0>,
  332. 'entries': [post1.md, post2.md, post3.md]},
  333. 'git': {'tag': <__main__.Tag object at 0x7f0e5623c2d0>,
  334. 'entries': [post1.md, post2.md, post3.md]},
  335. 'bash': {'tag': <__main__.Tag object at 0x7f0e5623c0d0>,
  336. 'entries': [post1.md, post2.md, post3.md]}}
  337. """
  338. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  339. for k, v in tag_tree.items():
  340. context['tag'] = v['tag']
  341. context['entries'] = _sort_entries(v['entries'])
  342. destination = "%s/tags/%s" % (CONFIG['output_to'], context['tag'].slug)
  343. try:
  344. os.makedirs(destination)
  345. except:
  346. pass
  347. template = jinja_env.get_template('tag_index.html')
  348. html = template.render(context)
  349. file = codecs.open("%s/index.html" %
  350. destination, 'w', CONFIG['content_encoding'])
  351. file.write(html)
  352. file.close()
  353. render_atom_feed(context[
  354. 'entries'], render_to="%s/atom.xml" % destination)
  355. def find_new_posts(posts_table):
  356. """
  357. Walk content dir, put each post in the database
  358. """
  359. Posts = Query()
  360. for root, dirs, files in os.walk(CONFIG['content_root']):
  361. for filename in files:
  362. if filename.endswith(('md', 'markdown')):
  363. if not posts_table.contains(Posts.filename == filename):
  364. post_id = posts_table.insert({'filename': filename})
  365. yield post_id, filename
  366. def update_tags(tags):
  367. try:
  368. tags['python']['tag'].posts
  369. except KeyError:
  370. pass
  371. for t in tags:
  372. pass
  373. def new_build():
  374. """
  375. a. For each new post:
  376. 1. render html
  377. 2. find post tags
  378. 3. update atom feeds for old tags
  379. 4. create new atom feeds for new tags
  380. b. update index page
  381. c. update archive page
  382. """
  383. print
  384. print "Rendering website now..."
  385. print
  386. print " entries:"
  387. entries = list()
  388. tags = dict()
  389. root = CONFIG['content_root']
  390. for post_id, post in find_new_posts(DB['posts']):
  391. try:
  392. entry = Entry(os.path.join(root, post))
  393. if entry.render():
  394. entries.append(entry)
  395. for tag in entry.tags:
  396. if tag.name not in tags:
  397. tags[tag.name] = {
  398. 'tag': tag,
  399. 'entries': list(),
  400. }
  401. tags[tag.name]['entries'].append(entry)
  402. tags[tag.name]['tag'].posts = [post_id]
  403. print " %s" % entry.path
  404. except Exception as e:
  405. print "Found some problem in: ", post
  406. print e
  407. print "Please correct this problem ..."
  408. sys.exit()
  409. update_tags(tags)
  410. render_tag_pages(tags)
  411. def build():
  412. print
  413. print "Rendering website now..."
  414. print
  415. print " entries:"
  416. entries = list()
  417. tags = dict()
  418. for root, dirs, files in os.walk(CONFIG['content_root']):
  419. for filename in files:
  420. try:
  421. if filename.endswith(('md', 'markdown')):
  422. entry = Entry(os.path.join(root, filename))
  423. if entry.render():
  424. entries.append(entry)
  425. for tag in entry.tags:
  426. if tag.name not in tags:
  427. tags[tag.name] = {
  428. 'tag': tag,
  429. 'entries': list(),
  430. }
  431. tags[tag.name]['entries'].append(entry)
  432. print " %s" % entry.path
  433. except Exception as e:
  434. print "Found some problem in: ", filename
  435. print e
  436. print "Please correct this problem ..."
  437. sys.exit()
  438. print " :done"
  439. print
  440. print " tag pages & their atom feeds:"
  441. render_tag_pages(tags)
  442. print " :done"
  443. print
  444. print " site wide index"
  445. entries = _sort_entries(entries)
  446. render_index(entries)
  447. print "................done"
  448. print " archive index"
  449. render_archive(entries)
  450. print "................done"
  451. print " site wide atom feeds"
  452. render_atom_feed(entries)
  453. print "...........done"
  454. print
  455. print "All done "
  456. class StoppableHTTPServer(BaseHTTPServer.HTTPServer):
  457. def server_bind(self):
  458. BaseHTTPServer.HTTPServer.server_bind(self)
  459. self.socket.settimeout(1)
  460. self.run = True
  461. def get_request(self):
  462. while self.run:
  463. try:
  464. sock, addr = self.socket.accept()
  465. sock.settimeout(None)
  466. return (sock, addr)
  467. except socket.timeout:
  468. pass
  469. def stop(self):
  470. self.run = False
  471. def serve(self):
  472. while self.run:
  473. self.handle_request()
  474. def preview(PREVIEW_ADDR='127.0.1.1', PREVIEW_PORT=11000):
  475. """
  476. launch an HTTP to preview the website
  477. """
  478. os.chdir(CONFIG['output_to'])
  479. print "and ready to test at http://127.0.0.1:%d" % CONFIG['http_port']
  480. print "Hit Ctrl+C to exit"
  481. try:
  482. httpd = StoppableHTTPServer(("127.0.0.1", CONFIG['http_port']),
  483. SimpleHTTPServer.SimpleHTTPRequestHandler)
  484. thread.start_new_thread(httpd.serve, ())
  485. sp.call('xdg-open http://127.0.0.1:%d' % CONFIG['http_port'],
  486. shell=True)
  487. while True:
  488. continue
  489. except KeyboardInterrupt:
  490. print
  491. print "Shutting Down... Bye!."
  492. print
  493. httpd.stop()
  494. def publish(GITDIRECTORY=CONFIG['output_to']):
  495. sp.call('git push', cwd=GITDIRECTORY, shell=True)
  496. def new_post(GITDIRECTORY=CONFIG['output_to'],
  497. kind=KINDS['writing']):
  498. """
  499. This function should create a template for a new post with a title
  500. read from the user input.
  501. Most other fields should be defaults.
  502. """
  503. title = raw_input("Give the title of the post: ")
  504. while ':' in title:
  505. title = raw_input("Give the title of the post (':' not allowed): ")
  506. author = CONFIG['author']
  507. date = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d')
  508. tags = '[' + raw_input("Give the tags, separated by ', ':") + ']'
  509. published = 'yes'
  510. chronological = 'yes'
  511. summary = ("summary: |\n Type your summary here.\n Do not change the "
  512. "indentation"
  513. "to the left\n ...\n\nStart writing your post here!")
  514. # make file name
  515. fname = os.path.join(os.getcwd(), 'content', kind['name_plural'],
  516. datetime.datetime.strftime(datetime.datetime.now(),
  517. '%Y'),
  518. date+'-'+title.replace(' ', '-')+'.markdown')
  519. with open(fname, 'w') as npost:
  520. npost.write('title: %s\n' % title)
  521. npost.write('author: %s\n' % author)
  522. npost.write('published: %s\n' % date)
  523. npost.write('tags: %s\n' % tags)
  524. npost.write('public: %s\n' % published)
  525. npost.write('chronological: %s\n' % chronological)
  526. npost.write('kind: %s\n' % kind['name'])
  527. npost.write('%s' % summary)
  528. print '%s %s' % (CONFIG['editor'], repr(fname))
  529. os.system('%s %s' % (CONFIG['editor'], fname))
  530. def clean(GITDIRECTORY=CONFIG['output_to']):
  531. directoriestoclean = ["writings", "notes", "links", "tags", "archive"]
  532. os.chdir(GITDIRECTORY)
  533. for directory in directoriestoclean:
  534. shutil.rmtree(directory)
  535. def dist(SOURCEDIR=os.getcwd()+"/content/",
  536. DESTDIR=CONFIG['raw_content']):
  537. """
  538. sync raw files from SOURCE to DEST
  539. """
  540. sp.call(["rsync", "-avP", SOURCEDIR, DESTDIR], shell=False,
  541. cwd=os.getcwd())
  542. if __name__ == '__main__':
  543. parser = argparse.ArgumentParser(
  544. description='blogit - a tool to blog on github.')
  545. parser.add_argument('-b', '--build', action="store_true",
  546. help='convert the markdown files to HTML')
  547. parser.add_argument('-p', '--preview', action="store_true",
  548. help='Launch HTTP server to preview the website')
  549. parser.add_argument('-c', '--clean', action="store_true",
  550. help='clean output files')
  551. parser.add_argument('-n', '--new', action="store_true",
  552. help='create new post')
  553. parser.add_argument('-d', '--dist', action="store_true",
  554. help='sync raw files from SOURCE to DEST')
  555. parser.add_argument('--publish', action="store_true",
  556. help='push built HTML to git upstream')
  557. args = parser.parse_args()
  558. if len(sys.argv) < 2:
  559. parser.print_help()
  560. sys.exit()
  561. if args.clean:
  562. clean()
  563. if args.build:
  564. build()
  565. if args.dist:
  566. dist()
  567. if args.preview:
  568. preview()
  569. if args.new:
  570. new_post()
  571. if args.publish:
  572. publish()