blogit.py 15 KB

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