Dinu Gherman avatar Dinu Gherman committed d33c055

Initial checkin

Comments (0)

Files changed (6)

+.. -*- mode: rst -*-
+
+=========
+Quizcards
+=========
+
+-------------------------------------------------------------------
+Generate double-sided paper quiz cards with questions and answers
+-------------------------------------------------------------------
+
+:Author:  Dinu Gherman
+:Homepage: http://www.bitbucket.org/deeplook/quizcards
+:Version: Version 0.0.0
+:Date: 2013-01-13
+:Copyright: GNU Public Licence v3 (GPLv3)
+
+
+About
+-----
+
+`Quizcards` is a very simple tool, stripped-down from another project,
+that helps generate plain old, traditional do-it-yourself double-sided 
+paper quiz cards with questions on one side and answers on the other.
+
+
+Getting started
+----------------
+
+For now you can use quizcards like this from the command-line to create
+a PDF from a sample ReST document included with the code, after 
+unpacking it as a downloaded archive or a Mercurial repository::
+
+    $ cd quizcards/src
+    $ python quizcards.py -h
+    usage: quizcards.py [-h] [-V] PATH [PATH ...]
+    
+    Generate quiz cards as PDFs from Q&A files in ReST format.
+    
+    positional arguments:
+      PATH           Path of a ReST file.
+    
+    optional arguments:
+      -h, --help     show this help message and exit
+      -V, --verbose  Set verbose output.
+
+    $ python quizcards.py ../samples/python.rst
+    $
+
+Dependencies
+-------------
+
+- docutils
+- reportlab
+
+
+Features
+--------
+
+...
+
+
+Examples
+--------
+
+...
+
+
+History
+-------
+
+...
+
+
+Installation
+------------
+
+...
+
+
+Testing
+-------
+
+...
+
+
+Bug reports
+-----------
+
+...

Binary file added.

+%PDF-1.4
+%���� ReportLab Generated PDF document http://www.reportlab.com
+% 'BasicFonts': class PDFDictionary 
+1 0 obj
+% The standard fonts dictionary
+<< /F1 2 0 R >>
+endobj
+% 'F1': class PDFType1Font 
+2 0 obj
+% Font Helvetica
+<< /BaseFont /Helvetica
+ /Encoding /WinAnsiEncoding
+ /Name /F1
+ /Subtype /Type1
+ /Type /Font >>
+endobj
+% 'Page1': class PDFPage 
+3 0 obj
+% Page dictionary
+<< /Contents 8 0 R
+ /MediaBox [ 0
+ 0
+ 841.8898
+ 595.2756 ]
+ /Parent 7 0 R
+ /Resources << /Font 1 0 R
+ /ProcSet [ /PDF
+ /Text
+ /ImageB
+ /ImageC
+ /ImageI ] >>
+ /Rotate 0
+ /Trans <<  >>
+ /Type /Page >>
+endobj
+% 'Page2': class PDFPage 
+4 0 obj
+% Page dictionary
+<< /Contents 9 0 R
+ /MediaBox [ 0
+ 0
+ 841.8898
+ 595.2756 ]
+ /Parent 7 0 R
+ /Resources << /Font 1 0 R
+ /ProcSet [ /PDF
+ /Text
+ /ImageB
+ /ImageC
+ /ImageI ] >>
+ /Rotate 0
+ /Trans <<  >>
+ /Type /Page >>
+endobj
+% 'R5': class PDFCatalog 
+5 0 obj
+% Document Root
+<< /Outlines 10 0 R
+ /PageMode /UseNone
+ /Pages 7 0 R
+ /Type /Catalog >>
+endobj
+% 'R6': class PDFInfo 
+6 0 obj
+<< /Author (anonymous)
+ /CreationDate (D:20130113171103-01'00')
+ /Creator (ReportLab PDF Library - www.reportlab.com)
+ /Keywords ()
+ /Producer (ReportLab PDF Library - www.reportlab.com)
+ /Subject (unspecified)
+ /Title (untitled) >>
+endobj
+% 'R7': class PDFPages 
+7 0 obj
+% page tree
+<< /Count 2
+ /Kids [ 3 0 R
+ 4 0 R ]
+ /Type /Pages >>
+endobj
+% 'R8': class PDFStream 
+8 0 obj
+% page stream
+<< /Filter [ /ASCII85Decode
+ /FlateDecode ]
+ /Length 1145 >>
+stream
+Gau`SD/\/e&H6R_s5?.&RE_+F<sicrL+_4g$r(6)h<KB\--B[IOfh6oc$oUdPV'M$PH$o)_h'>.C#8I:*M9)T9^tL;G>\M?15CeF-r:02),QB=0V?2)q/ES:+_p?u#nG1$]0m>EcOYUEr.#DET&Q7Q7VfHpeKm1p#bSo+;^fgD,cjTk!W3dO&6#s,_>HI?cg@0"]AW"Uh-IE,%S/R=Jq1^M32#d(E:D7]44eIC_Us&VnD00_'9S3DY[?;gkS0Nk>"8f`+e-l%c*\-UlH;OKfCh8jCJ&sL3pMY:Ga"%2LHAgVF:'LM=8+&^]>^][+#pnZIuclFZNJZZaZk\>fJ!a]kFa*@OVqp_P[#AemN4b^XZ7qXVCLGJEe!ApcO9\8r9.",cAj";C2fIY\YU4L'P[_:J&X&$R?0N7KXhJB2O0@Sh&!(5Ig5RjZ5[JF`B/\m)&Or=(g8TideeV@I@stc8A`rpYX892(F)VrjMrk?Su=gH,cEHMl!gIS-+k14_^^9`Qg*mS90l+t'>G3P"M_+_P#XF(mn3;Qi:?RJ_5HoNi/KG0PH)T]r.b=%mt(9/:.E;>PIb-+@2lX+30m4[$Z:3PZV%^3n+Q<>PZh*5E^J*MnE&;YrIMo1I5A^Z5ls!(J-I0:4HG*SLVuXb2=9P1>?jChFZ9DVeW%9BUNPpOd9`s8GXC1UV3PIg^7q``d,lk$g*-,a<-:"6ena.+PZkfaM.qi`!j)o8',]9)6u,R^_'qFaN!P:-f<kF(!ppYks7$o<Rf30MbO_=`+);_>RLE/G<0^a]Rt/fM/41k"C'fRPV/Fq-q.NpEkXg[rdRTmq(,+2YEk5aLc!+YZ#r5?u*2HPse5aJ!_OF84B9Rf"h"[!io;p_h/i"t;Q"?DR?..)AR'PiPSt_b\CSc_b1-C<jE&"W';2J:j/ddW^Sb8iHpGtAW59"qPDXp9>E2uW4@PUngJ[o-/NT'=;1TeVgQ<YUQ\^`S>^Z4kG'':.`-t,'D=XF<<).%mV+)@?<]4,,61k%J/T>4^J`enr6au,CWOGXaTC69ts-e[I+XYcVe``C\1OcTe-hn9qe_:Ib"m^8!3Wqq6?f-]fUWE$cqW;AM3@)mK]GoD*@".&X(ngK?h(uLHbT/`MdCo>-hn/i[u=fM~>endstream
+endobj
+% 'R9': class PDFStream 
+9 0 obj
+% page stream
+<< /Filter [ /ASCII85Decode
+ /FlateDecode ]
+ /Length 835 >>
+stream
+Gaua<?Z4XP'ZJslp^TgJ$f-m`)s8<k*]@l9<:usPG['nRL^BJ?SCpJbIJVcq1f0E.l)Lj0,)Nl*riXT;22=O@hL$t3i8Y7/?T>R2C^D_^`9[i'B>_RbLS,\!R<LL3pJ\Rf^j$F-of."[LI*bTMPs?`$5*-e6?!042T?prnJ#q0)FMS[ljU3BR([@6FIN#C0-6`Z1)r"*#s=8Y1E:@=%L54fX3DW1bR4TNT(oAsUf*Y3_ZWK"%Rc<-<^T\_bihE32p0e;1!343F_5Z=Y=tLd35`/Oo4YkpJ^t1^5VU#.1HVE.]s;4<6tMf7O"@&sY)HBH-$5/fV/WKd.^I3kL_9m)C]daM=\R/f2k(7,G3)?Z9Ug_.X%E/*T:1";9f5+cfdb/hPQ"J$'W$j&CSK$q$+3KEflA^Ek3E3_[PUs`m=9TBA:a6HbAhj3*HL5!]JUh"NHl1%!;CjfUuW)icU(\90FD0J*Hq$5G*=+\;?jtP\(`&>mq&tGBdOA=eBTi2(adC8SWQ<MckGaq>on*5:sG/>#LRK9esbI-V!<Pt_$g/d)ec8`O_2#_$Z0`A?df#2P<;e>J\'VOWp`hX076+:/QGNMCs]?SaGnRfp\cG^Thh5D_uCjg9s`gcqilEaHgmWNmc5+MUFN67<IBDsY!;[&#5LL=[7?i3J(p!gqd(rM(nrCgi3,CP<i7:T)qF;!B?u1%o\4WHLQfo#;5LZ"1OW'IV`?B)]Sbod&e:P,eEsF9A99QX>IMlr2QLr)6<,+!qbC!I%GJ:jC#]Srj(^mI`6]UcBSSPEj\9(hLP8Vi\*N5%9-?5&JrohEkVD,/hEDXk_/I0<';Y~>endstream
+endobj
+% 'R10': class PDFOutlines 
+10 0 obj
+<< /Count 0
+ /Type /Outlines >>
+endobj
+xref
+0 11
+0000000000 65535 f
+0000000113 00000 n
+0000000209 00000 n
+0000000372 00000 n
+0000000649 00000 n
+0000000926 00000 n
+0000001061 00000 n
+0000001343 00000 n
+0000001456 00000 n
+0000002742 00000 n
+0000003720 00000 n
+trailer
+<< /ID 
+ % ReportLab generated PDF document -- digest (http://www.reportlab.com) 
+ [(\316N\001\367%\014:\336cR\204\270\351?\330\245) (\316N\001\367%\014:\336cR\204\270\351?\330\245)] 
+
+ /Info 6 0 R
+ /Root 5 0 R
+ /Size 11 >>
+startxref
+3772
+%%EOF

samples/python.rst

+.. Questions and answers for quiz cards about Python.
+
+
+.. container:: question
+
+    Which version of Python (like X.Y, not like X.Y.Z) was the one that was longest in use before version 3?
+
+.. container:: answer
+
+    Probably 1.5 or 2.2. Try investigating yourself on the `Releases <http://www.python.org/getit/releases/>`_ page of http://www.python.org.
+
+
+.. container:: question
+
+    Which of the methods below is *not* defined for Python strings? 
+
+    - ``S.capitalize() -> string``
+    - ``S.find(sub [,start [,end]]) -> int``
+    - ``S.summarize([maxlen]) -> string``
+    - ``S.title() -> string``
+    - ``S.translate(table [,deletechars]) -> string``
+
+    .. generated with:
+        for n in dir(""):
+            if not n.startswith("_"):
+                print getattr("", n).__doc__.split("\n")[0]
+
+.. container:: answer
+
+    The ``summarize`` method does not exist for Python strings.
+
+    
+.. container:: question
+
+    Which of the methods below are *not* defined for Python lists? 
+
+    - ``L.append(object) -- append object to end``
+    - ``L.invert() -- invert *IN PLACE*``
+    - ``L.randomize() -- shuffle list items *IN PLACE*;``
+    - ``L.remove(value) -- remove first occurrence of value.``
+    - ``L.shift(offset) -- shift list items by offset positions (+/- for right/left).``
+
+    .. generated with:
+        for n in dir([]):
+            if not n.startswith("_"):
+                print "    - ``%s``" % getattr([], n).__doc__.split("\n")[0]
+
+.. container:: answer
+
+    The ``invert``, ``randomize``, and ``shift`` methods do not exist for Python lists.
+
+    
+.. container:: question
+
+    Show and name four types of assignment in Python. Hint: Two are named after sequence types.
+    
+    .. http://www.linkedin.com/groupItem?view=&gid=1846027&type=member&item=197213748
+
+
+.. container:: answer
+
+    These are the possible assignment types:
+    
+    - simple assignment: x = 1
+    - augmented assignment: x += 1
+    - list assignment: [x, y] = 1, 2
+    - tuple assignment: (x, y) = 1, 2
+
+
+.. container:: question
+
+    To which company did Guido van Rossum, the inventor of Python, move to after working for Google?
+
+
+.. container:: answer
+
+    Guido moved in January 2013 to Dropbox.com which uses Python for their product from day #1.
+
+#!/usr/bin/env python
+# _*_ coding: UTF-8 _*_
+
+"""Mini-page like double-sided cards to be printed on paper.
+"""
+
+import os.path
+import random
+from types import StringType, UnicodeType
+from cStringIO import StringIO
+
+from reportlab.lib import colors
+from reportlab.lib.units import mm, cm
+from reportlab.lib.pagesizes import A3, A4, A5, A6, LETTER, LEGAL
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.pdfgen.canvas import Canvas
+from reportlab.platypus.flowables import Image, Spacer, KeepInFrame
+from reportlab.platypus.paragraph import Paragraph
+
+
+# Utils
+
+def reverseSeq(seq):
+    "Return reversed sequence, being either a list, tuple or string."
+
+    seqList = [el for el in seq]
+    seqList.reverse()
+
+    if type(seq) is tuple:
+        res = tuple(seqList)
+    elif type(seq) is str:
+        res = "".join(seqList)
+    else:
+        res = seqList
+
+    return res
+
+
+class Record(object):
+    def __init__(self, **kwdict):
+        for k, v in kwdict.items():
+            setattr(self, k, v)
+
+
+# Core stuff
+
+class Card(object):
+    "A mini-page like card."
+
+    lm, rm = 3 * mm, 3 * mm  # left/right margin
+    tm, bm = 3 * mm, 3 * mm  # top/bottom margin
+    fn, fs = "Helvetica", 10
+
+    def __init__(self, frame=False, debug=False, **dikt):
+        # set received vars as instance variables
+        self.frame = frame
+        self.debug = debug
+        for k, v in dikt.items():
+            setattr(self, k, v)
+
+    def render(self, **dikt):
+        for k, v in dikt.items():
+            setattr(self, k, v)
+
+        # set derived instance variables
+        self.width, self.height = self.size
+        self.xp = self.x0 + self.lm
+        self.yp = self.y0 + self.height - self.tm
+
+        # start drawing
+        self.begin()
+        self.draw()
+        self.end()
+
+    def begin(self):
+        canv = self.canv
+        canv.setStrokeColor(colors.black)
+        canv.setFillColor(colors.black)
+        canv.setLineWidth(0.25 * mm)
+        canv.setLineCap(1)
+
+    def addSpace(self, x=0, y=0):
+        "Add empty space."
+
+        self.xp += x
+        self.yp += y
+
+    def drawFrame(self):
+        if self.frame:
+            c = self.canv
+            c.saveState()
+            c.setStrokeColor(colors.lightgrey)
+            c.setLineWidth(0.25 * mm)
+            c.rect(self.x0, self.y0, self.width, self.height)
+            c.setStrokeColor(colors.black)
+            c.restoreState()
+
+    def end(self):
+        if self.frame:
+            self.drawFrame()
+
+    def draw(self):
+        pass
+
+
+class EmptyCard(Card):
+
+    pass
+
+
+class LabelCard(Card):
+
+    def draw(self):
+        self.canv.saveState()
+
+        fn, fs = 'Helvetica-Bold', 18
+        self.canv.setFont(fn, fs)
+        x, y = self.x0 + self.width / 2.0, self.y0 + self.height / 2.0
+        y -= fs / 1.2 / 2.0
+        self.canv.drawCentredString(x, y, self.title)
+
+        self.canv.restoreState()
+
+
+class QuizCardRest(Card):
+    "Quiz cards using ReST input."
+
+    def resize_images(self, story):
+        styleSheet = getSampleStyleSheet()
+        bt = styleSheet['BodyText']
+        bt.fontName = "Helvetica"
+        bt.fontSize = 8
+        bt.leading = bt.fontSize * 1.25
+
+        # replace images with resized ones fitting into the available width
+        W, H = (
+            self.width - self.lm - self.rm), self.height - self.tm - self.bm
+        for i, el in enumerate(story):
+            if el.__class__ == Image:
+                img = PIL.Image.open(el.filename)
+                h = W / img.size[0] * img.size[1]
+                img = Image(el.filename, width=w, height=h, kind='direct',
+                            mask="auto", lazy=1)
+                story[i] = img
+            elif type(el) in (StringType, UnicodeType):
+                story[i] = Paragraph(el, bt)  # Spacer(0, 0)
+
+    def fits(self, flowables):
+        # TODO: make more configurable
+        styleSheet = getSampleStyleSheet()
+        bt = styleSheet['BodyText']
+        bt.fontName = "Helvetica"
+        bt.fontSize = 8
+        bt.leading = bt.fontSize * 1.25
+
+        story = flowables
+        self.resize_images(story)
+
+        W, H = (
+            self.width - self.lm - self.rm), self.height - self.tm - self.bm
+        canv = Canvas(StringIO(), (W, H))
+        total_height = sum([el.wrapOn(canv, W, 0)[1] + bt.spaceBefore
+                            for el in story])
+        if getattr(self, "verbose", False) == True:
+            print "***", total_height / mm, H / mm, \
+                [txt.text[:5] for txt in story]
+        return total_height < H
+
+    def draw(self):
+        self.canv.saveState()
+
+        XP, YP = self.xp, self.yp
+
+        styleSheet = getSampleStyleSheet()
+        bt = styleSheet['BodyText']
+        bt.fontName = "Helvetica"
+        bt.fontSize = 8
+        bt.leading = bt.fontSize * 1.25
+
+        self.resize_images(self.text)
+        story = self.text
+        W, H = (self.width - self.lm - self.rm), self.yp - self.y0 - self.bm
+        # possible modes: shrink, truncate, overflow, error
+        frame = KeepInFrame(W, H, story, mode="shrink")
+        w, h = frame.wrapOn(self.canv, W, H)
+        frame.drawOn(self.canv, self.xp, self.yp - h)
+        self.addSpace(0, -h - 5 * mm)
+
+        self.canv.restoreState()
+
+
+# Layouting
+
+def maximizeCardsOnPage(cardSize, pageSize, margins=None, autoRotate=False):
+    "Maximize number of cards on a page, perhaps turning page 90 degrees."
+
+    if margins is None:
+        margins = Record(lm=0, rm=0, tm=0, bm=0)
+
+    f1 = (pageSize[0] - margins.lm - margins.rm) / cardSize[0]
+    f2 = (pageSize[1] - margins.bm - margins.tm) / cardSize[1]
+    i1, i2 = int(f1), int(f2)
+    n1 = i1 * i2
+
+    g1 = (pageSize[0] - margins.lm - margins.rm) / cardSize[1]
+    g2 = (pageSize[1] - margins.bm - margins.tm) / cardSize[0]
+    j1, j2 = int(g1), int(g2)
+    m1 = j1 * j2
+
+    if autoRotate and m1 > n1:
+        i1, i2 = j1, j2
+        pageSize = reverseSeq(pageSize)
+        i1, i2 = i2, i1
+
+    if autoRotate and pageSize[0] <= pageSize[1]:
+        if cardSize[0] > cardSize[1]:
+            i1, i2 = i2, i1
+
+    newPageSize = pageSize
+    numCardsX, numCardsY = i1, i2
+
+    return newPageSize, numCardsX, numCardsY
+
+
+def addSizeInfoLabel(canv, x, y, pageSize, cardSize):
+    "Put info label about used sizes on a page."
+
+    canv.setFont("Helvetica", 8)
+    args = (pageSize + cardSize)
+    args = tuple([a / mm for a in args])
+    desc = "paper: %3.0f x %3.0f mm, card: %3.0f x %3.0f mm" % args
+    canv.drawString(x, y + cardSize[1] + 6, desc)
+
+
+def layout(path, cards, pageSize, cardSize,
+           autoRotate=True, frame=False, debug=False):
+    "Make double-sided multi card sheet with all different cards per sheet."
+
+    nppm = Record(lm=7.21 * mm, rm=7.21 * mm, tm=15.1 * mm, bm=15.1 * mm)
+    pageSize, i1, i2 = maximizeCardsOnPage(
+        cardSize, pageSize, margins=nppm, autoRotate=autoRotate)
+
+    canv = Canvas(path, pagesize=pageSize)
+    xc, yc = pageSize[0] / 2.0, pageSize[1] / 2.0
+    cw, ch = cardSize
+
+    # regroup pages for double-sided printing
+    cardGroups = []
+    k = 0
+    for j, card in enumerate(cards):
+        if j > 0 and j % (2 * i1 * i2) == 0:
+            cardGroups.append(cards[k:j:2])
+            cardGroups.append(cards[k + 1:j + 1:2])
+            k = j
+    cardGroups.append(cards[k::2])
+    cardGroups.append(cards[k + 1::2])
+
+    # place cards on paper
+    for g, cardGroup in enumerate(cardGroups):
+        x0, y0 = xc - i1 * cw / 2., yc - i2 * ch / 2.
+        r = range
+        if g % 2 == 0:
+            coords = [(x0 + x * cw, y0 + y * ch)
+                      for y in reverseSeq(r(i2)) for x in r(i1)]
+        else:
+            coords = [(x0 + x * cw, y0 + y * ch)
+                      for y in reverseSeq(r(i2)) for x in reverseSeq(r(i1))]
+        j = 0
+        while j < len(cardGroup):
+            obj = cardGroup[j]
+            if g % 2 != 0:
+                if hasattr(obj, "lm"):
+                    obj.rm = obj.lm
+                    delattr(obj, "lm")
+            x, y = coords[j % (i1 * i2)]
+            if j % (i1 * i2) == 0:
+                addSizeInfoLabel(canv, x, y, pageSize, cardSize)
+            obj.render(canv=canv, x0=x, y0=y, size=cardSize,
+                       frame=frame, debug=debug)
+            j += 1
+        if frame:
+            canv.rect(0, 0, pageSize[0], pageSize[1])
+        canv.showPage()
+
+    canv.save()
+#!/usr/bin/env python
+# _*_ coding: UTF-8 _*_
+
+"""Programm to generate double-sided Hipster-PDA-style quiz cards.
+"""
+
+import sys
+import os.path
+import readline
+import argparse
+
+from reportlab.lib.units import mm, cm
+from reportlab.lib.pagesizes import A4
+from docutils.core import publish_parts
+
+from rstplaty import SimplePlatypusWriter, bt, Paragraph
+from cards import LabelCard, QuizCardRest, layout
+
+
+def rst_to_items(path):
+    "Convert a ReST file into Q&A items."
+
+    rst = open(path).read()
+    docutils_settings = {}
+    writer = SimplePlatypusWriter()
+    parts = publish_parts(source=rst, 
+            writer=writer,
+            settings_overrides=docutils_settings)
+    
+    q_stories = writer.visitor.stories["q"]
+    a_stories = writer.visitor.stories["a"]
+    assert len(q_stories) == len(q_stories)
+    
+    items = [
+        {"q": q_stories[i], "a": a_stories[i]} 
+            for i in range(len(q_stories))
+    ]
+
+    return items
+
+
+def make_cards_platypus(cardSize, items, verbose=False):
+    "Generate q/a sides of quiz cards from a list of quiz items."
+    # items are Platypus flowables created from ReST!
+            
+    cw, ch = cardSize
+    kwDict = {"lm": 4*mm, "rm": 4*mm, "text": "", 
+        "width": cw, "height": ch, "verbose": verbose}
+    q_side = QuizCardRest(**kwDict)
+    a_side = QuizCardRest(**kwDict)
+
+    questions = [Paragraph("Questions:", bt)]
+    answers = [Paragraph("Answers:", bt)]
+    
+    for i, item in enumerate(items):
+        q = item["q"]
+        a = item["a"]
+        if q_side.fits(questions + q) and a_side.fits(answers + a):
+            questions += q
+            answers += a
+        else:
+            q_side.text = questions
+            a_side.text = answers
+            yield q_side, a_side
+            questions = [Paragraph("Questions:", bt)] + q
+            answers = [Paragraph("Answers:", bt)] + a
+            q_side = QuizCardRest(**kwDict)
+            a_side = QuizCardRest(**kwDict)
+    
+    q_side.text = questions
+    a_side.text = answers
+    yield q_side, a_side
+
+
+def make(path, items, kind, verbose=False):
+    "Make cards with as many items as possible, shrinking too big ones."
+    
+    cardSize = (5.9*cm, 9.1*cm)
+
+    cards = []
+    if kind.lower() == "rst":
+        for q, a in make_cards_platypus(cardSize, items, verbose=verbose):
+            cards += [q, a]
+    
+    pageSize = A4
+    outPath = os.path.splitext(path)[0] + ".pdf"
+    layout(outPath, cards, pageSize, cardSize, frame=True, debug=False)
+
+    if verbose:
+        print "wrote %s" % outPath
+
+
+def _main():
+    desc = "Generate quiz cards as PDFs from Q&A files in ReST format."
+    parser = argparse.ArgumentParser(description=desc)
+    paa = parser.add_argument
+    paa("-V", "--verbose", action="store_true", help="Set verbose output.", 
+        dest="verbose", default=False)
+    paa("paths", metavar='PATH', help="Path of a ReST file.", nargs='+')
+
+    options = parser.parse_args()
+    for path in options.paths:
+        if path.lower().endswith(".rst"):
+            items = rst_to_items(path)
+            make(path, items, "rst", verbose=options.verbose)
+
+
+if __name__ == "__main__":
+    _main()
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+"""An experimental Reportlab Platypus writer for ReStructuredText.
+
+Handles ReST like this:
+
+- .. container:: question
+
+    foo
+
+  .. container:: answer
+
+    bar
+"""
+
+# Inspired by:
+# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
+# http://docutils.sourceforge.net/sandbox/dreamcatcher/rlpdf/rlpdf.py
+# http://code.google.com/p/python-creole/wiki/rest2html
+# https://github.com/jedie/python-creole/blob/master/creole/rest2html/clean_writer.py
+
+
+import os
+
+from docutils.core import publish_parts
+from docutils.writers.html4css1 import HTMLTranslator, Writer
+from reportlab.lib.styles import getSampleStyleSheet
+from reportlab.platypus import Paragraph, Image, PageBreak
+
+
+# TODO: make this more configurable
+styleSheet = getSampleStyleSheet()
+bt = styleSheet['BodyText']
+bt.fontName = "Helvetica"
+bt.fontSize = 8
+bt.leading = bt.fontSize * 1.25
+
+
+BaseTranslator = HTMLTranslator
+
+
+class SimplePlatypusTranslator(BaseTranslator):
+    """A docutils translator for Q&A cards.
+
+    Collecting paragraphs into seperate Platypus stories, one for
+    questions and one for answers.
+    """
+
+    def __init__(self, *args, **kwdict):
+        BaseTranslator.__init__(self, *args, **kwdict)
+
+        # a dict with one list of question stories and
+        # one list of answer stories (one story per question, one per answer)
+        self.stories = {"q": [], "a": [], None: []}
+
+        # the current story, either question or answer
+        self.curr_story = []
+
+        # flag to indicate if self.curr_story is a question or answer
+        self.current_card_side = None
+
+        # number of current question/answer
+        self.qa_number = 1
+
+        self.bullet_level = 0
+
+    # container stuff
+
+    def visit_container(self, node):
+        if "question" in node['classes']:
+            self.current_card_side = "q"
+        elif "answer" in node['classes']:
+            self.current_card_side = "a"
+        self.curr_story = []
+
+    def depart_container(self, node):
+        ccs = self.current_card_side
+        if ccs == "q":
+            self.stories[ccs].append(self.curr_story)
+        elif ccs == "a":
+            self.stories[ccs].append(self.curr_story)
+            self.qa_number += 1
+        self.current_card_side = None
+        self.curr_story = []
+
+    # paragraph stuff
+
+    def visit_paragraph(self, node):
+        # raw, unprocessed input
+        if self.bullet_level == 0:
+            if len(self.curr_story) == 0:
+                t = "%d. %s" % (self.qa_number, node.astext())
+            else:
+                t = node.astext()
+            p = Paragraph(t, bt)
+            self.curr_story.append(p)  # raw, unprocessed input
+
+    def depart_paragraph(self, node):
+        pass
+
+    def visit_Text(self, node):
+        pass
+
+    # list markup
+
+    def visit_bullet_list(self, node):
+        self.bullet_level += 1
+        BaseTranslator.visit_bullet_list(self, node)
+
+    def depart_bullet_list(self, node):
+        self.bullet_level -= 1
+        BaseTranslator.depart_bullet_list(self, node)
+
+    def visit_enumerated_list(self, node):
+        self.bullet_level += 1
+        BaseTranslator.visit_enumerated_list(self, node)
+
+    def depart_enumerated_list(self, node):
+        self.bullet_level -= 1
+        BaseTranslator.depart_enumerated_list(self, node)
+
+    def visit_list_item(self, node):
+        # apparently for enumeration and bullet lists
+        # print "visit_list_item", self.bullet_level, node.astext()
+        # ... node.parent["classes"]
+        lev = self.bullet_level
+        indent = 10 * (lev - 1)
+        args = (indent, indent + 10, node.astext())
+        t = "<para bulletIndent='%d' leftIndent='%d' firstLineIndent='0'>"
+        t += "%s</para>"
+        t = t % args
+        p = Paragraph(t, bt, bulletText="•")
+        self.curr_story.append(p)  # raw, unprocessed input
+
+    def depart_list_item(self, node):
+        pass
+
+    # intra-paragraph markup, not yet used...
+
+    def visit_emphasis(self, node):
+        self.body.append(self.starttag(node, 'i', ''))
+
+    def depart_emphasis(self, node):
+        self.body.append('</i>')
+
+    def visit_strong(self, node):
+        self.body.append(self.starttag(node, 'b', ''))
+
+    def depart_strong(self, node):
+        self.body.append('</b>')
+
+    # literals with ``...``? (-> <font face="courier">...</font>)
+
+    # images
+
+    def visit_image(self, node):
+        uri = node['uri']
+        img = Image(uri, lazy=1)
+        self.curr_story.append(img)
+
+    def depart_image(self, node):
+        pass
+
+
+class SimplePlatypusWriter(Writer):
+    "A docutils writer creating a simple Platypus story."
+
+    def __init__(self):
+        Writer.__init__(self)
+        self.translator_class = SimplePlatypusTranslator
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.