Commits

David Villa Alises  committed 5dca1d1

initial release

  • Participants

Comments (0)

Files changed (5)

+
+all: sample
+
+.PHONY: sample
+sample:
+	$(MAKE) -C sample
+
+check:
+	atheist -i3 -veo test/
+
+clean:
+	$(RM) *~

File src/kissite.py

+#!/usr/bin/python
+# -*- mode:python; coding:utf-8 -*-
+"""\
+usage: {0} <template.tmpl> <source xml files>
+       {0} -e <template.xml.tmpl>
+"""
+
+
+import sys, os
+import datetime
+import StringIO
+import subprocess as subp
+import logging
+logging.getLogger().setLevel(logging.DEBUG)
+
+import collections, operator
+
+from lxml import etree
+import jinja2
+
+
+class FakeFile(StringIO.StringIO):
+    def __init__(self, text, name=None):
+        self.name = name
+        StringIO.StringIO.__init__(self, text)
+
+
+class StringConstructible:
+    @classmethod
+    def fromstring(cls, text, fname='<str>'):
+        return cls(FakeFile(text, fname))
+
+
+class Node:
+    attrs = []
+
+    def __getattr__(self, name):
+        if name not in self.attrs:
+            raise AttributeError(name)
+
+        return self.node.attrib.get(name, '')
+
+    def content(self):
+        retval = u''
+        retval += self.node.text
+        for child in self.node.getchildren():
+            if child.tag != 'section':
+                retval += etree.tostring(child)
+        return retval
+
+    def __str__(self):
+        return etree.tostring(self.node)
+
+
+class Section(Node):
+    attrs = ['title', 'href']
+
+    def __init__(self, node):
+        self.node = node
+
+    def sections(self):
+        return [Section(x) for x in self.node.getchildren() if x.tag == 'section']
+
+
+class Page(Section, StringConstructible):
+    attrs = ['name', 'title', 'menu', 'order', 'baseof']
+
+    def __init__(self, fd):
+        fd.seek(0)
+        self.fd = fd
+        self.href = os.path.splitext(fd.name)[0]
+        content = fd.read()
+        try:
+            self.node = etree.fromstring(content)
+        except etree.XMLSyntaxError:
+            raise Exception("XML parse error in '{0}'".format(self.href))
+
+    def __eq__(self, other):
+        return self.fd == other.fd
+
+    def __repr__(self):
+        return '<Page {0}>'.format(self.name)
+
+
+class Template(StringConstructible):
+    def __init__(self, fd):
+        env = jinja2.Environment(
+            loader=jinja2.DictLoader({'template':fd.read().decode('utf-8')}))
+        self.template = env.get_template('template')
+
+        self.context = {'time': datetime.datetime.now()}
+
+    def render_page(self, page):
+        retval = self.template.render(page=page, **self.context)
+        return retval.strip()
+
+    def render_site(self, site):
+        retval = {}
+        for page in site.pages:
+            content = self.template.render(
+                site=site,
+                page=page,
+                **self.context)
+            retval[page.href] = content
+        return retval
+
+
+class Menu(list):
+    def __init__(self, name):
+        self.name = name
+        self.parent = None   # parent menu
+        self.base = None     # page base
+        list.__init__(self)
+
+    def show(self):
+        print "\nMenu '%s'" % self.name
+        print 'parent:', self.parent.name if self.parent else ''
+        print 'base:', self.base.name if self.base else ''
+
+        for page in self:
+            print '-', page.name
+        print
+
+
+class Site:
+    def __init__(self, fs, page_names):
+
+        def get_menu(name):
+            if not self.menus.has_key(name):
+                self.menus[name] = Menu(name)
+            return self.menus[name]
+
+        self.pages = []
+        for fname in page_names:
+            logging.info("loading {0}".format(fname))
+            self.pages.append(Page(fs[fname]))
+
+        self.menus = {'root': Menu('root')}
+        for page in self.pages:
+            if not page.menu: continue
+
+#            print page.name, '-->', page.menu
+            get_menu(page.menu).append(page)
+
+            if page.baseof:
+#                print page.name, '(base)-->', page.baseof
+                get_menu(page.baseof).base = page
+                get_menu(page.baseof).parent = self.menus[page.menu]
+
+        # sort menus by @order
+        for menu in self.menus.values():
+            menu.sort(key=operator.attrgetter('order'))
+
+        # put base as first element
+        for menu in self.menus.values():
+            if not menu.base: continue
+            if menu.base in menu:
+                menu.remove(menu.base)
+            menu.insert(0, menu.base)
+
+        # show menus
+#        for key,menu in self.menus.items():
+#            menu.show()
+
+
+class FSLoader(dict):
+    def __getitem__(self, name):
+        return file(name)
+
+def main(template, *pages):
+    tmpl = Template(file(template))
+    site = Site(FSLoader(), pages)
+    result = tmpl.render_site(site)
+
+    for name, content in result.items():
+        with file(name+'.html', 'w') as fd:
+            fd.write(content.encode('utf-8'))
+            logging.info("saving {0}".format(fd.name))
+
+
+def exec_scripts(fname):
+    """run 'exec' in the template and substitute output"""
+
+    def exec_cmd(cmd):
+        logging.info("'executing '%s'" % cmd)
+        return subp.Popen(cmd, shell=True, stdout=subp.PIPE).communicate()[0]
+
+    env = jinja2.Environment(loader=jinja2.FileSystemLoader(['.', '/']))
+    template = env.get_template(fname)
+    output = template.render(exec_cmd=exec_cmd)
+    print output.encode('utf-8')
+
+
+if __name__ == '__main__':
+    if len(sys.argv) < 2:
+        print __doc__.format( __file__)
+        sys.exit(1)
+
+    if sys.argv[1] == '-e':
+        exec_scripts(sys.argv[2])
+        sys.exit(0)
+
+    main(*sys.argv[1:])

File test/exec.test

+# -*- mode:python; coding:utf-8 -*-
+
+t1 = Test('echo {{ exec_cmd("uname -a") }}', save_stdout=True)
+t2 = Test('uname -a', save_stdout=True)
+t3 = Test('$basedir/src/kissite.py -e %s' % t1.stdout, save_stdout=True)
+t3.post += FileEquals(t3.stdout, t2.stdout)
+

File test/page.test

+# -*- mode:python; coding:utf-8 -*-
+import sys, os
+import unittest
+
+sys.path.insert(0, os.path.abspath(os.path.join((os.path.dirname(__file__)), '../src')))
+from kissite import Page, FakeFile
+
+
+class TestPage(unittest.TestCase):
+    def test_get_title(self):
+        title = "Sample title"
+        xml = '<page title="{0}"></page>'.format(title)
+        cut = Page.fromstring(xml)
+        self.assert_(cut.title == title)
+
+    def test_get_title_in_untitled_page(self):
+        xml = '<page></page>'
+        cut = Page.fromstring(xml)
+        self.assert_(cut.title == '')
+
+    def test_get_section_count(self):
+        xml = '''
+<page>
+  <section></section>
+  <section></section>
+</page>
+'''
+        cut = Page.fromstring(xml)
+        self.assert_(len(cut.sections()) == 2)
+
+    def test_section_title(self):
+        text = "this is a section"
+        xml = '''
+<page>
+  <section title="{0}">
+  </section>
+</page>
+'''.format(text)
+
+        sec = Page.fromstring(xml).sections()[0]
+        self.assert_(sec.title.strip() == text)
+
+
+    def test_section_content(self):
+        text = 'Hello <b>World</b> and bye'
+        xml = '''
+<page>
+  <section>
+  {0}
+  </section>
+</page>
+'''.format(text)
+
+        sec = Page.fromstring(xml).sections()[0]
+        self.assert_(sec.content().strip() == text)
+
+
+
+
+
+
+UnitTestCase(TestPage)

File test/template.test

+# -*- mode:python; coding:utf-8 -*-
+import sys
+from os.path import *
+import unittest
+import StringIO
+
+from mock import Mock
+
+sys.path.insert(0, abspath(join((dirname(__file__)), '../src')))
+from kissite import Site,  Page, Template, FakeFile
+
+
+class TestTemplate(unittest.TestCase):
+    def test_page_title(self):
+        text = 'This is the title'
+        page = Mock()
+        page.title = text
+
+        template = u'''
+<html>
+  <head>
+    <title>{{page.title}}</title>
+  </head>
+<html>
+'''
+
+        expected = template.replace('{{page.title}}', text)
+        cut = Template.fromstring(template)
+
+        self.assert_(cut.render_page(page) == expected.strip())
+
+
+    def test_sections(self):
+        title = "This is the title"
+        content = 'Hello <b>World</b>'
+        sec1 = Mock()
+        sec1.title = title
+        sec1.content = content
+
+        page = Mock()
+        page.sections = [sec1]
+
+        template = u'''
+<html>
+  <body>
+{% for sec in page.sections -%}
+<div class="section">
+<span class="title">{{sec.title}}</span>
+{{sec.content}}
+</div>{% endfor %}
+  </body>
+</html>
+'''
+        expected = u'''
+<html>
+  <body>
+<div class="section">
+<span class="title">{0}</span>
+{1}
+</div>
+  </body>
+</html>
+'''.format(title, content).strip()
+
+        cut = Template.fromstring(template)
+
+#        print repr(expected)
+#        print repr(cut.render_page(page))
+
+        self.assert_(cut.render_page(page) == expected)
+
+
+class TestSite2(unittest.TestCase):
+    def test_pages(self):
+        site_xml = '''
+<site>
+  <page href="index"/>
+</site>
+'''
+        index_xml = '''
+<page title="sample">
+</page>
+'''
+        fs = {'site.xml': FakeFile(site_xml, 'site.xml'),
+              'index.xml':  FakeFile(index_xml, 'index.xml')}
+
+        site = Site(fs, 'site.xml')
+        self.assert_(site.pages() == [Page(fs['index.xml'])])
+
+
+    def test_menu(self):
+        site_xml = '''
+<site>
+  <page href="index"/>
+  <page href="other"/>
+</site>
+'''
+        index_xml = '''
+<page name="home" title="Home">
+</page>
+'''
+        other_xml = '''
+<page name="other" title="Other">
+</page>
+'''
+        template = '''
+<html>
+  <body>
+{% for page in site.pages() %}
+<a href="{{page.href}}.html">{{page.name}}</a>
+{% endfor %}
+  </body>
+</html>
+'''
+        fs = {'site.xml':  FakeFile(site_xml,  'site.xml'),
+              'index.xml': FakeFile(index_xml, 'index.xml'),
+              'other.xml': FakeFile(other_xml, 'other.xml')}
+
+        site = Site(fs, 'site.xml')
+        tmpl = Template.fromstring(template)
+        retval = tmpl.render_site(site)
+
+        self.assert_(set(retval.keys()) == set(['index', 'other']))
+        for page in retval.values():
+            self.assert_('<a href="index.html">home</a>' in page)
+            self.assert_('<a href="other.html">other</a>' in page)
+
+
+    def test_page_not_found(self):
+        site_xml = '''
+<site>
+  <page href="index"/>
+</site>
+'''
+        template = '''
+{% for page in site.pages() %}
+{{page.name}}
+{% endfor %}
+'''
+
+        fs = {'site.xml':  FakeFile(site_xml,  'site.xml')}
+        cut = Site(fs, 'site.xml')
+        tmpl = Template.fromstring(template)
+        self.assertRaises(KeyError, tmpl.render_site, cut)
+
+
+
+class TestSite(unittest.TestCase):
+    first_xml  = '<page name="z-first"  menu="main" order="1"></page>'
+    second_xml = '<page name="r-second" menu="main" order="2"></page>'
+    third_xml  = '<page name="b-third"  menu="main" order="3"></page>'
+    foo_xml    = '<page name="foo"    menu="last"></page>'
+
+    fs = {'first.xml':  FakeFile(first_xml,  'first.xml'),
+          'third.xml':  FakeFile(third_xml,  'third.xml'),
+          'second.xml': FakeFile(second_xml, 'second.xml'),
+          'last.xml':   FakeFile(foo_xml,    'last.xml'),
+          }
+
+    def test_menu_items(self):
+        site = Site(self.fs, self.fs.keys())
+        self.assert_(sorted(site.menus.keys()) == ['last', 'main', 'root'])
+
+
+    def test_menu_order(self):
+        site = Site(self.fs, self.fs.keys())
+        self.assert_([p.name for p in site.menus['main']] == ['z-first', 'r-second', 'b-third'])
+
+
+
+UnitTestCase(TestTemplate)
+UnitTestCase(TestSite)