1
0

blogit.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8
  3. # Copyleft (C) 2010 Mir Nazim <hello@mirnazim.org>
  4. # Copyleft (C) 2013 Oz Nahum <nahumoz@gmail.com>
  5. #
  6. # Everyone is permitted to copy and distribute verbatim or modified
  7. # copies of this license document, and changing it is allowed as long
  8. # as the name is changed.
  9. #
  10. # TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
  11. #
  12. # 0. You just DO WHATEVER THE FUCK YOU WANT TO. (IT'S SLOPPY CODE ANYWAY)
  13. #
  14. # WARANTIES:
  15. # 0. Are you kidding me?
  16. # 1. Seriously, Are you fucking kidding me?
  17. # 2. If anything goes wrong, sue the "The Empire".
  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 yaml # in debian python-yaml
  37. from StringIO import StringIO
  38. import codecs
  39. from jinja2 import Environment, FileSystemLoader # in debian python-jinja2
  40. try:
  41. import markdown2
  42. except ImportError:
  43. import markdown as markdown2
  44. import argparse
  45. import sys
  46. from distutils import dir_util
  47. import shutil
  48. CONFIG = {
  49. 'content_root': 'content', # where the markdown files are
  50. 'output_to': 'oz123.github.com',
  51. 'templates': 'templates',
  52. 'date_format': '%Y-%m-%d',
  53. 'base_url': 'http://oz123.github.com',
  54. 'http_port': 3030,
  55. 'content_encoding': 'utf-8',
  56. }
  57. # EDIT THIS PARAMETER TO CHANGE ARCHIVE SIZE
  58. # 0 Means that all the entries will be in the archive
  59. # 10 meas that all the entries except the last 10
  60. ARCHIVE_SIZE = 0
  61. GLOBAL_TEMPLATE_CONTEXT = {
  62. 'media_base': '/media/',
  63. 'media_url': '../media/',
  64. 'site_url': 'http://oz123.github.com',
  65. 'last_build': datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ"),
  66. 'twitter': 'https://twitter.com/#!/OzNTiram',
  67. 'stackoverflow': "http://stackoverflow.com/users/492620/oz123",
  68. 'github': "https://github.com/oz123",
  69. 'side_bar': """
  70. <div id="nav">
  71. <div><img src="/media/img/me.png"></div>
  72. <a title="Home" href="/">home</a>
  73. <a title="About" class="about" href="/about.html">about</a>
  74. <a title="Archive" class="archive" href="/archive">archive</a>
  75. <a title="Atom feeds" href="/atom.xml">atom</a>
  76. <a title="Twitter" href="https://twitter.com/#!/OzNTiram">twitter</a>
  77. <a title="Stackoverflow" href="http://stackoverflow.com/users/492620/oz123">stackoverflow</a>
  78. <a title="Github" href="https://github.com/oz123">github</a>
  79. <script type="text/javascript"><!--
  80. google_ad_client = "ca-pub-2570499281263620";
  81. /* new_tower_for_oz123githubcom */
  82. google_ad_slot = "8107518414";
  83. google_ad_width = 120;
  84. google_ad_height = 600;
  85. //-->
  86. </script>
  87. <script type="text/javascript"
  88. src="http://pagead2.googlesyndication.com/pagead/show_ads.js">
  89. </script>
  90. </div>
  91. """,
  92. 'google_analytics': """
  93. <script type="text/javascript">
  94. var _gaq = _gaq || [];
  95. _gaq.push(['_setAccount', 'UA-36587163-1']);
  96. _gaq.push(['_trackPageview']);
  97. (function() {
  98. var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  99. ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  100. var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  101. })();
  102. </script>""",
  103. 'disquss' : """<div id="disqus_thread"></div>
  104. <script type="text/javascript">
  105. /* * * CONFIGURATION VARIABLES: EDIT BEFORE PASTING INTO YOUR WEBPAGE * * */
  106. var disqus_shortname = 'oz123githubcom'; // required: replace example with your forum shortname
  107. /* * * DON'T EDIT BELOW THIS LINE * * */
  108. (function() {
  109. var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
  110. dsq.src = 'http://' + disqus_shortname + '.disqus.com/embed.js';
  111. (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
  112. })();
  113. </script>
  114. <noscript>Please enable JavaScript to view the <a href="http://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
  115. <a href="http://disqus.com" class="dsq-brlink">comments powered by <span class="logo-disqus">Disqus</span></a>
  116. """
  117. }
  118. KINDS = {
  119. 'writing': {
  120. 'name': 'writing', 'name_plural': 'writings',
  121. },
  122. 'note': {
  123. 'name': 'note', 'name_plural': 'notes',
  124. },
  125. 'link': {
  126. 'name': 'link', 'name_plural': 'links',
  127. },
  128. 'photo': {
  129. 'name': 'photo', 'name_plural': 'photos',
  130. },
  131. 'page': {
  132. 'name': 'page', 'name_plural': 'pages',
  133. },
  134. }
  135. jinja_env = Environment(loader=FileSystemLoader(CONFIG['templates']))
  136. class Tag(object):
  137. def __init__(self, name):
  138. super(Tag, self).__init__()
  139. self.name = name
  140. self.prepare()
  141. self.permalink = GLOBAL_TEMPLATE_CONTEXT["site_url"]
  142. def prepare(self):
  143. _slug = self.name.lower()
  144. _slug = re.sub(r'[;;,. ]', '-', _slug)
  145. self.slug = _slug
  146. class Entry(object):
  147. def __init__(self, path):
  148. super(Entry, self).__init__()
  149. path = path.split('content/')[-1]
  150. self.path = path
  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. return markdown2.markdown(self.body) # , extras=['code-color'])
  202. @property
  203. def permalink(self):
  204. return "/%s/%s" % (KINDS[self.kind]['name_plural'], self.name)
  205. @property
  206. def tags(self):
  207. tags = list()
  208. for t in self.header['tags']:
  209. tags.append(Tag(t))
  210. return tags
  211. def prepare(self):
  212. file = codecs.open(self.abspath, 'r')
  213. header = ['---']
  214. while True:
  215. line = file.readline()
  216. line = line.rstrip()
  217. if not line:
  218. break
  219. header.append(line)
  220. self.header = yaml.load(StringIO('\n'.join(header)))
  221. for h in self.header.items():
  222. if h:
  223. try:
  224. setattr(self, h[0], h[1])
  225. except:
  226. pass
  227. body = list()
  228. for line in file.readlines():
  229. body.append(line)
  230. self.body = ''.join(body)
  231. file.close()
  232. if self.kind == 'link':
  233. from urlparse import urlparse
  234. self.domain_name = urlparse(self.url).netloc
  235. elif self.kind == 'photo':
  236. pass
  237. elif self.kind == 'note':
  238. pass
  239. elif self.kind == 'writing':
  240. pass
  241. def render(self):
  242. if not self.header['public']:
  243. return False
  244. try:
  245. os.makedirs(os.path.dirname(self.destination))
  246. except:
  247. pass
  248. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  249. context['entry'] = self
  250. template = jinja_env.get_template("entry.html")
  251. html = template.render(context)
  252. destination = codecs.open(
  253. self.destination, 'w', CONFIG['content_encoding'])
  254. destination.write(html)
  255. destination.close()
  256. return True
  257. class Link(Entry):
  258. def __init__(self, path):
  259. super(Link, self).__init__(path)
  260. @property
  261. def permalink(self):
  262. print "self.url", self.url
  263. raw_input()
  264. return self.url
  265. def entry_factory():
  266. pass
  267. def _sort_entries(entries):
  268. _entries = dict()
  269. sorted_entries = list()
  270. for entry in entries:
  271. _published = entry.header['published'].isoformat()
  272. _entries[_published] = entry
  273. sorted_keys = sorted(_entries.keys())
  274. sorted_keys.reverse()
  275. for key in sorted_keys:
  276. sorted_entries.append(_entries[key])
  277. return sorted_entries
  278. def render_index(entries):
  279. """
  280. this function renders the main page located at index.html
  281. under oz123.github.com
  282. """
  283. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  284. context['entries'] = entries[:10]
  285. template = jinja_env.get_template('entry_index.html')
  286. html = template.render(context)
  287. destination = codecs.open("%s/index.html" % CONFIG[
  288. 'output_to'], 'w', CONFIG['content_encoding'])
  289. destination.write(html)
  290. destination.close()
  291. def render_archive(entries, render_to=None):
  292. """
  293. this function creates the archive page
  294. """
  295. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  296. context['entries'] = entries[ARCHIVE_SIZE:]
  297. template = jinja_env.get_template('archive_index.html')
  298. html = template.render(context)
  299. if not render_to:
  300. render_to = "%s/archive/index.html" % CONFIG['output_to']
  301. dir_util.mkpath("%s/archive" % CONFIG['output_to'])
  302. destination = codecs.open("%s/archive/index.html" % CONFIG[
  303. 'output_to'], 'w', CONFIG['content_encoding'])
  304. destination.write(html)
  305. destination.close()
  306. def render_atom_feed(entries, render_to=None):
  307. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  308. context['entries'] = entries[:10]
  309. template = jinja_env.get_template('atom.xml')
  310. html = template.render(context)
  311. if not render_to:
  312. render_to = "%s/atom.xml" % CONFIG['output_to']
  313. destination = codecs.open(render_to, 'w', CONFIG['content_encoding'])
  314. destination.write(html)
  315. destination.close()
  316. def render_tag_pages(tag_tree):
  317. context = GLOBAL_TEMPLATE_CONTEXT.copy()
  318. for t in tag_tree.items():
  319. context['tag'] = t[1]['tag']
  320. context['entries'] = _sort_entries(t[1]['entries'])
  321. destination = "%s/tags/%s" % (CONFIG['output_to'], context['tag'].slug)
  322. try:
  323. os.makedirs(destination)
  324. except:
  325. pass
  326. template = jinja_env.get_template('tag_index.html')
  327. html = template.render(context)
  328. file = codecs.open("%s/index.html" %
  329. destination, 'w', CONFIG['content_encoding'])
  330. file.write(html)
  331. file.close()
  332. render_atom_feed(context[
  333. 'entries'], render_to="%s/atom.xml" % destination)
  334. def build():
  335. print
  336. print "Rendering website now..."
  337. print
  338. print " entries:"
  339. entries = list()
  340. tags = dict()
  341. for root, dirs, files in os.walk(CONFIG['content_root']):
  342. for fileName in files:
  343. try:
  344. if fileName.endswith('md') or fileName.endswith('markdown'):
  345. entry = Entry(os.path.join(root, fileName))
  346. except Exception, e:
  347. print "Found some problem in: ", fileName
  348. print e
  349. raw_input("Please correct")
  350. sys.exit()
  351. if entry.render():
  352. entries.append(entry)
  353. for tag in entry.tags:
  354. if tag.name not in tags:
  355. tags[tag.name] = {
  356. 'tag': tag,
  357. 'entries': list(),
  358. }
  359. tags[tag.name]['entries'].append(entry)
  360. print " %s" % entry.path
  361. print " :done"
  362. print
  363. print " tag pages & their atom feeds:"
  364. render_tag_pages(tags)
  365. print " :done"
  366. print
  367. print " site wide index"
  368. entries = _sort_entries(entries)
  369. render_index(entries)
  370. print "................done"
  371. print " archive index"
  372. render_archive(entries)
  373. print "................done"
  374. print " site wide atom feeds"
  375. render_atom_feed(entries)
  376. print "...........done"
  377. print
  378. print "All done "
  379. def preview(PREVIEW_ADDR='127.0.1.1', PREVIEW_PORT=11000):
  380. """
  381. launch an HTTP to preview the website
  382. """
  383. import SimpleHTTPServer
  384. import SocketServer
  385. Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
  386. httpd = SocketServer.TCPServer(("", CONFIG['http_port']), Handler)
  387. os.chdir(CONFIG['output_to'])
  388. print "and ready to test at http://127.0.0.1:%d" % CONFIG['http_port']
  389. print "Hit Ctrl+C to exit"
  390. try:
  391. httpd.serve_forever()
  392. except KeyboardInterrupt:
  393. print
  394. print "Shutting Down... Bye!."
  395. print
  396. httpd.server_close()
  397. def publish(GITDIRECTORY="oz123.github.com"):
  398. pass
  399. def clean(GITDIRECTORY="oz123.github.com"):
  400. directoriestoclean = ["writings", "notes", "links", "tags", "archive"]
  401. os.chdir(GITDIRECTORY)
  402. for directory in directoriestoclean:
  403. shutil.rmtree(directory)
  404. def dist(SOURCEDIR=os.getcwd()+"/content/", DESTDIR="oz123.github.com/writings_raw/content/"):
  405. """
  406. sync raw files from SOURCE to DEST
  407. """
  408. import subprocess as sp
  409. sp.call(["rsync", "-avP", SOURCEDIR, DESTDIR], shell=False, cwd=os.getcwd())
  410. if __name__ == '__main__':
  411. parser = argparse.ArgumentParser(
  412. description='blogit - a tool to blog on github.')
  413. parser.add_argument('-b', '--build', action="store_true",
  414. help='convert the markdown files to HTML')
  415. parser.add_argument('-p', '--preview', action="store_true",
  416. help='Launch HTTP server to preview the website')
  417. parser.add_argument('-c', '--clean', action="store_true",
  418. help='clean output files')
  419. parser.add_argument('-d', '--dist', action="store_true",
  420. help='sync raw files from SOURCE to DEST')
  421. args = parser.parse_args()
  422. if len(sys.argv) < 2:
  423. parser.print_help()
  424. sys.exit()
  425. if args.clean:
  426. clean()
  427. if args.build:
  428. build()
  429. if args.dist:
  430. dist()
  431. if args.preview:
  432. preview()