49dd47c7e542 — Josiah Ulfers 10 years ago
Merged the problematic line ending change that I thought might help, but didn't
2 files changed, 563 insertions(+), 556 deletions(-)

M dss.py
M test.py
M dss.py +271 -265
@@ 1,265 1,271 @@ 
-#!/usr/bin/env python
-import argparse
-import fnmatch
-import os
-from os import path
-import re
-import shutil
-from SimpleHTTPServer import SimpleHTTPRequestHandler
-import SocketServer
-import string
-import subprocess
-import sys
-
-from docutils.core import publish_parts
-
-# TODO: ie http-equiv tag
-# TODO: generator meta tag?
-default_template = string.Template('''<!DOCTYPE html>
-<html><head>
-    $favicon
-    $title
-    $stylesheet
-    $scripts
-</head><body>
-    $content
-</body></html>
-''')
-
-def _normpath(p):
-    return path.normcase(path.normpath(path.abspath(p)))
-
-class GitError(Exception):
-
-    def __init__(self, error):
-        if isinstance(error, subprocess.CalledProcessError):
-            Exception.__init__(self, 'Failed: %s\n%s' % (error.cmd, error.output))
-        else:
-            Exception.__init__(self, error)
-
-# TODO: show fatal rst syntax errors, e.g. include file that doesn't exist
-class LiveSiteHandler(SimpleHTTPRequestHandler):
-    '''
-    Can extend server behavior by replacing this class with a subclass of itself that overrides the
-    do_* methods as usual.
-    '''
-
-    def do_GET(self):
-        # TODO: expire immediately
-        # TODO: race condition when reloading during a compile
-        # TODO: tests for this
-        self.server.site.compile()
-        prev_cwd = os.getcwd()
-        try:
-            os.chdir(self.server.site.target)
-            SimpleHTTPRequestHandler.do_GET(self)
-        finally:
-            os.chdir(prev_cwd)
-
-class DeadSimpleSite(object):
-
-    def __init__(self, source):
-        self.source = path.abspath(source)
-        self.target = path.join(self.source, '_site')
-
-    def _clean(self):
-        for root, dirs, files in os.walk(self.target, topdown=False):
-            for filename in files:
-                target = path.join(root, filename)
-                relpath = path.relpath(target, self.target)
-                if relpath[0] == '.' and filename != '.htaccess':
-                    break
-                os.remove(target)
-            if path.relpath(root, self.target)[0] != '.' and len(os.listdir(root)) == 0:
-                os.rmdir(root)
-
-    def _git(self, *args):
-        if not path.exists(self.target):
-            os.makedirs(self.target)
-        with open(os.devnull, 'w') as devnull:
-            if args[0] == 'push':
-                # Git reads username and password from console, so don't suppress output
-                # Also, don't want to throw an error since most likely the user just mistyped
-                subprocess.call(('git',) + args, cwd=self.target)
-            else:
-                return subprocess.check_output(('git',) + args, stderr=subprocess.STDOUT, cwd=self.target)
-
-    def _render(self, source, template):
-        relpath = path.relpath(source, self.source)
-        depth = len(re.split(r'[/\\]', relpath)) - 1
-        html_root = depth * '../'
-        style_tag = '<link rel="stylesheet" href="%sstyle/global.css">' % html_root \
-            if path.exists(path.join(self.source, 'style/global.css')) else ''
-        script_tags = '\n'.join(['<script src="%s%s"></script>' % (html_root, script)
-            for script in self._scripts()])
-        favicon_tag = '<link rel="shortcut icon" href="favicon.ico">' \
-            if path.exists(path.join(self.source, 'favicon.ico')) else ''
-        with open(source) as source_file:
-            parts = publish_parts(
-                source=source_file.read(),
-                source_path=source,
-                writer_name='html',
-                # TODO: smart quotes on
-                # TODO: going to need something like this to get sensible title behavior
-                #       also, I don't like the default docinfo, e.g. authors, copyright, they
-                #       are related because both depend on being the first thing in the source
-                #settings_overrides={'doctitle_xform': False}
-                )
-        return template.substitute(
-            content = parts['html_body'],
-            favicon = favicon_tag,
-            title = '<title>%s</title>' % parts['title'],
-            root = html_root,
-            stylesheet = style_tag,
-            scripts = script_tags)
-
-    def _scripts(self):
-        scripts = []
-        for root, dirs, files in self._walk_source():
-            # TODO: this walks the output directory - that's bad
-            for script in fnmatch.filter(files, '*.js'):
-                if script[0] == '.':
-                    continue
-                script_path = path.join(root, script)
-                relpath = path.relpath(script_path, self.source)
-                # In *.nix, paths may contain backslashes. Don't worry about psychos who do that.
-                scripts.insert(0, relpath.replace('\\', '/')) # for Windows
-        return scripts
-
-    def _walk_source(self):
-        for root, dirs, files in os.walk(self.source):
-            dirs.sort()
-            files.sort()
-            remove = [d for d in dirs if d[0] == '.' or d == '_site']
-            for dirname in remove:
-                dirs.remove(dirname)
-            yield root, dirs, [f for f in files if f[0] != '.' or f == '.htaccess']
-
-    def compile(self):
-        self._clean()
-        for root, dirs, files in self._walk_source():
-            rel = path.relpath(root, self.source)
-            if not path.exists(path.join(self.target, rel)):
-                os.makedirs(path.join(self.target, rel))
-            rst = {path.splitext(f)[0] for f in files if path.splitext(f)[1] == '.rst'}
-            for rst_name in rst:
-                source = path.join(root, rst_name + '.rst')
-                template_path = path.join(root, rst_name + '.html')
-                target = path.join(self.target, rel, rst_name + '.html')
-                ancestor = rel
-                while not path.exists(template_path):
-                    template_path = path.join(self.source, ancestor, '__template__.html')
-                    if not ancestor:
-                        break
-                    ancestor = path.dirname(ancestor)
-                if path.exists(template_path):
-                    with open(template_path) as template_in:
-                        template = string.Template(template_in.read())
-                else:
-                    template = default_template
-                with open(target, 'w') as html_out:
-                    html_out.write(self._render(source, template))
-            for filename in files:
-                source = path.join(root, filename)
-                target = path.join(self.target, rel, filename)
-                if path.splitext(filename)[1] in ['.rst', '.html']:
-                    if path.splitext(filename)[0] in rst:
-                        continue
-                if filename == '__template__.html':
-                    continue
-                shutil.copy2(source, target)
-
-    def serve(self, port=8000):
-        server = SocketServer.TCPServer(('', port), LiveSiteHandler)
-        server.site = self
-        server.serve_forever()
-
-    def publish(self, origin=None):
-        ''' This will do the following:
-        #. check whether _site is a git repo
-        #. if not, clone the repository from Github
-        #. checkout gh-pages branch
-        #. if that fails, create a gh-pages branch
-        #. compile site
-        #. git addremove
-        #. git commit
-        #. git push gh-pages gh-pages
-        '''
-        def clone():
-            if not origin:
-                raise GitError('Origin required to clone remote repository')
-            try:
-                self._git('clone', origin, '.')
-            except subprocess.CalledProcessError as e:
-                raise GitError(e)
-        # TODO: handle "user pages" as well as project pages, where site is on master branch
-        try:
-            self._git('--version')
-        except subprocess.CalledProcessError as e:
-            raise GitError('No git command found. Is git installed and on the path?')
-        self._clean()
-        if not path.exists(self.target):
-            os.makedirs(self.target)
-        try:
-            gitdir = self._git('rev-parse', '--git-dir')
-        except subprocess.CalledProcessError:
-            clone()
-        else:
-            if not path.isabs(gitdir):
-                gitdir = path.join(self.target, gitdir)
-            if path.dirname(_normpath(gitdir)) != _normpath(self.target):
-                clone()
-        try:
-            self._git('reset', '--hard')
-            try:
-                self._git('checkout', 'gh-pages')
-            except subprocess.CalledProcessError:
-                self._git('branch', 'gh-pages')
-                self._git('checkout', 'gh-pages')
-            else:
-                self._git('pull')
-            self.compile()
-            self._git('add', '-A')
-            self._git('commit', '-m', 'Dead Simple Site auto publish')
-            self._git('push', '-u', 'origin', 'gh-pages')
-        except subprocess.CalledProcessError as e:
-            raise GitError(e)
-
-def cli(args=None):
-
-    def serve(parsed):
-        DeadSimpleSite(parsed.directory).serve(parsed.port)
-
-    def compile(parsed):
-        DeadSimpleSite(parsed.directory).compile()
-
-    def publish(parsed):
-        try:
-            DeadSimpleSite(parsed.directory).publish(parsed.origin)
-        except GitError as e:
-            sys.stderr.write(str(e) + '\n')
-            sys.exit(1)
-    
-    parser = argparse.ArgumentParser(description='Dead Simple Site generator')
-    parser.add_argument('-d', '--directory', help='folder containing site source files', default='.')
-    subs = parser.add_subparsers(title='subcommands')
-
-    parser_serve = subs.add_parser('serve', help='serve the site for development')
-    parser_serve.add_argument('-p', '--port', type=int, default=8000)
-    parser_serve.set_defaults(func=serve)
-
-    parser_compile = subs.add_parser('compile', help='build the site')
-    parser_compile.set_defaults(func=compile)
-    
-    parser_publish = subs.add_parser('publish', help='publish to Github pages')
-    # TODO: arguments could be optional if the working dir is a repo on github, by
-    #       `git remote show origin`, or if _site is already a github repo
-    parser_publish.add_argument('origin', nargs='?',
-        help='Github URL to repository. Ignored if _site is already a repository.')
-    parser_publish.set_defaults(func=publish)
-    
-    parsed = parser.parse_args(args)
-    parsed.func(parsed)
-
-if __name__ == '__main__':
-    cli()
+#!/usr/bin/env python
+import argparse
+import fnmatch
+import os
+from os import path
+import re
+import shutil
+from SimpleHTTPServer import SimpleHTTPRequestHandler
+import SocketServer
+import string
+import subprocess
+import sys
+
+from docutils.core import publish_parts
+
+# TODO: ie http-equiv tag
+# TODO: generator meta tag?
+# TODO: header with home link
+# TODO: footer with mod date info
+default_template = string.Template('''<!DOCTYPE html>
+<html><head>
+    $favicon
+    $title
+    $stylesheet
+    $scripts
+</head><body>
+    $content
+</body></html>
+''')
+
+def _normpath(p):
+    return path.normcase(path.normpath(path.abspath(p)))
+
+class GitError(Exception):
+
+    def __init__(self, error):
+        if isinstance(error, subprocess.CalledProcessError):
+            Exception.__init__(self, 'Failed: %s\n%s' % (error.cmd, error.output))
+        else:
+            Exception.__init__(self, error)
+
+# TODO: show fatal rst syntax errors, e.g. include file that doesn't exist
+class LiveSiteHandler(SimpleHTTPRequestHandler):
+    '''
+    Can extend server behavior by replacing this class with a subclass of itself that overrides the
+    do_* methods as usual.
+    '''
+
+    def do_GET(self):
+        # TODO: expire immediately
+        # TODO: race condition when reloading during a compile
+        # TODO: tests for this
+        self.server.site.compile()
+        prev_cwd = os.getcwd()
+        try:
+            os.chdir(self.server.site.target)
+            SimpleHTTPRequestHandler.do_GET(self)
+        finally:
+            os.chdir(prev_cwd)
+
+class DeadSimpleSite(object):
+
+    def __init__(self, source):
+        self.source = path.abspath(source)
+        self.target = path.join(self.source, '_site')
+
+    def _clean(self):
+        for root, dirs, files in os.walk(self.target, topdown=False):
+            for filename in files:
+                target = path.join(root, filename)
+                relpath = path.relpath(target, self.target)
+                if relpath[0] == '.' and filename != '.htaccess':
+                    break
+                os.remove(target)
+            if path.relpath(root, self.target)[0] != '.' and len(os.listdir(root)) == 0:
+                os.rmdir(root)
+
+    def _git(self, *args):
+        if not path.exists(self.target):
+            os.makedirs(self.target)
+        with open(os.devnull, 'w') as devnull:
+            if args[0] == 'push':
+                # Git reads username and password from console, so don't suppress output
+                # Also, don't want to throw an error since most likely the user just mistyped
+                subprocess.call(('git',) + args, cwd=self.target)
+            else:
+                return subprocess.check_output(('git',) + args, stderr=subprocess.STDOUT, cwd=self.target)
+
+    def _render(self, source, template):
+        relpath = path.relpath(source, self.source)
+        depth = len(re.split(r'[/\\]', relpath)) - 1
+        html_root = depth * '../'
+        style_tag = '<link rel="stylesheet" href="%sstyle/global.css">' % html_root \
+            if path.exists(path.join(self.source, 'style/global.css')) else ''
+        script_tags = '\n'.join(['<script src="%s%s"></script>' % (html_root, script)
+            for script in self._scripts()])
+        favicon_tag = '<link rel="shortcut icon" href="favicon.ico">' \
+            if path.exists(path.join(self.source, 'favicon.ico')) else ''
+        with open(source) as source_file:
+            parts = publish_parts(
+                source=source_file.read(),
+                source_path=source,
+                writer_name='html',
+                # TODO: smart quotes on
+                # TODO: going to need something like this to get sensible title behavior
+                #       also, I don't like the default docinfo, e.g. authors, copyright, they
+                #       are related because both depend on being the first thing in the source
+                #settings_overrides={'doctitle_xform': False}
+                )
+        return template.substitute(
+            content = parts['html_body'],
+            favicon = favicon_tag,
+            title = '<title>%s</title>' % parts['title'],
+            root = html_root,
+            stylesheet = style_tag,
+            scripts = script_tags)
+
+    def _scripts(self):
+        scripts = []
+        for root, dirs, files in self._walk_source():
+            # TODO: this walks the output directory - that's bad
+            for script in fnmatch.filter(files, '*.js'):
+                if script[0] == '.':
+                    continue
+                script_path = path.join(root, script)
+                relpath = path.relpath(script_path, self.source)
+                # In *.nix, paths may contain backslashes. Don't worry about psychos who do that.
+                scripts.insert(0, relpath.replace('\\', '/')) # for Windows
+        return scripts
+
+    def _walk_source(self):
+        for root, dirs, files in os.walk(self.source):
+            dirs.sort()
+            files.sort()
+            remove = [d for d in dirs if d[0] == '.' or d == '_site']
+            for dirname in remove:
+                dirs.remove(dirname)
+            yield root, dirs, [f for f in files if f[0] != '.' or f == '.htaccess']
+
+    def compile(self):
+        self._clean()
+        for root, dirs, files in self._walk_source():
+            rel = path.relpath(root, self.source)
+            if not path.exists(path.join(self.target, rel)):
+                os.makedirs(path.join(self.target, rel))
+            rst = {path.splitext(f)[0] for f in files if path.splitext(f)[1] == '.rst'}
+            for rst_name in rst:
+                source = path.join(root, rst_name + '.rst')
+                template_path = path.join(root, rst_name + '.html')
+                target = path.join(self.target, rel, rst_name + '.html')
+                ancestor = rel
+                while not path.exists(template_path):
+                    template_path = path.join(self.source, ancestor, '__template__.html')
+                    if not ancestor:
+                        break
+                    ancestor = path.dirname(ancestor)
+                if path.exists(template_path):
+                    with open(template_path) as template_in:
+                        template = string.Template(template_in.read())
+                else:
+                    template = default_template
+                with open(target, 'w') as html_out:
+                    html_out.write(self._render(source, template))
+            for filename in files:
+                source = path.join(root, filename)
+                target = path.join(self.target, rel, filename)
+                if path.splitext(filename)[1] in ['.rst', '.html']:
+                    if path.splitext(filename)[0] in rst:
+                        continue
+                if filename == '__template__.html':
+                    continue
+                shutil.copy2(source, target)
+
+    def serve(self, port=8000):
+        SocketServer.TCPServer.allow_reuse_address = True
+        server = SocketServer.TCPServer(('', port), LiveSiteHandler)
+        server.site = self
+        server.serve_forever()
+
+    def publish(self, origin=None):
+        ''' This will do the following:
+        #. check whether _site is a git repo
+        #. if not, clone the repository from Github
+        #. checkout gh-pages branch
+        #. if that fails, create a gh-pages branch
+        #. compile site
+        #. git addremove
+        #. git commit
+        #. git push gh-pages gh-pages
+        '''
+        def clone():
+            if not origin:
+                raise GitError('Origin required to clone remote repository')
+            try:
+                # TODO: if the github repo is not initialized, clone fails, so make a first commit
+                self._git('clone', origin, '.')
+            except subprocess.CalledProcessError as e:
+                raise GitError(e)
+        # TODO: handle "user pages" as well as project pages, where site is on master branch
+        try:
+            self._git('--version')
+        except subprocess.CalledProcessError as e:
+            # TODO: on nix, this raises OSError
+            raise GitError('No git command found. Is git installed and on the path?')
+        self._clean()
+        if not path.exists(self.target):
+            os.makedirs(self.target)
+        try:
+            gitdir = self._git('rev-parse', '--git-dir')
+        except subprocess.CalledProcessError:
+            clone()
+        else:
+            if not path.isabs(gitdir):
+                gitdir = path.join(self.target, gitdir)
+            if path.dirname(_normpath(gitdir)) != _normpath(self.target):
+                clone()
+        try:
+            self._git('reset', '--hard')
+            try:
+                self._git('checkout', 'gh-pages')
+            except subprocess.CalledProcessError:
+                self._git('branch', 'gh-pages')
+                self._git('checkout', 'gh-pages')
+            else:
+                self._git('pull')
+            self.compile()
+            self._git('add', '-A')
+            self._git('commit', '-m', 'Dead Simple Site auto publish')
+            # TODO: revert commit if push fails
+            self._git('push', '-u', 'origin', 'gh-pages')
+        except subprocess.CalledProcessError as e:
+            raise GitError(e)
+
+def cli(args=None):
+
+    def serve(parsed):
+        DeadSimpleSite(parsed.directory).serve(parsed.port)
+
+    def compile(parsed):
+        DeadSimpleSite(parsed.directory).compile()
+
+    def publish(parsed):
+        try:
+            DeadSimpleSite(parsed.directory).publish(parsed.origin)
+        except GitError as e:
+            sys.stderr.write(str(e) + '\n')
+            sys.exit(1)
+    
+    parser = argparse.ArgumentParser(description='Dead Simple Site generator')
+    parser.add_argument('-d', '--directory', help='folder containing site source files', default='.')
+    subs = parser.add_subparsers(title='subcommands')
+
+    parser_serve = subs.add_parser('serve', help='serve the site for development')
+    parser_serve.add_argument('-p', '--port', type=int, default=8000)
+    parser_serve.set_defaults(func=serve)
+
+    parser_compile = subs.add_parser('compile', help='build the site')
+    parser_compile.set_defaults(func=compile)
+    
+    parser_publish = subs.add_parser('publish', help='publish to Github pages')
+    # TODO: arguments could be optional if the working dir is a repo on github, by
+    #       `git remote show origin`, or if _site is already a github repo
+    parser_publish.add_argument('origin', nargs='?',
+        help='Github URL to repository. Ignored if _site is already a repository.')
+    parser_publish.set_defaults(func=publish)
+    
+    parsed = parser.parse_args(args)
+    parsed.func(parsed)
+
+if __name__ == '__main__':
+    cli()

          
M test.py +292 -291
@@ 1,291 1,292 @@ 
-import os
-from os import path
-import re
-import shutil
-import subprocess
-import unittest
-
-import dss
-from dss import DeadSimpleSite
-
-def page_contain_assertion(testcase, filepath, invert=False):
-    with open(filepath) as f:
-        filetext = f.read()
-    def page_contains(pattern):
-        testcase.assertTrue(invert != bool(re.search(pattern, filetext, re.DOTALL)))
-    return page_contains
-
-def clean(directory):
-    if path.exists(directory):
-        shutil.rmtree(directory)
-
-class TestCompile(unittest.TestCase):
-
-    def setUp(self):
-        clean('test/_site')
-
-    def test_canary(self):
-        ''' Do nothing '''
-
-    def test_copy(self):
-        self.assertFalse(path.exists('test/_site'))
-        DeadSimpleSite('test').compile()
-        self.assertTrue(path.exists('test/_site'))
-        self.assertTrue(path.exists('test/_site/favicon.ico'))
-        self.assertTrue(path.exists('test/_site/subdir/plain.html'))
-
-    def test_render_default(self):
-        DeadSimpleSite('test').compile()
-        self.assertFalse(path.exists('test/_site/index.rst'))
-        page_contains = page_contain_assertion(self, 'test/_site/index.html')
-        # TODO: inline markup in title?
-        page_contains(r'<head>.*<title>Becomes Title</title>.*</head>')
-        page_contains(r'<body>.*Becomes content.*</body>')
-        page_contains(r'<link rel="stylesheet" href="style/global\.css">')
-        # TODO: html escape js paths
-        page_contains(r'<script src="js/first/foo\.js"></script>')
-        page_contains(r'<script src="js/bar\.js"></script>')
-        page_contains(r'<link rel="shortcut icon" href="favicon.ico">')
-
-    def test_render_default_subdir(self):
-        DeadSimpleSite('test').compile()
-        page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html')
-        page_contains(r'<link rel="stylesheet" href="\.\./style/global\.css">')
-        # TODO: test for script alphabetical ordering
-        # Notice this tests script depth ordering:
-        page_contains(r'<script src="\.\./js/first/foo\.js"></script>.*<script src="\.\./js/bar\.js"></script>.*')
-        # TODO: rst includes
-        DeadSimpleSite('test').compile()
-        not_page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html', invert=True)
-        # Tests that recompiling does not pick up scripts from the output directory
-        not_page_contains(r'<script src=.*_site')
-
-    def test_render_custom(self):
-        DeadSimpleSite('test').compile()
-        page_contains = page_contain_assertion(self, 'test/_site/custom.html')
-        page_contains(r'<link rel="stylesheet" href="style/global\.css">')
-        page_contains(r'<script src="js/first/foo\.js"></script>')
-        page_contains(r'<script src="js/bar\.js"></script>')
-        page_contains(r'Title From Content')
-        page_contains(r'ReStructured Text content')
-        page_contains(r'<p>Some html content</p>')
-
-    def test_cleanup(self):
-        ''' Compiling removes files and directories that do not exist in the source '''
-        os.makedirs('test/_site/empty')
-        open('test/_site/empty/bar', 'w').close()
-        DeadSimpleSite('test').compile()
-        self.assertFalse(path.exists('test/_site/empty/bar'))
-        self.assertFalse(path.exists('test/_site/empty'))
-
-    def test_ignore_dotfiles(self):
-        ''' Ignores any files that begin with "." except .htaccess '''
-        # TODO: worry about hidden files and such on windows?
-        os.makedirs('test/_site/.ignored-empty-target-folder')
-        os.makedirs('test/_site/.ignored-full-target-folder')
-        open('test/_site/.ignored-target-file', 'w').close()
-        open('test/_site/.ignored-full-target-folder/non-dotfile', 'w').close()
-        DeadSimpleSite('test').compile()
-        self.assertTrue(path.exists('test/_site/.htaccess'))
-        self.assertTrue(path.exists('test/_site/subdir/.htaccess'))
-        self.assertFalse(path.exists('test/_site/.ignored'))
-        self.assertFalse(path.exists('test/_site/.ignored-folder'))
-        self.assertTrue(path.exists('test/_site/.ignored-empty-target-folder'))
-        self.assertTrue(path.exists('test/_site/.ignored-target-file'))
-        self.assertTrue(path.exists('test/_site/.ignored-full-target-folder/non-dotfile'))
-        not_page_contains = page_contain_assertion(self, 'test/_site/index.html', invert=True)
-        not_page_contains(r'<script src=.*anything\.js')
-        not_page_contains(r'\.ignored\.js')
-
-class TestBarrenCompile(unittest.TestCase):
-
-    def setUp(self):
-        clean('test-barren/_site')
-
-    def test_render_barren(self):
-        DeadSimpleSite('test-barren').compile()
-        not_contains = page_contain_assertion(self, 'test-barren/_site/index.html', True)
-        not_contains('<link rel="stylesheet"')
-        not_contains('<script')
-        not_contains('favicon')
-
-    def test_cleans_htaccess(self):
-        os.makedirs('test-barren/_site')
-        open('test-barren/_site/.htaccess', 'w').close()
-        DeadSimpleSite('test-barren').compile()
-        self.assertFalse(path.exists('test-barren/_site/.htaccess'))
-
-class TestPublish(unittest.TestCase):
-
-    prep_commands = [
-        ('--version',),
-        ('rev-parse', '--git-dir')]
-    update_commands = [
-        ('checkout', 'gh-pages'),
-        ('pull',),
-        ('add', '-A'),
-        ('commit', '-m', 'Dead Simple Site auto publish'),
-        ('push', '-u', 'origin', 'gh-pages',)]
-    clone_commands = prep_commands \
-        + [('clone', 'https://example.com/user/repo.git', '.'), ('reset', '--hard')] \
-        + update_commands
-    
-    remote = 'https://example.com/user/repo.git'
-
-    def setUp(self):
-        self._real_git = DeadSimpleSite._git
-
-        self.fail_git = None
-        self.gitdir = '.git'
-        self.site = DeadSimpleSite('test')
-        self.git_invocations = []
-
-        clean('test/_site')
-        self.site.compile() # make sure the output dir does not start clean, to test cleanup
-
-        def git(site, *gitargs):
-            self.git_invocations.append(gitargs)
-            if (gitargs[0] == self.fail_git):
-                self.fail_git = None
-                raise subprocess.CalledProcessError('foo', 'bar', 'baz')
-            if (gitargs == ('rev-parse', '--git-dir')):
-                self.assertTrue(path.exists(self.site.target))
-                return self.gitdir
-            if (gitargs[0] == 'clone'):
-                self.assertTrue(gitargs[1] != None)
-            if (gitargs[0] == 'reset'):
-                self.assertFalse(path.exists('test/_site/index.html'))
-            if (gitargs[0] == 'add'):
-                self.assertTrue(path.exists('test/_site/index.html'))
-        DeadSimpleSite._git = git
-
-    def tearDown(self):
-        DeadSimpleSite._git = self._real_git
-
-    def test_creates_target(self):
-        clean('test/_site')
-        self.site.publish()
-
-    def test_no_git(self):
-        self.fail_git = '--version'
-        with self.assertRaisesRegexp(dss.GitError, 'No git command found'):
-            self.site.publish()
-
-    def test_arbitrary_git_error(self):
-        self.fail_git = 'reset'
-        with self.assertRaisesRegexp(dss.GitError, 'baz'):
-            self.site.publish()
-
-    def test_existing_repo(self):
-        self.site.publish()
-        self.assertEqual(self.prep_commands + [('reset', '--hard')] + self.update_commands,
-            self.git_invocations)
-
-    def test_clone_when_source_in_git(self):
-        self.gitdir = self.site.source + '/.git'
-        self.site.publish(self.remote)
-        self.assertEqual(self.clone_commands, self.git_invocations)
-
-    def test_clone_when_source_not_in_git(self):
-        self.fail_git = 'rev-parse'
-        self.site.publish(self.remote)
-        self.assertEqual(self.clone_commands, self.git_invocations)
-
-    def test_clone_error_no_origin(self):
-        self.gitdir = self.site.source + '/.git'
-        with self.assertRaisesRegexp(dss.GitError, 'Origin required to clone remote repository'):
-            self.site.publish()
-
-    def test_clone_error_wrapping(self):
-        self.gitdir = self.site.source + '/.git'
-        self.fail_git = 'clone'
-        with self.assertRaisesRegexp(dss.GitError, 'baz'):
-            self.site.publish(self.remote)
-
-    def test_no_gh_pages_branch(self):
-        self.fail_git = 'checkout'
-        self.site.publish()
-        self.assertEqual(self.prep_commands
-            + [ ('reset', '--hard'),
-                ('checkout', 'gh-pages'),
-                ('branch', 'gh-pages')]
-            + [c for c in self.update_commands if c != ('pull',)],
-            self.git_invocations)
-
-class TestTemplate(unittest.TestCase):
-
-    def setUp(self):
-        clean('test-templates/_site')
-
-    def test_template_nest(self):
-        DeadSimpleSite('test-templates').compile()
-        self.assertFalse(path.exists('test-templates/_site/__template__.html'))
-        self.assertFalse(path.exists('test-templates/_site/subdir/subsubdir/__template__.html'))
-        index_contains = page_contain_assertion(self, 'test-templates/_site/index.html')
-        index_contains(r'<a href="index\.html">Home</a>')
-        sub_contains = page_contain_assertion(self, 'test-templates/_site/subdir/root-template.html')
-        sub_contains(r'<a href="\.\./index\.html">Home</a>')
-        not_sub_sub_contains = page_contain_assertion(self,
-            'test-templates/_site/subdir/subsubdir/nested.html', invert=True)
-        not_sub_sub_contains(r'index\.html')
-
-class TestCli(unittest.TestCase):
-
-    def setUp(self):
-        self.compiled = None
-        self.served = None
-        self.served_on = None
-        self.origin = None
-
-        def dummy_compile(site):
-            self.compiled = site.source
-        
-        def dummy_serve(site, port):
-            self.served = site.source
-            self.served_on = port
-
-        def dummy_publish(site, origin):
-            self.origin = origin
-        
-        self._real_compile = DeadSimpleSite.compile
-        self._real_serve = DeadSimpleSite.serve
-        self._real_publish = DeadSimpleSite.publish
-        DeadSimpleSite.serve = dummy_serve
-        DeadSimpleSite.compile = dummy_compile
-        DeadSimpleSite.publish = dummy_publish
-
-    def tearDown(self):
-        DeadSimpleSite.serve = self._real_serve
-        DeadSimpleSite.compile = self._real_compile
-        DeadSimpleSite.publish = self._real_publish
-
-    def test_compile(self):
-        dss.cli(['compile'])
-        self.assertTrue(self.compiled == path.abspath('.'))
-    
-    def test_serve(self):
-        dss.cli(['serve'])
-        self.assertTrue(self.served == path.abspath('.'))
-        self.assertTrue(self.served_on == 8000)
-
-    def test_path_arg(self):
-        # TODO: SimpleHTTPRequestHandler still thinks its root is pwd
-        dss.cli(['--directory', 'test', 'serve'])
-        self.assertTrue(self.served == path.abspath('test'))
-        dss.cli(['-d', 'test', 'serve'])
-        self.assertTrue(self.served == path.abspath('test'))
-
-    def test_port_arg(self):
-        dss.cli(['serve', '-p', '8001'])
-        self.assertTrue(self.served_on == 8001)
-
-    def test_publish(self):
-        dss.cli(['publish'])
-        self.assertTrue(self.origin == None)
-        dss.cli(['publish', 'http://example.com/user/repo'])
-        self.assertTrue(self.origin == 'http://example.com/user/repo')
-
-    # TODO: tests for errors
-
-if __name__ == '__main__':
-    unittest.main()
+#!/usr/bin/env python
+import os
+from os import path
+import re
+import shutil
+import subprocess
+import unittest
+
+import dss
+from dss import DeadSimpleSite
+
+def page_contain_assertion(testcase, filepath, invert=False):
+    with open(filepath) as f:
+        filetext = f.read()
+    def page_contains(pattern):
+        testcase.assertTrue(invert != bool(re.search(pattern, filetext, re.DOTALL)))
+    return page_contains
+
+def clean(directory):
+    if path.exists(directory):
+        shutil.rmtree(directory)
+
+class TestCompile(unittest.TestCase):
+
+    def setUp(self):
+        clean('test/_site')
+
+    def test_canary(self):
+        ''' Do nothing '''
+
+    def test_copy(self):
+        self.assertFalse(path.exists('test/_site'))
+        DeadSimpleSite('test').compile()
+        self.assertTrue(path.exists('test/_site'))
+        self.assertTrue(path.exists('test/_site/favicon.ico'))
+        self.assertTrue(path.exists('test/_site/subdir/plain.html'))
+
+    def test_render_default(self):
+        DeadSimpleSite('test').compile()
+        self.assertFalse(path.exists('test/_site/index.rst'))
+        page_contains = page_contain_assertion(self, 'test/_site/index.html')
+        # TODO: inline markup in title?
+        page_contains(r'<head>.*<title>Becomes Title</title>.*</head>')
+        page_contains(r'<body>.*Becomes content.*</body>')
+        page_contains(r'<link rel="stylesheet" href="style/global\.css">')
+        # TODO: html escape js paths
+        page_contains(r'<script src="js/first/foo\.js"></script>')
+        page_contains(r'<script src="js/bar\.js"></script>')
+        page_contains(r'<link rel="shortcut icon" href="favicon.ico">')
+
+    def test_render_default_subdir(self):
+        DeadSimpleSite('test').compile()
+        page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html')
+        page_contains(r'<link rel="stylesheet" href="\.\./style/global\.css">')
+        # TODO: test for script alphabetical ordering
+        # Notice this tests script depth ordering:
+        page_contains(r'<script src="\.\./js/first/foo\.js"></script>.*<script src="\.\./js/bar\.js"></script>.*')
+        # TODO: rst includes
+        DeadSimpleSite('test').compile()
+        not_page_contains = page_contain_assertion(self, 'test/_site/subdir/page.html', invert=True)
+        # Tests that recompiling does not pick up scripts from the output directory
+        not_page_contains(r'<script src=.*_site')
+
+    def test_render_custom(self):
+        DeadSimpleSite('test').compile()
+        page_contains = page_contain_assertion(self, 'test/_site/custom.html')
+        page_contains(r'<link rel="stylesheet" href="style/global\.css">')
+        page_contains(r'<script src="js/first/foo\.js"></script>')
+        page_contains(r'<script src="js/bar\.js"></script>')
+        page_contains(r'Title From Content')
+        page_contains(r'ReStructured Text content')
+        page_contains(r'<p>Some html content</p>')
+
+    def test_cleanup(self):
+        ''' Compiling removes files and directories that do not exist in the source '''
+        os.makedirs('test/_site/empty')
+        open('test/_site/empty/bar', 'w').close()
+        DeadSimpleSite('test').compile()
+        self.assertFalse(path.exists('test/_site/empty/bar'))
+        self.assertFalse(path.exists('test/_site/empty'))
+
+    def test_ignore_dotfiles(self):
+        ''' Ignores any files that begin with "." except .htaccess '''
+        # TODO: worry about hidden files and such on windows?
+        os.makedirs('test/_site/.ignored-empty-target-folder')
+        os.makedirs('test/_site/.ignored-full-target-folder')
+        open('test/_site/.ignored-target-file', 'w').close()
+        open('test/_site/.ignored-full-target-folder/non-dotfile', 'w').close()
+        DeadSimpleSite('test').compile()
+        self.assertTrue(path.exists('test/_site/.htaccess'))
+        self.assertTrue(path.exists('test/_site/subdir/.htaccess'))
+        self.assertFalse(path.exists('test/_site/.ignored'))
+        self.assertFalse(path.exists('test/_site/.ignored-folder'))
+        self.assertTrue(path.exists('test/_site/.ignored-empty-target-folder'))
+        self.assertTrue(path.exists('test/_site/.ignored-target-file'))
+        self.assertTrue(path.exists('test/_site/.ignored-full-target-folder/non-dotfile'))
+        not_page_contains = page_contain_assertion(self, 'test/_site/index.html', invert=True)
+        not_page_contains(r'<script src=.*anything\.js')
+        not_page_contains(r'\.ignored\.js')
+
+class TestBarrenCompile(unittest.TestCase):
+
+    def setUp(self):
+        clean('test-barren/_site')
+
+    def test_render_barren(self):
+        DeadSimpleSite('test-barren').compile()
+        not_contains = page_contain_assertion(self, 'test-barren/_site/index.html', True)
+        not_contains('<link rel="stylesheet"')
+        not_contains('<script')
+        not_contains('favicon')
+
+    def test_cleans_htaccess(self):
+        os.makedirs('test-barren/_site')
+        open('test-barren/_site/.htaccess', 'w').close()
+        DeadSimpleSite('test-barren').compile()
+        self.assertFalse(path.exists('test-barren/_site/.htaccess'))
+
+class TestPublish(unittest.TestCase):
+
+    prep_commands = [
+        ('--version',),
+        ('rev-parse', '--git-dir')]
+    update_commands = [
+        ('checkout', 'gh-pages'),
+        ('pull',),
+        ('add', '-A'),
+        ('commit', '-m', 'Dead Simple Site auto publish'),
+        ('push', '-u', 'origin', 'gh-pages',)]
+    clone_commands = prep_commands \
+        + [('clone', 'https://example.com/user/repo.git', '.'), ('reset', '--hard')] \
+        + update_commands
+    
+    remote = 'https://example.com/user/repo.git'
+
+    def setUp(self):
+        self._real_git = DeadSimpleSite._git
+
+        self.fail_git = None
+        self.gitdir = '.git'
+        self.site = DeadSimpleSite('test')
+        self.git_invocations = []
+
+        clean('test/_site')
+        self.site.compile() # make sure the output dir does not start clean, to test cleanup
+
+        def git(site, *gitargs):
+            self.git_invocations.append(gitargs)
+            if (gitargs[0] == self.fail_git):
+                self.fail_git = None
+                raise subprocess.CalledProcessError('foo', 'bar', 'baz')
+            if (gitargs == ('rev-parse', '--git-dir')):
+                self.assertTrue(path.exists(self.site.target))
+                return self.gitdir
+            if (gitargs[0] == 'clone'):
+                self.assertTrue(gitargs[1] != None)
+            if (gitargs[0] == 'reset'):
+                self.assertFalse(path.exists('test/_site/index.html'))
+            if (gitargs[0] == 'add'):
+                self.assertTrue(path.exists('test/_site/index.html'))
+        DeadSimpleSite._git = git
+
+    def tearDown(self):
+        DeadSimpleSite._git = self._real_git
+
+    def test_creates_target(self):
+        clean('test/_site')
+        self.site.publish()
+
+    def test_no_git(self):
+        self.fail_git = '--version'
+        with self.assertRaisesRegexp(dss.GitError, 'No git command found'):
+            self.site.publish()
+
+    def test_arbitrary_git_error(self):
+        self.fail_git = 'reset'
+        with self.assertRaisesRegexp(dss.GitError, 'baz'):
+            self.site.publish()
+
+    def test_existing_repo(self):
+        self.site.publish()
+        self.assertEqual(self.prep_commands + [('reset', '--hard')] + self.update_commands,
+            self.git_invocations)
+
+    def test_clone_when_source_in_git(self):
+        self.gitdir = self.site.source + '/.git'
+        self.site.publish(self.remote)
+        self.assertEqual(self.clone_commands, self.git_invocations)
+
+    def test_clone_when_source_not_in_git(self):
+        self.fail_git = 'rev-parse'
+        self.site.publish(self.remote)
+        self.assertEqual(self.clone_commands, self.git_invocations)
+
+    def test_clone_error_no_origin(self):
+        self.gitdir = self.site.source + '/.git'
+        with self.assertRaisesRegexp(dss.GitError, 'Origin required to clone remote repository'):
+            self.site.publish()
+
+    def test_clone_error_wrapping(self):
+        self.gitdir = self.site.source + '/.git'
+        self.fail_git = 'clone'
+        with self.assertRaisesRegexp(dss.GitError, 'baz'):
+            self.site.publish(self.remote)
+
+    def test_no_gh_pages_branch(self):
+        self.fail_git = 'checkout'
+        self.site.publish()
+        self.assertEqual(self.prep_commands
+            + [ ('reset', '--hard'),
+                ('checkout', 'gh-pages'),
+                ('branch', 'gh-pages')]
+            + [c for c in self.update_commands if c != ('pull',)],
+            self.git_invocations)
+
+class TestTemplate(unittest.TestCase):
+
+    def setUp(self):
+        clean('test-templates/_site')
+
+    def test_template_nest(self):
+        DeadSimpleSite('test-templates').compile()
+        self.assertFalse(path.exists('test-templates/_site/__template__.html'))
+        self.assertFalse(path.exists('test-templates/_site/subdir/subsubdir/__template__.html'))
+        index_contains = page_contain_assertion(self, 'test-templates/_site/index.html')
+        index_contains(r'<a href="index\.html">Home</a>')
+        sub_contains = page_contain_assertion(self, 'test-templates/_site/subdir/root-template.html')
+        sub_contains(r'<a href="\.\./index\.html">Home</a>')
+        not_sub_sub_contains = page_contain_assertion(self,
+            'test-templates/_site/subdir/subsubdir/nested.html', invert=True)
+        not_sub_sub_contains(r'index\.html')
+
+class TestCli(unittest.TestCase):
+
+    def setUp(self):
+        self.compiled = None
+        self.served = None
+        self.served_on = None
+        self.origin = None
+
+        def dummy_compile(site):
+            self.compiled = site.source
+        
+        def dummy_serve(site, port):
+            self.served = site.source
+            self.served_on = port
+
+        def dummy_publish(site, origin):
+            self.origin = origin
+        
+        self._real_compile = DeadSimpleSite.compile
+        self._real_serve = DeadSimpleSite.serve
+        self._real_publish = DeadSimpleSite.publish
+        DeadSimpleSite.serve = dummy_serve
+        DeadSimpleSite.compile = dummy_compile
+        DeadSimpleSite.publish = dummy_publish
+
+    def tearDown(self):
+        DeadSimpleSite.serve = self._real_serve
+        DeadSimpleSite.compile = self._real_compile
+        DeadSimpleSite.publish = self._real_publish
+
+    def test_compile(self):
+        dss.cli(['compile'])
+        self.assertTrue(self.compiled == path.abspath('.'))
+    
+    def test_serve(self):
+        dss.cli(['serve'])
+        self.assertTrue(self.served == path.abspath('.'))
+        self.assertTrue(self.served_on == 8000)
+
+    def test_path_arg(self):
+        # TODO: SimpleHTTPRequestHandler still thinks its root is pwd
+        dss.cli(['--directory', 'test', 'serve'])
+        self.assertTrue(self.served == path.abspath('test'))
+        dss.cli(['-d', 'test', 'serve'])
+        self.assertTrue(self.served == path.abspath('test'))
+
+    def test_port_arg(self):
+        dss.cli(['serve', '-p', '8001'])
+        self.assertTrue(self.served_on == 8001)
+
+    def test_publish(self):
+        dss.cli(['publish'])
+        self.assertTrue(self.origin == None)
+        dss.cli(['publish', 'http://example.com/user/repo'])
+        self.assertTrue(self.origin == 'http://example.com/user/repo')
+
+    # TODO: tests for errors
+
+if __name__ == '__main__':
+    unittest.main()