blogit.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. #!/usr/bin/env python
  2. # Copyright (C) 2013 Oz Nahum <nahumoz@gmail.com>
  3. #
  4. # Everyone is permitted to copy and distribute verbatim or modified
  5. # copies of this license document, and changing it is allowed as long
  6. # as the name is changed.
  7. #
  8. # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
  9. #
  10. # 0. You just DO WHATEVER THE FUCK YOU WANT TO. (IT'S SLOPPY CODE ANYWAY)
  11. #
  12. # WARANTIES:
  13. # 0. Are you kidding me?
  14. # 1. Seriously, Are you fucking kidding me?
  15. # 2. If anything goes wrong, sue the "The Empire".
  16. # Note about Summary
  17. # has to be 1 line, no '\n' allowed!
  18. """
  19. Summary: |
  20. some summary ...
  21. Your post
  22. """
  23. """
  24. Everything the Header can't have ":" or "..." in it, you can't have title
  25. with ":" it makes markdown break!
  26. """
  27. """
  28. The content directory can contain only mardown or txt files, no images
  29. allowed!
  30. """
  31. import os
  32. import re
  33. import datetime
  34. import argparse
  35. import sys
  36. from distutils import dir_util
  37. import shutil
  38. from StringIO import StringIO
  39. import codecs
  40. try:
  41. import yaml # in debian python-yaml
  42. from jinja2 import Environment, FileSystemLoader # in debian python-jinja2
  43. except ImportError, e:
  44. print e
  45. print "On Debian based system you can install the dependencies with: "
  46. print "apt-get install python-yaml python-jinja2"
  47. sys.exit(1)
  48. try:
  49. import markdown2
  50. except ImportError, e:
  51. print e
  52. print "try: sudo pip install markdown2"
  53. sys.exit(1)
  54. CONFIG = {
  55. 'content_root': 'content', # where the markdown files are
  56. 'output_to': 'oz123.github.com',
  57. 'templates': 'templates',
  58. 'date_format': '%Y-%m-%d',
  59. 'base_url': 'http://oz123.github.com',
  60. 'http_port': 3030,
  61. 'content_encoding': 'utf-8',
  62. 'author': 'Oz Nahum Tiram',
  63. 'editor': 'editor'
  64. }
  65. # EDIT THIS PARAMETER TO CHANGE ARCHIVE SIZE
  66. # 0 Means that all the entries will be in the archive
  67. # 10 meas that all the entries except the last 10
  68. ARCHIVE_SIZE = 0
  69. GLOBAL_TEMPLATE_CONTEXT = {
  70. 'media_base': '/media/',
  71. 'media_url': '../media/',
  72. 'site_url': 'http://oz123.github.com',
  73. 'last_build': datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
  74. 'twitter': 'https://twitter.com/#!/OzNTiram',
  75. 'stackoverflow': "http://stackoverflow.com/users/492620/oz123",
  76. 'github': "https://github.com/oz123",
  77. }
  78. KINDS = {
  79. 'writing': {
  80. 'name': 'writing', 'name_plural': 'writings',
  81. },
  82. 'note': {
  83. 'name': 'note', 'name_plural': 'notes',
  84. },
  85. 'link': {
  86. 'name': 'link', 'name_plural': 'links',
  87. },
  88. 'photo': {
  89. 'name': 'photo', 'name_plural': 'photos',
  90. },
  91. 'page': {
  92. 'name': 'page', 'name_plural': 'pages',
  93. },
  94. }
  95. jinja_env = Environment(loader=FileSystemLoader(CONFIG['templates']))
  96. class Tag(object):
  97. def __init__(self, name):
  98. super(Tag, self).__init__()
  99. self.name = name
  100. self.prepare()
  101. self.permalink = GLOBAL_TEMPLATE_CONTEXT["site_url"]
  102. def prepare(self):
  103. _slug = self.name.lower()
  104. _slug = re.sub(r'[;;,. ]', '-', _slug)
  105. self.slug = _slug
  106. class Entry(object):
  107. def __init__(self, path):
  108. super(Entry, self).__init__()
  109. path = path.split('content/')[-1]
  110. self.path = path
  111. self.prepare()
  112. def __str__(self):
  113. return self.path
  114. def __repr__(self):
  115. return self.path
  116. @property
  117. def name(self):
  118. return os.path.splitext(os.path.basename(self.path))[0]
  119. @property
  120. def abspath(self):
  121. return os.path.abspath(os.path.join(CONFIG['content_root'], self.path))
  122. @property
  123. def destination(self):
  124. dest = "%s/%s/index.html" % (KINDS[
  125. self.kind]['name_plural'], self.name)
  126. print dest
  127. return os.path.join(CONFIG['output_to'], dest)
  128. @property
  129. def title(self):
  130. return self.header['title']
  131. @property
  132. def summary_html(self):
  133. return "%s" % markdown2.markdown(self.header['summary'].strip())
  134. @property
  135. def credits_html(self):
  136. return "%s" % markdown2.markdown(self.header['credits'].strip())
  137. @property
  138. def summary_atom(self):
  139. summarya = markdown2.markdown(self.header['summary'].strip())
  140. summarya = re.sub("<p>|</p>", "", summarya)
  141. more = '<a href="%s"> continue reading...</a>' % (self.permalink)
  142. return summarya+more
  143. @property
  144. def published_html(self):
  145. if self.kind in ['link', 'note', 'photo']:
  146. return self.header['published'].strftime("%B %d, %Y %I:%M %p")
  147. return self.header['published'].strftime("%B %d, %Y")
  148. @property
  149. def published_atom(self):
  150. return self.published.strftime("%Y-%m-%dT%H:%M:%SZ")
  151. @property
  152. def atom_id(self):
  153. return "tag:%s,%s:%s" % \
  154. (
  155. self.published.strftime("%Y-%m-%d"),
  156. self.permalink,
  157. GLOBAL_TEMPLATE_CONTEXT["site_url"]
  158. )
  159. @property
  160. def body_html(self):
  161. return markdown2.markdown(self.body, extras=['fenced-code-blocks'])
  162. @property
  163. def permalink(self):
  164. return "/%s/%s" % (KINDS[self.kind]['name_plural'], self.name)
  165. @property
  166. def tags(self):
  167. tags = list()
  168. for t in self.header['tags']:
  169. tags.append(Tag(t))
  170. return tags
  171. def prepare(self):
  172. file = codecs.open(self.abspath, 'r')
  173. header = ['---']
  174. while True:
  175. line = file.readline()
  176. line = line.rstrip()
  177. if not line:
  178. break
  179. header.append(line)
  180. self.header = yaml.load(StringIO('\n'.join(header)))
  181. for h in self.header.items():
  182. if h:
  183. try:
  184. setattr(self, h[0], h[1])
  185. except:
  186. pass
  187. body = list()
  188. for line in file.readlines():
  189. body.append(line)
  190. self.body = ''.join(body)
  191. file.close()
  192. if self.kind == 'link':
  193. from urlparse import urlparse
  194. self.domain_name = urlparse(self.url).netloc
  195. elif self.kind == 'photo':
  196. pass
  197. elif self.kind == 'note':
  198. pass
  199. elif self.kind == 'writing':
  200. pass
  201. def render(self):
  202. if not self.header['public']:
  203. return False
  204. try:
  205. os.makedirs(os.path.dirname(self.destination))
  206. except:
  207. pass
  208. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  209. context['entry'] = self
  210. template = jinja_env.get_template("entry.html")
  211. html = template.render(context)
  212. destination = codecs.open(
  213. self.destination, 'w', CONFIG['content_encoding'])
  214. destination.write(html)
  215. destination.close()
  216. return True
  217. class Link(Entry):
  218. def __init__(self, path):
  219. super(Link, self).__init__(path)
  220. @property
  221. def permalink(self):
  222. print "self.url", self.url
  223. raw_input()
  224. return self.url
  225. def entry_factory():
  226. pass
  227. def _sort_entries(entries):
  228. _entries = dict()
  229. sorted_entries = list()
  230. for entry in entries:
  231. _published = entry.header['published'].isoformat()
  232. _entries[_published] = entry
  233. sorted_keys = sorted(_entries.keys())
  234. sorted_keys.reverse()
  235. for key in sorted_keys:
  236. sorted_entries.append(_entries[key])
  237. return sorted_entries
  238. def render_index(entries):
  239. """
  240. this function renders the main page located at index.html
  241. under oz123.github.com
  242. """
  243. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  244. context['entries'] = entries[:10]
  245. template = jinja_env.get_template('entry_index.html')
  246. html = template.render(context)
  247. destination = codecs.open("%s/index.html" % CONFIG[
  248. 'output_to'], 'w', CONFIG['content_encoding'])
  249. destination.write(html)
  250. destination.close()
  251. def render_archive(entries, render_to=None):
  252. """
  253. this function creates the archive page
  254. """
  255. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  256. context['entries'] = entries[ARCHIVE_SIZE:]
  257. template = jinja_env.get_template('archive_index.html')
  258. html = template.render(context)
  259. if not render_to:
  260. render_to = "%s/archive/index.html" % CONFIG['output_to']
  261. dir_util.mkpath("%s/archive" % CONFIG['output_to'])
  262. destination = codecs.open("%s/archive/index.html" % CONFIG[
  263. 'output_to'], 'w', CONFIG['content_encoding'])
  264. destination.write(html)
  265. destination.close()
  266. def render_atom_feed(entries, render_to=None):
  267. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  268. context['entries'] = entries[:10]
  269. template = jinja_env.get_template('atom.xml')
  270. html = template.render(context)
  271. if not render_to:
  272. render_to = "%s/atom.xml" % CONFIG['output_to']
  273. destination = codecs.open(render_to, 'w', CONFIG['content_encoding'])
  274. destination.write(html)
  275. destination.close()
  276. def render_tag_pages(tag_tree):
  277. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  278. for t in tag_tree.items():
  279. context['tag'] = t[1]['tag']
  280. context['entries'] = _sort_entries(t[1]['entries'])
  281. destination = "%s/tags/%s" % (CONFIG['output_to'], context['tag'].slug)
  282. try:
  283. os.makedirs(destination)
  284. except:
  285. pass
  286. template = jinja_env.get_template('tag_index.html')
  287. html = template.render(context)
  288. file = codecs.open("%s/index.html" %
  289. destination, 'w', CONFIG['content_encoding'])
  290. file.write(html)
  291. file.close()
  292. render_atom_feed(context[
  293. 'entries'], render_to="%s/atom.xml" % destination)
  294. def build():
  295. print
  296. print "Rendering website now..."
  297. print
  298. print " entries:"
  299. entries = list()
  300. tags = dict()
  301. for root, dirs, files in os.walk(CONFIG['content_root']):
  302. for fileName in files:
  303. try:
  304. if fileName.endswith('md') or fileName.endswith('markdown'):
  305. entry = Entry(os.path.join(root, fileName))
  306. except Exception, e:
  307. print "Found some problem in: ", fileName
  308. print e
  309. raw_input("Please correct")
  310. sys.exit()
  311. if entry.render():
  312. entries.append(entry)
  313. for tag in entry.tags:
  314. if tag.name not in tags:
  315. tags[tag.name] = {
  316. 'tag': tag,
  317. 'entries': list(),
  318. }
  319. tags[tag.name]['entries'].append(entry)
  320. print " %s" % entry.path
  321. print " :done"
  322. print
  323. print " tag pages & their atom feeds:"
  324. render_tag_pages(tags)
  325. print " :done"
  326. print
  327. print " site wide index"
  328. entries = _sort_entries(entries)
  329. render_index(entries)
  330. print "................done"
  331. print " archive index"
  332. render_archive(entries)
  333. print "................done"
  334. print " site wide atom feeds"
  335. render_atom_feed(entries)
  336. print "...........done"
  337. print
  338. print "All done "
  339. def preview(PREVIEW_ADDR='127.0.1.1', PREVIEW_PORT=11000):
  340. """
  341. launch an HTTP to preview the website
  342. """
  343. import SimpleHTTPServer
  344. import SocketServer
  345. Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
  346. httpd = SocketServer.TCPServer(("", CONFIG['http_port']), Handler)
  347. os.chdir(CONFIG['output_to'])
  348. print "and ready to test at http://127.0.0.1:%d" % CONFIG['http_port']
  349. print "Hit Ctrl+C to exit"
  350. try:
  351. httpd.serve_forever()
  352. except KeyboardInterrupt:
  353. print
  354. print "Shutting Down... Bye!."
  355. print
  356. httpd.server_close()
  357. def publish(GITDIRECTORY=CONFIG['output_to']):
  358. pass
  359. def new_post(GITDIRECTORY=CONFIG['output_to'],
  360. kind=KINDS['writing']):
  361. """
  362. This function should create a template for a new post with a title
  363. read from the user input.
  364. Most other fields should be defaults.
  365. """
  366. title = raw_input("Give the title of the post: ")
  367. while ':' in title:
  368. title = raw_input("Give the title of the post (':' not allowed): ")
  369. author = CONFIG['author']
  370. date = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d')
  371. tags = '['+raw_input("Give the tags, separated by ', ':")+']'
  372. published = 'yes'
  373. chronological = 'yes'
  374. summary = ("summary: |\n\tType your summary here\n\tDo not change the "
  375. "indentation"
  376. "to the left\n...\n\nStart writing your post here!")
  377. # make file name
  378. fname = os.path.join(os.getcwd(), 'content', kind['name_plural'],
  379. datetime.datetime.strftime(datetime.datetime.now(),
  380. '%Y'),
  381. date+'-'+title.replace(' ', '-')+'.markdown')
  382. with open(fname, 'w') as npost:
  383. npost.write('title: %s\n' % title)
  384. npost.write('author: %s\n' % author)
  385. npost.write('published: %s\n' % date)
  386. npost.write('tags: %s\n' % tags)
  387. npost.write('public: %s\n' % published)
  388. npost.write('chronological: %s\n' % chronological)
  389. npost.write('kind: %s\n' % kind['name'])
  390. npost.write('summary: %s' % summary)
  391. os.system('%s %s' % (CONFIG['editor'], fname))
  392. def clean(GITDIRECTORY="oz123.github.com"):
  393. directoriestoclean = ["writings", "notes", "links", "tags", "archive"]
  394. os.chdir(GITDIRECTORY)
  395. for directory in directoriestoclean:
  396. shutil.rmtree(directory)
  397. def dist(SOURCEDIR=os.getcwd()+"/content/",
  398. DESTDIR="oz123.github.com/writings_raw/content/"):
  399. """
  400. sync raw files from SOURCE to DEST
  401. """
  402. import subprocess as sp
  403. sp.call(["rsync", "-avP", SOURCEDIR, DESTDIR], shell=False,
  404. cwd=os.getcwd())
  405. if __name__ == '__main__':
  406. parser = argparse.ArgumentParser(
  407. description='blogit - a tool to blog on github.')
  408. parser.add_argument('-b', '--build', action="store_true",
  409. help='convert the markdown files to HTML')
  410. parser.add_argument('-p', '--preview', action="store_true",
  411. help='Launch HTTP server to preview the website')
  412. parser.add_argument('-c', '--clean', action="store_true",
  413. help='clean output files')
  414. parser.add_argument('-n', '--new', action="store_true",
  415. help='create new post')
  416. parser.add_argument('-d', '--dist', action="store_true",
  417. help='sync raw files from SOURCE to DEST')
  418. args = parser.parse_args()
  419. if len(sys.argv) < 2:
  420. parser.print_help()
  421. sys.exit()
  422. if args.clean:
  423. clean()
  424. if args.build:
  425. build()
  426. if args.dist:
  427. dist()
  428. if args.preview:
  429. preview()
  430. if args.new:
  431. new_post()