Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

First checkin

  • Loading branch information...
commit c43c6fbcdf771df6fa9a09ea70e82383c9f06678 0 parents
@gregorycollins authored
Showing with 1,241 additions and 0 deletions.
  1. +3 −0  .gitignore
  2. 0  LICENSE
  3. +3 −0  Setup.lhs
  4. +33 −0 homepage.cabal
  5. +9 −0 src/Homepage.hs
  6. +80 −0 src/Homepage/Handlers.hs
  7. +98 −0 src/Homepage/Types.hs
  8. +150 −0 src/Homepage/Util/Delicious.hs
  9. +61 −0 src/Homepage/Util/Templates.hs
  10. +31 −0 src/Main.hs
  11. +76 −0 static/css/common.css
  12. +29 −0 static/css/highlighting.css
  13. +148 −0 static/css/home.css
  14. +223 −0 static/css/posts.css
  15. BIN  static/i/background.jpg
  16. BIN  static/i/cc-license-badge.png
  17. BIN  static/i/grid-background.png
  18. BIN  static/i/intro-background.png
  19. +32 −0 templates/about.st
  20. +24 −0 templates/contact.st
  21. +5 −0 templates/deliciousBookmark.st
  22. +4 −0 templates/disqus.st
  23. +10 −0 templates/disqusThread.st
  24. +14 −0 templates/disqusfooter.st
  25. +4 −0 templates/footer.st
  26. +9 −0 templates/googleanalytics.st
  27. +4 −0 templates/header.st
  28. +78 −0 templates/home.st
  29. +3 −0  templates/htmlfooter.st
  30. +10 −0 templates/htmlheader.st
  31. +8 −0 templates/navigation.st
  32. +27 −0 templates/post.st
  33. +65 −0 templates/temppost1.st
3  .gitignore
@@ -0,0 +1,3 @@
+dist/**
+dist
+*~
0  LICENSE
No changes.
3  Setup.lhs
@@ -0,0 +1,3 @@
+#!/usr/bin/env runhaskell
+> import Distribution.Simple
+> main = defaultMain
33 homepage.cabal
@@ -0,0 +1,33 @@
+Name: homepage
+Version: 0.1
+Synopsis: gregorycollins.net
+
+License: BSD3
+License-file: LICENSE
+Author: Gregory Collins
+
+Stability: Experimental
+Category: Web
+Build-type: Simple
+
+Extra-Source-Files:
+ src/*hs
+ static/*
+ templates/*.st
+
+Cabal-Version: >= 1.6
+
+Executable gregorycollins.net
+ Main-is: Main.hs
+ hs-source-dirs:
+ src
+ ghc-options: -Wall -funbox-strict-fields -O2 -fvia-C -optc-O3 -funfolding-use-threshold=16
+ Build-Depends: base >= 4 && <5, syb, HStringTemplate,
+ HStringTemplateHelpers, mtl, bytestring,
+ happstack-server >= 0.3,
+ containers, pretty, pureMD5,
+ directory, filepath, hscolour, HTTP, safe,
+ old-time, parsec, happstack-helpers,
+ DebugTraceHelpers, delicious, unix,
+ time, old-locale
+
9 src/Homepage.hs
@@ -0,0 +1,9 @@
+module Homepage
+(
+ module Homepage.Handlers
+, module Homepage.Types
+)
+where
+
+import Homepage.Handlers
+import Homepage.Types
80 src/Homepage/Handlers.hs
@@ -0,0 +1,80 @@
+{-# LANGUAGE NoMonomorphismRestriction, OverloadedStrings, FlexibleContexts #-}
+
+-- | This module contains the top-level handler for the website.
+
+module Homepage.Handlers (topLevelHandler) where
+
+import Control.Monad.State.Strict
+
+import Data.Monoid
+import qualified Data.ByteString.Lazy.Char8 as B
+
+import Happstack.Helpers
+import Happstack.Server
+--import Happstack.Server.Parts
+
+import Homepage.Types
+import Homepage.Util.Templates
+import Homepage.Util.Delicious as Delicious
+
+import Text.StringTemplate
+
+
+topLevelHandler :: HomepageHandler
+topLevelHandler =
+-- gzip currently doesn't work. augh
+--
+-- do
+-- compressedResponseFilter
+ frontpage `mappend`
+ aboutpage `mappend`
+ (liftH staticfiles) `mappend`
+ temporaryPosts `mappend`
+ fourohfour
+
+
+frontpage :: HomepageHandler
+frontpage =
+ exactdir "/" $ do
+ bookmarks <- lift Delicious.getRecent
+ serveTemplate' "." "home" (setAttribute "recentBookmarks" bookmarks .
+ setAttribute "whichCss" ("home" :: String))
+
+aboutpage :: HomepageHandler
+aboutpage =
+ exactdir "/about" $ do
+ serveTemplate' "." "about" (setAttribute "whichCss"
+ ("posts" :: String))
+
+
+temporaryPosts :: HomepageHandler
+temporaryPosts = do
+ postContent <- lift $ (getTemplate "." "temppost1") >>=
+ (return . B.unpack . render)
+
+ let attrs :: [(String,String)]
+ attrs = [ ("websiteTitleExtra",
+ ": Building a website with Haskell, part 1")
+ , ("whichCss", "posts")
+ , ("postContent", postContent)
+ , ("postTitle", "Building a website with Haskell, part 1")
+ , ("postSummary", "Using the <a href=\"\
+ \http://www.happstack.com/\">happstack</a> \
+ \web framework to power a simple personal \
+ \website.")
+ , ("postDate", "march 26, 2009") ]
+
+ exactdir "/posts/2009/03/26/building-a-website-part-1" $
+ serveTemplate' "." "post" (setManyAttrib attrs)
+
+
+fourohfour :: HomepageHandler
+fourohfour = serveTemplate "." "404"
+
+
+-- N.B. "fileServeStrict" here is like normal "fileServe" from
+-- happstack 0.2.1, except modified to consume the file strictly
+-- (avoiding handle leaks)
+staticfiles :: WebHandler
+staticfiles = staticserve "static"
+ where staticserve d = dir d (fileServeStrict [] d)
98 src/Homepage/Types.hs
@@ -0,0 +1,98 @@
+{-# LANGUAGE OverloadedStrings, UnboxedTuples, BangPatterns #-}
+
+-- | This module contains types (and a couple of functions) pertaining
+-- | to the website's global state, plus some synonyms
+module Homepage.Types where
+
+import Control.Concurrent.MVar
+import Control.Monad.State.Strict
+import qualified Network.Delicious as D
+import qualified Data.ByteString.Lazy.Char8 as B
+
+import Data.Time
+
+import Happstack.Server
+
+import Text.StringTemplate
+import Text.StringTemplate.Helpers
+
+
+------------------------------------------------------------------------
+-- * Type synonyms
+
+-- | type synonyms for templates & template groups
+type TemplateDirs = STDirGroups B.ByteString
+type TemplateGroup = STGroup B.ByteString
+type Template = StringTemplate B.ByteString
+
+
+------------------------------------------------------------------------
+-- * Homepage State
+
+-- | In order to not spam delicious, we only pull my recent feeds once
+-- | every four hours. So we need to keep the last posts and update
+-- | time.
+data DeliciousState = DeliciousState ![D.Post] !UTCTime
+
+
+-- | We're going to keep the templates inside the homepage state. The
+-- | variable is wrapped in an MVar because I'm planning on using
+-- | inotify to handle template reloads
+data HomepageState = HomepageState {
+ homepageDeliciousMVar :: MVar DeliciousState
+ , homepageTemplateMVar :: MVar TemplateDirs
+}
+
+
+-- | Create a homepage state object with new empty mvars
+emptyHomepageState :: IO HomepageState
+emptyHomepageState = do
+ d <- newEmptyMVar
+ t <- newEmptyMVar
+ return $! HomepageState d t
+
+
+-- | We'll put the homepage state into a state monad so we don't have
+-- | to pass it around everywhere
+type HomepageMonad = StateT HomepageState IO
+
+-- | Homepage handlers will have the following type
+type HomepageHandler = ServerPartT HomepageMonad Response
+
+-- | "standard" web handlers (like 'staticfiles') will have this type
+type WebHandler = ServerPartT IO Response
+
+-- | ..so we'll need a function to lift a "WebHandler" into the
+-- | "HomepageHandler"
+liftH :: ServerPartT IO a -> ServerPartT HomepageMonad a
+liftH = mapServerPartT liftIO
+
+
+-- | this IO action initializes the homepage's state and returns a
+-- | monad evaluator function
+-- | runner :: HomepageMonad a -> IO a
+-- | we'll pass this into simpleHTTP'.
+initHomepage :: IO (HomepageMonad a -> IO a)
+initHomepage = do
+ s <- emptyHomepageState
+
+ directoryGroups "templates" >>=
+ putMVar (homepageTemplateMVar s)
+
+ return $! runHomepage s
+
+
+
+runHomepage :: HomepageState -> HomepageMonad a -> IO a
+runHomepage hps = flip evalStateT hps
+
+
+------------------------------------------------------------------------
+-- * odds and ends
+
+-- | this instance should be in happstack already, it allows us to
+-- | treat a bytestring as an HTML response
+newtype BStoHTML = BStoHTML B.ByteString
+instance ToMessage BStoHTML where
+ toContentType _ = "text/html"
+ toMessage (BStoHTML a) = a
150 src/Homepage/Util/Delicious.hs
@@ -0,0 +1,150 @@
+{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}
+
+module Homepage.Util.Delicious
+(
+ getRecent
+, DiffPost(..)
+) where
+
+import qualified Control.Exception as Ex
+import Control.Concurrent.MVar
+import Control.Monad.State.Strict
+
+import Data.Char (isSpace)
+import Data.Maybe
+import Data.Time
+
+import System.Locale
+
+import Text.StringTemplate()
+import Text.StringTemplate.Classes
+
+import qualified Data.Map as Map
+import qualified Network.Delicious.JSON as D
+import qualified Network.Delicious.Types as D
+
+import Homepage.Types
+
+myDeliciousUserName :: String
+myDeliciousUserName = "how.gauche"
+
+type Age = String
+
+-- | a DiffPost is a delicious post plus an age string, e.g. '2 hours
+-- | ago'; we need to interpret the post in the context of the current
+-- | time in order to compute the age
+data DiffPost = DiffPost !D.Post !Age
+
+
+agePost :: TimeZone -> UTCTime -> D.Post -> DiffPost
+agePost tz now post = DiffPost post s
+ where
+ dt = parseDeliciousTime $ D.postStamp post
+ s = humanReadableTimeDiff tz now dt
+
+
+getRecentPosts :: MVar DeliciousState -> IO [D.Post]
+getRecentPosts mvar = do
+ now <- getCurrentTime
+ empty <- isEmptyMVar mvar
+ if empty then do
+ posts <- getRecentPosts'
+ tryPutMVar mvar $! DeliciousState posts now
+ return posts
+ else do
+ modifyMVar mvar $! \oldstate@(DeliciousState oldposts oldtime) -> do
+ if tooOld now oldtime then do
+ posts <- getRecentPosts'
+ let newstate = DeliciousState (posts `seq` posts) now
+ return $! (newstate `seq` newstate, posts `seq` posts)
+ else
+ return $! (oldstate, oldposts)
+ where
+ tooOld :: UTCTime -> UTCTime -> Bool
+ tooOld now old = diffUTCTime now old > 60 * 60 * 4
+
+
+getRecentPosts' :: IO [D.Post]
+getRecentPosts' = do
+ posts <- Ex.handle (\(_::Ex.SomeException) -> return [])
+ (D.runDelic D.nullUser
+ "http://feeds.delicious.com/v2/json" $
+ D.getUserBookmarks myDeliciousUserName)
+ return $ take 5 posts
+
+
+instance ToSElem DiffPost where
+ toSElem (DiffPost (D.Post href _ desc notes tags _ _) age) =
+ SM $! Map.fromList [ ("date", toSElem age)
+ , ("title", toSElem desc)
+ , ("summary", toSElem notes)
+ , ("href", toSElem href)
+ , ("tags", toSElem tags) ]
+
+
+
+parseDeliciousTime :: String -> UTCTime
+parseDeliciousTime = fromJust . parseTime defaultTimeLocale "%Y-%m-%dT%H:%M:%SZ"
+
+
+humanReadableTimeDiff :: TimeZone -- ^ our timezone
+ -> UTCTime -- ^ current time
+ -> UTCTime -- ^ old time
+ -> String
+humanReadableTimeDiff tz curTime oldTime =
+ helper diff
+ where
+ diff = diffUTCTime curTime oldTime
+
+ minutes :: NominalDiffTime -> Double
+ minutes n = realToFrac $ n / 60
+
+ hours :: NominalDiffTime -> Double
+ hours n = (minutes n) / 60
+
+ days :: NominalDiffTime -> Double
+ days n = (hours n) / 24
+
+ weeks :: NominalDiffTime -> Double
+ weeks n = (days n) / 7
+
+ years :: NominalDiffTime -> Double
+ years n = (days n) / 365
+
+ i2s :: RealFrac a => a -> String
+ i2s !n = show m
+ where
+ m :: Int
+ m = truncate n
+
+ old = utcToLocalTime tz oldTime
+
+ trim = f . f
+ where f = reverse . dropWhile isSpace
+
+ dow = trim $! formatTime defaultTimeLocale "%l:%M %p on %A" old
+ thisYear = trim $! formatTime defaultTimeLocale "%b %e" old
+ previousYears = trim $! formatTime defaultTimeLocale "%b %e, %Y" old
+
+ helper !d | d < 1 = "one second ago"
+ | d < 60 = i2s d ++ " seconds ago"
+ | minutes d < 2 = "one minute ago"
+ | minutes d < 60 = i2s (minutes d) ++ " minutes ago"
+ | hours d < 2 = "one hour ago"
+ | hours d < 24 = i2s (hours d) ++ " hours ago"
+ | days d < 5 = dow
+ | days d < 10 = i2s (days d) ++ " days ago"
+ | weeks d < 2 = i2s (weeks d) ++ " week ago"
+ | weeks d < 5 = i2s (weeks d) ++ " weeks ago"
+ | years d < 1 = thisYear
+ | otherwise = previousYears
+
+
+getRecent :: HomepageMonad [DiffPost]
+getRecent = do
+ delMVar <- get >>= return . homepageDeliciousMVar
+ now <- liftIO $ getCurrentTime
+ tz <- liftIO $ getCurrentTimeZone
+
+ liftIO $ getRecentPosts delMVar >>=
+ return . map (agePost tz now)
61 src/Homepage/Util/Templates.hs
@@ -0,0 +1,61 @@
+module Homepage.Util.Templates
+ ( getTemplates
+ , getTemplate
+ , templateToResponse
+ , serveTemplate
+ , serveTemplate' )
+where
+
+import Control.Concurrent.MVar
+import Control.Monad.State.Strict
+
+import Data.Maybe
+
+import Homepage.Types
+
+import Text.StringTemplate
+import Text.StringTemplate.Helpers
+
+import Happstack.Server
+
+
+badTemplate :: String -> Template
+badTemplate nm = newSTMP $ "bad template: " ++ nm
+
+
+getTemplateFromGroup :: String -> TemplateGroup -> Template
+getTemplateFromGroup tmpl group =
+ fromMaybe (badTemplate tmpl)
+ (getStringTemplate tmpl group)
+
+
+getTemplates :: HomepageMonad TemplateDirs
+getTemplates = (get >>= return . homepageTemplateMVar)
+ >>= (\x -> liftIO $ readMVar x)
+
+
+getTemplate :: String -- ^ directory group
+ -> String -- ^ template name
+ -> HomepageMonad Template
+getTemplate grp nm =
+ getTemplates
+ >>= return . getTemplateFromGroup nm
+ . getTemplateGroup grp
+
+
+templateToResponse :: Template -> HomepageHandler
+templateToResponse = return . toResponse . BStoHTML . render
+
+
+serveTemplate :: String -- ^ directory group
+ -> String -- ^ template name
+ -> HomepageHandler
+serveTemplate grp nm = (lift $ getTemplate grp nm) >>= templateToResponse
+
+
+serveTemplate' :: String -- ^ directory group
+ -> String -- ^ template name
+ -> (Template -> Template) -- ^ function to mutate the template
+ -> HomepageHandler
+serveTemplate' grp nm f =
+ (lift $ getTemplate grp nm) >>= (templateToResponse . f)
31 src/Main.hs
@@ -0,0 +1,31 @@
+module Main where
+
+import Happstack.Server hiding (port)
+
+import Homepage
+
+import System.Environment
+
+main :: IO ()
+main = do
+ args <- getArgs
+ case args of
+ [port] -> startServer $ read port
+ _ -> putStrLn "pass me a port number, please"
+
+
+
+startServer :: Int -> IO ()
+startServer port = do
+ -- this IO action initializes the homepage's state and returns a
+ -- monad evaluator function
+ --
+ -- eval :: HomepageMonad a -> IO a
+ --
+ -- we'll pass this into simpleHTTP'. (the HTTP server expects to
+ -- be given IO and needs a function to produce an IO action given
+ -- our custom state monad.)
+ initHomepage >>= go
+
+ where
+ go eval = simpleHTTP' eval (Conf port Nothing) topLevelHandler
76 static/css/common.css
@@ -0,0 +1,76 @@
+body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td {
+ margin:0;
+ padding:0;
+}
+table {
+ border-collapse:collapse;
+ border-spacing:0;
+}
+fieldset,img {
+ border:0;
+}
+address,caption,cite,code,dfn,em,strong,th,var {
+ font-style:normal;
+ font-weight:normal;
+}
+ol,ul {
+ list-style:none;
+}
+caption,th {
+ text-align:left;
+}
+h1,h2,h3,h4,h5,h6 {
+ font-size:100%;
+ font-weight:normal;
+ letter-spacing:-0.05em;
+}
+q:before,q:after {
+ content:'';
+}
+abbr,acronym { border:0; }
+
+
+body {
+ background: url(/static/i/background.jpg) repeat;
+ color: #333;
+ font-family: 'Helvetica Neue', Calibri, sans-serif;
+ font-size: 1.25em;
+ font-size-adjust:none;
+ font-stretch:normal;
+ font-style:normal;
+ font-variant:normal;
+ font-weight:normal;
+ line-height:2;
+ width:100%;
+}
+
+a {
+ text-decoration: none;
+}
+
+a:link, a:visited {
+ color: #366ac9;
+}
+
+a:hover {
+ color: #417eef;
+}
+
+#footer {
+ font-size:0.5em;
+ width: 960px;
+ color: #ddd;
+ height: 29px;
+ text-align: center;
+ vertical-align: middle;
+ background: #333;
+ margin: 0px auto;
+}
+
+#footer span {
+ background: url(/static/i/cc-license-badge.png) no-repeat center left;
+ line-height: 29px;
+ padding-left: 85px;
+ height: 29px;
+ display: inline-block;
+}
29 static/css/highlighting.css
@@ -0,0 +1,29 @@
+table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre
+ { margin: 0; padding: 0; border: 0; vertical-align: baseline; border: none; }
+td.lineNumbers { text-align: right; background-color: #EBEBEB; color: black; padding-right: 5px; padding-left: 5px; }
+td.sourceCode { padding-left: 5px; }
+pre.sourceCode { }
+pre.sourceCode span.Normal { }
+pre.sourceCode span.Keyword { font-weight: bold; }
+pre.sourceCode span.DataType { color: #800000; }
+pre.sourceCode span.DecVal { color: #0000FF; }
+pre.sourceCode span.BaseN { color: #0000FF; }
+pre.sourceCode span.Float { color: #800080; }
+pre.sourceCode span.Char { color: #FF00FF; }
+pre.sourceCode span.String { color: #DD0000; }
+pre.sourceCode span.Comment { color: #808080; font-style: italic; }
+pre.sourceCode span.Others { }
+pre.sourceCode span.Alert { color: green; font-weight: bold; }
+pre.sourceCode span.Function { color: #000080; }
+pre.sourceCode span.RegionMarker { }
+pre.sourceCode span.Error { color: red; font-weight: bold; }
+
+pre.sourceCode span.ModuleName { color: blue; }
+pre.sourceCode span.Keyword { color: #000080; }
+pre.sourceCode span.Function { }
+pre.sourceCode span.FunctionDefinition { color: green; }
+pre.sourceCode span.Class { color: #800080; text-decoration: underline; font-weight: normal; }
+pre.sourceCode span.Constructor { text-decoration: underline; }
+pre.sourceCode span.DataConstructor { color: #000080; }
+pre.sourceCode span.TypeConstructor { text-decoration: underline; }
+pre.sourceCode span.InfixOperator { color: #808080; }
148 static/css/home.css
@@ -0,0 +1,148 @@
+@import url("/static/css/common.css");
+
+#page {
+ width: 960px;
+ background: #f0f0f0;
+ /*background: url(/static/i/grid-background.png);*/
+ margin: 0px auto;
+}
+
+#topbar {
+ width: 960px;
+ height: 3em;
+ background: #f0f0f0;
+ margin: 0px auto;
+ clear: both;
+ color: #333;
+ z-index: 0;
+}
+
+#content {
+ margin-top: 190px;
+ padding: 12px;
+ clear: both;
+ color: #333;
+}
+
+#content h2 { font-size: 1.75em; font-weight: bold;}
+#content h3 { font-size: 1.25em; }
+
+
+#header {
+ float: left;
+ height: 3em;
+ line-height: 3em;
+ vertical-align: bottom;
+ padding-left: 1em;
+}
+
+#header h1 {
+ font-size: 1.25em;
+ font-weight: bold;
+}
+
+#navigation {
+ float: right;
+ height: 3em;
+ letter-spacing:-0.05em;
+ line-height: 3em;
+ padding-right: 1em;
+ z-index: 0;
+}
+
+#navigation li {
+ display: inline;
+ list-style-type: none;
+}
+
+#navigation li a {
+ display: inline-block;
+ padding: 0px 8px;
+ height: 3em;
+}
+
+#navigation li a:link, #navigation li a:visited {
+ color: #333;
+ background: #f0f0f0;
+}
+
+#navigation li a:hover {
+ color: #f0f0f0;
+ background: #333;
+}
+
+
+#intro {
+ position: absolute;
+ height: 210px;
+ width: 100%;
+ top: 3em;
+ left: 0;
+ background: url(/static/i/intro-background.png) repeat-x;
+ width: 100%;
+ text-align: center;
+ color: #f3f3f3;
+ letter-spacing:-0.05em;
+ line-height:2em;
+ z-index: 2;
+ min-width: 960px;
+ overflow: hidden;
+}
+
+#intro table { width: 100%; height: 100%; }
+
+#intro td {
+ font-size: 2em;
+ width: 100%;
+ height: 100%;
+}
+
+#hello { color: #ee2; }
+
+
+#footer {
+ clear: both;
+}
+
+
+td.date {
+ width:148px;
+ padding-right:12px;
+ font-size:0.6em;
+ text-align:right;
+}
+
+td.summary {
+ width:308px;
+ padding-left: 12px;
+ font-size:0.6em;
+}
+
+td.title {
+ width: 480px;
+ font-weight: bold;
+}
+
+
+#recently-bookmarked, #links, #recent-posts {
+ margin-bottom: 2em;
+}
+
+#recently-bookmarked h2, #links h2, #recent-posts h2 {
+ border-bottom: 1px solid #333;
+ padding-left: 158px;
+}
+
+h2 a:link, h2 a:visited {
+ color: #333;
+}
+
+h2 a:hover {
+ color: #49628f;
+}
+
+
+
+#recently-bookmarked tr, #links tr, #recent-posts tr {
+ border-bottom: 1px dotted #666;
+}
223 static/css/posts.css
@@ -0,0 +1,223 @@
+@import url("/static/css/common.css");
+
+ul li {
+ list-style-type: square;
+ list-style-image: inherit;
+ list-style-position: inside;
+}
+
+
+#page {
+ width: 960px;
+ background: #f0f0f0;
+ margin: 0px auto;
+}
+
+#topbar {
+ width: 960px;
+ height: 3em;
+ background: #911;
+ margin: 0px auto;
+ clear: both;
+ color: #f0f0f0;
+}
+
+#content {
+ padding: 12px;
+ clear: both;
+ color: #333;
+}
+
+#content h2 {
+ font-size: 1.75em;
+ font-weight: bold;
+ font-family: 'Helvetica Neue', Calibri, sans-serif;
+}
+
+
+#content h3 {
+ font-size: 1.25em;
+ font-weight: bold;
+ font-family: 'Helvetica Neue', Calibri, sans-serif;
+}
+
+
+#header {
+ float: left;
+ height: 3em;
+ line-height: 3em;
+ vertical-align: bottom;
+ padding-left: 1em;
+}
+
+#header h1 {
+ font-size: 1.25em;
+ font-weight: bold;
+}
+
+#navigation {
+ float: right;
+ height: 3em;
+ letter-spacing:-0.05em;
+ line-height: 3em;
+ padding-right: 1em;
+ z-index: 0;
+}
+
+#navigation li {
+ display: inline;
+ list-style-type: none;
+}
+
+#navigation li a {
+ display: inline-block;
+ padding: 0px 8px;
+ height: 3em;
+}
+
+#navigation li a:link, #navigation li a:visited {
+ color: #f0f0f0;
+ background: #911;
+}
+
+#navigation li a:hover {
+ color: #911;
+ background: #f0f0f0;
+}
+
+
+div.post {
+ width: 936px;
+}
+
+div.post h2 {
+ clear: both;
+ margin-left: 248px;
+}
+
+div.post p {
+ margin-bottom: 1em;
+}
+
+ol, ul {
+ margin-bottom: 1em;
+}
+
+div.post li {
+ margin-left: 2em;
+}
+
+div.post-meta {
+ width: 230px;
+ padding-right: 18px;
+ float: left;
+}
+
+div.post-date {
+ font-size: 70%;
+ margin-top:2px;
+ margin-bottom: 1em;
+}
+
+div.post-summary {
+ letter-spacing:-0.05em;
+ font-weight: bold;
+ line-height:1.25em;
+ color: #666;
+}
+
+div.post-content {
+ font-family:Georgia,Palatino,Times,'Times New Roman',serif;
+ font-size: 90%;
+ width: 688px;
+ float: left;
+}
+
+div.post-comments {
+ clear: both;
+ margin-top: 1em;
+ border-top: 1px solid #666;
+ padding-left: 248px;
+}
+
+
+#intro {
+ position: absolute;
+ height: 210px;
+ width: 100%;
+ top: 3em;
+ left: 0;
+ background: url(/static/i/intro-background.png) repeat-x;
+ width: 100%;
+ text-align: center;
+ color: #f3f3f3;
+ letter-spacing:-0.05em;
+ line-height:2em;
+ z-index: 2;
+ min-width: 960px;
+ overflow: hidden;
+}
+
+#intro table { width: 100%; height: 100%; }
+
+#intro td {
+ font-size: 2em;
+ width: 100%;
+ height: 100%;
+}
+
+#hello { color: #ee2; }
+
+
+#footer {
+ clear: both;
+}
+
+
+td.date {
+ width:148px;
+ padding-right:12px;
+ font-size:0.6em;
+ text-align:right;
+}
+
+td.summary {
+ width:308px;
+ padding-left: 12px;
+ font-size:0.6em;
+}
+
+td.title {
+ width: 480px;
+ font-weight: bold;
+}
+
+
+#recently-bookmarked, #links, #recent-posts {
+ margin-bottom: 2em;
+}
+
+#recently-bookmarked h2, #links h2, #recent-posts h2 {
+ border-bottom: 1px solid #333;
+ padding-left: 158px;
+}
+
+h2 a:link, h2 a:visited {
+ color: #333;
+}
+
+h2 a:hover {
+ color: #49628f;
+}
+
+
+pre {
+ font-size: 0.8em;
+ margin-left: 2em;
+ margin-bottom: 1em;
+}
+
+
+#recently-bookmarked tr, #links tr, #recent-posts tr {
+ border-bottom: 1px dotted #666;
+}
BIN  static/i/background.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/i/cc-license-badge.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/i/grid-background.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
BIN  static/i/intro-background.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 templates/about.st
@@ -0,0 +1,32 @@
+$htmlheader()$
+<body>
+ <div id="page">
+ <div id="topbar">
+ $header()$
+ $navigation()$
+ </div>
+
+ <div id="content">
+ <div id="about">
+ <h2>about me (list format)</h2>
+
+ <ul>
+ <li>I'm a 29-year old computer programmer and musician
+ living in Toronto, Ontario, Canada.</li>
+ <li>I did an MSc. in Computer
+ Science <a href="http://flint.cs.yale.edu/">here</a>.</li>
+ <li>As of April 15, I'll be working
+ at <a href="http://www.polarmobile.com/">Polar
+ Mobile</a>.</li>
+ <li>I'm engaged to be married (sorry ladies!) to the
+ wonderful Amy Lee Anne Emel this summer.</li>
+ <li>I enjoy playing basketball.</li>
+ <li>I used to be in <a href="http://ninjahighschool.info/">a
+ positive hardcore rap group</a>.</li>
+ </ul>
+ </div>
+ </div>
+ </div>
+$footer()$
+ </body>
+</html>
24 templates/contact.st
@@ -0,0 +1,24 @@
+$htmlheader()$
+<body>
+ <div id="page">
+ <div id="topbar">
+ $header()$
+ $navigation()$
+ </div>
+
+ <div id="content">
+ <div id="about">
+ <h2>contact me</h2>
+ <p>You can email me at:
+ <script type="text/javascript">
+ <!--
+ h='&#x67;&#114;&#x65;&#x67;&#x6f;&#114;&#x79;&#x63;&#x6f;&#108;&#108;&#x69;&#110;&#x73;&#46;&#110;&#x65;&#116;';a='&#64;';n='&#x67;&#114;&#x65;&#x67;';e=n+a+h;
+ document.write('<a h'+'ref'+'="ma'+'ilto'+':'+e+'">'+'<code>'+e+'</code>'+'<\/'+'a'+'>');
+ // -->
+ </script><noscript>&#x67;&#114;&#x65;&#x67;&#32;&#x61;&#116;&#32;&#x67;&#114;&#x65;&#x67;&#x6f;&#114;&#x79;&#x63;&#x6f;&#108;&#108;&#x69;&#110;&#x73;&#32;&#100;&#x6f;&#116;&#32;&#110;&#x65;&#116;</noscript>, or connect with me on <a href="http://www.linkedin.com/in/gregorydavidcollins">LinkedIn</a></p>
+ </div>
+ </div>
+ </div>
+$footer()$
+ </body>
+</html>
5 templates/deliciousBookmark.st
@@ -0,0 +1,5 @@
+<tr>
+<td class="date">$elem.date$</td>
+<td class="title"><a href="$elem.href$">$elem.title$</a></td>
+<td class="summary">$elem.summary$</td>
+</tr>
4 templates/disqus.st
@@ -0,0 +1,4 @@
+<div id="disqus_thread"></div>
+<script type="text/javascript" src="http://disqus.com/forums/gregorycollins/embed.js"></script>
+<noscript><a href="http://gregorycollins.disqus.com/?url=ref">View the discussion thread.</a></noscript>
+<a href="http://disqus.com" class="dsq-brlink">Blog comments powered by <span class="logo-disqus">Disqus</span></a>
10 templates/disqusThread.st
@@ -0,0 +1,10 @@
+ <div class="post-comments">
+ <div id="disqus_thread"></div>
+ <script type="text/javascript"
+ src="http://disqus.com/forums/gregorycollins/embed.js"></script>
+ <noscript><a href="http://gregorycollins.disqus.com/?url=ref">View
+ the discussion
+ thread.</a></noscript><a href="http://disqus.com"
+ class="dsq-brlink">blog comments powered
+ by <span class="logo-disqus">Disqus</span></a>
+ </div>
14 templates/disqusfooter.st
@@ -0,0 +1,14 @@
+<script type="text/javascript">
+//<![CDATA[
+(function() {
+ var links = document.getElementsByTagName('a');
+ var query = '?';
+ for(var i = 0; i < links.length; i++) {
+ if(links[i].href.indexOf('#disqus_thread') >= 0) {
+ query += 'url' + i + '=' + encodeURIComponent(links[i].href) + '&';
+ }
+ }
+ document.write('<script charset="utf-8" type="text/javascript" src="http://disqus.com/forums/gregorycollins/get_num_replies.js' + query + '"></' + 'script>');
+ })();
+//]]>
+</script>
4 templates/footer.st
@@ -0,0 +1,4 @@
+ <div id="footer"><span>
+ All content licensed under <a href="http://creativecommons.org/licenses/by-nc/3.0/">Creative Commons</a>: Attribution, Non-Commercial
+ </span>
+ </div>
9 templates/googleanalytics.st
@@ -0,0 +1,9 @@
+ <script type="text/javascript"><!--
+ var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
+ document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
+
+ try {
+ var pageTracker = _gat._getTracker("UA-8101711-1");
+ pageTracker._trackPageview();
+ } catch(err) {}
+ --></script>
4 templates/header.st
@@ -0,0 +1,4 @@
+ <div id="header">
+ <h1>gregory david collins</h1>
+ </div>
+
78 templates/home.st
@@ -0,0 +1,78 @@
+$htmlheader()$
+<body>
+ <div id="intro">
+ <table><tr><td align="middle" valign="middle">
+ <span id="hello">Hi!</span> Gregory David Collins is a
+ compu-dork living in Toronto, Canada.
+ </td></tr></table>
+ </div>
+
+ <div id="page">
+ <div id="topbar">
+ $header()$
+ $navigation()$
+ </div>
+
+ <div id="content">
+ <div id="recent-posts">
+ <h2>posts</h2>
+ <table>
+ <tr>
+ <td class="date">march 26, 2009</td>
+ <td class="title">
+ <a href="/posts/2009/03/26/building-a-website-part-1">Building
+ a website with Haskell, part 1</a>
+ </td>
+ <td class="summary">
+ Using
+ the <a href="http://www.happstack.com/">happstack</a> web
+ framework to power a simple personal website.
+ </td>
+ </tr>
+ </table>
+ </div>
+
+ <div id="recently-bookmarked">
+ <h2><a href="http://delicious.com/how.gauche/">recently bookmarked</a></h2>
+ <table>
+ $recentBookmarks:{elem | $deliciousBookmark()$}$
+ </table>
+ </div>
+
+ <div id="links">
+ <h2>links</h2>
+ <table>
+ <tr>
+ <td class="date"></td>
+ <td class="title">
+ <a href="http://github.com/gregorycollins">my github</a>
+ </td>
+ <td class="summary">
+ I've posted some code I'm working on here,
+ including <a href="http://github.com/gregorycollins/homepage/tree/v0.1">the
+ source for this website</a>.
+ </td>
+ </tr>
+
+ <tr>
+ <td class="date"></td>
+ <td class="title">
+ <a href="http://www.blocksblocksblocks.com/">blocks recording club</a>
+ </td>
+ <td class="summary">
+ The □□□□□□ Recording Club is an artist-owned worker’s
+ co-operative based in Toronto. I am its
+ &ldquo;web-goblin&rdquo; as well as a member of the (now
+ defunct) □□□□□□ rap group <em>Ninja High School</em>.
+ </td>
+ </tr>
+ </table>
+ </div>
+
+
+ </div>
+
+ $footer()$
+ </div>
+
+$htmlfooter()$
3  templates/htmlfooter.st
@@ -0,0 +1,3 @@
+$disqusfooter()$
+</body>
+</html>
10 templates/htmlheader.st
@@ -0,0 +1,10 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <head>
+ <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+ <title>gregory david collins$websiteTitleExtra$</title>
+ <link rel="stylesheet" type="text/css" href="/static/css/$whichCss$.css"/>
+ <link rel="stylesheet" type="text/css" href="/static/css/highlighting.css"/>
+
+ $! googleanalytics() !$
+ </head>
8 templates/navigation.st
@@ -0,0 +1,8 @@
+ <div id="navigation">
+ <ul>
+ <li id="home"><a href="/">home</a></li>
+ <li id="about"><a href="/about">about</a></li>
+ <li id="contact"><a href="/contact">contact</a></li>
+ </ul>
+ </div>
+
27 templates/post.st
@@ -0,0 +1,27 @@
+$htmlheader()$
+<body>
+ <div id="page">
+ <div id="topbar">
+ $header()$
+ $navigation()$
+ </div>
+
+ <div id="content">
+ <div class="post">
+ <h2>$postTitle$</h2>
+ <div class="post-meta">
+ <div class="post-date">$postDate$</div>
+ <div class="post-summary">$postSummary$</div>
+ </div>
+
+ <div class="post-content">
+ $postContent$
+ </div>
+
+ $disqusThread()$
+ </div>
+ </div>
+ </div>
+$footer()$
+ </body>
+</html>
65 templates/temppost1.st
@@ -0,0 +1,65 @@
+<p
+>I&rsquo;ve been meaning for a long time to write more about the projects I&rsquo;m working on. I&rsquo;ve had this web space for ages and have never really done anything useful with it, and I&rsquo;ve decided the time has come to change that.</p
+><h3 id="why-haskell"
+>Why Haskell?</h3
+><p
+>Lately most of my personal project time has been spent hacking on <a href="http://www.haskell.org/"
+ >Haskell</a
+ > code. I&rsquo;m a strong proponent of functional programming: my years of research and study in computer science and in programming have lead me to the conclusion that functional programs are simpler, shorter, more likely to be correct, and quicker to write than programs written in standard industry languages like Java or C++.</p
+><p
+>I did most of my graduate work in <a href="http://www.smlnj.org/"
+ >Standard ML</a
+ >. Back then (2001&ndash;2003), Haskell was still pretty immature, not very fast, and even more &ldquo;boutique&rdquo; than it is now. ML, &ldquo;Standard&rdquo; and <a href="http://caml.inria.fr/ocaml/"
+ >otherwise</a
+ > had several industrial-strength compilers, reasonable standard libraries, and a small but dedicated user community.</p
+><p
+>The tables have turned: Haskell has improved so much in the past six years, it&rsquo;s uncanny. The <a href="http://www.haskell.org/ghc/"
+ >GHC</a
+ > compiler is getting acceptably close to C&rsquo;s performance (between 2X&ndash;3X in my experience), libraries are proliferating, the user community is thriving. Much progress has been made on a lot of the old issues (e.g.: &ldquo;space leaks everywhere&rdquo;). Haskell is definitely my choice for the current &ldquo;best of breed&rdquo; functional language.</p
+><p
+>So when choosing the &ldquo;technology stack&rdquo; for this website, I decided that I would write the backend in Haskell. In a pathetic attempt at evangelism, I&rsquo;m also releasing the code on <a href="http://github.com/gregorycollins/homepage/tree/v0.1"
+ >my github page</a
+ > as a tutorial in the hope that it might help other people to make their own happstack websites, or at least see an example of a functioning (albeit simple) one &ldquo;in the wild.&rdquo;</p
+><h3 id="haskell-web-frameworks"
+>Haskell web frameworks</h3
+><p
+>A quick survey of <a href="http://hackage.haskell.org/"
+ >Hackage</a
+ > yields a bunch of web toolkits, of varying levels of immaturity. My main requirements are:</p
+><ol style="list-style-type: decimal;"
+><li
+ >simple, flexible, easy to use</li
+ ><li
+ >fast, preferably supporting <a href="http://www.haskell.org/ghc/docs/latest/html/libraries/bytestring/Data-ByteString.html"
+ >bytestrings</a
+ >.</li
+ ><li
+ >doesn&rsquo;t gobble or leak memory</li
+ ></ol
+><p
+>After a bit of research I settled on <a href="http://happstack.com/"
+ >happstack</a
+ >. Happstack is still under-documented and a little over-complicated in parts, and I won&rsquo;t be trusting my data to its &ldquo;MACID&rdquo; system anytime soon; but it supports bytestrings, it&rsquo;s modular enough to be used in mix-and-match fashion, and it&rsquo;s quick: on my last-gen Macbook Pro, it serves up static files about half as fast as Apache. This is pretty impressive given that Apache uses <code
+ >sendfile()</code
+ > to serve static files, avoiding a couple of copies and context switches in the process. For an equivalent dynamic program I suspect the race might be a little closer.</p
+><h3 id="to-be-continued"
+>To be continued&hellip;</h3
+><p
+>This website is running on happstack now (with static content), reverse-proxied through apache. Although happstack&rsquo;s documentation is sketchy, the tutorial contained enough detail to get me going. After spending some time to grok the code and writing a few combinators, I think I&rsquo;ve come up with some code that&rsquo;s a little bit cleaner than the tutorial&rsquo;s. And in case you were wondering &ldquo;is it fast?&rdquo;, here&rsquo;s the result of an <code
+ >httperf</code
+ > run (Macbook Pro, 2.4 GHz Core 2 Duo) for the home page:</p
+><pre
+><code
+ >Connection rate: 584.8 conn/s (1.7 ms/conn, &lt;=1 concurrent connections)
+Connection time [ms]: min 0.4 avg 1.7 max 15.1 median 1.5 stddev 0.6
+</code
+ ></pre
+><p
+>If I turn gzip compression on, I get nearly 3000 conn/s &mdash; but I had to turn it off because it looks like there&rsquo;s a bug serving static files.</p
+><p
+>My next post in this series will outline the software design behind this basic happstack website and demonstrate how simple it was to integrate the recent bookmarks from my <a href="http://www.delicious.com/"
+ >delicious</a
+ > feed. In part three, we&rsquo;ll bootstrap a simple content management system, with all (some) of the usual amenities: RSS feeds, archives, markdown, etc. If you&rsquo;re curious to read the source code now, you can browse it at <a href="http://github.com/gregorycollins/homepage/tree/v0.1"
+ >my github page</a
+ >.</p
+>
Please sign in to comment.
Something went wrong with that request. Please try again.