blogit2.py 18 KB

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