From 6bc399d9ca859dd84991f066754490a4283cff36 Mon Sep 17 00:00:00 2001 From: Anton Ekblad Date: Tue, 14 Mar 2017 14:28:07 +0100 Subject: [PATCH] Generalize some names and hide implementation details. --- CCWF.hs | 11 ++ CCWF/Config.hs | 75 +++++++++ CCWF/Impl.hs | 217 ++++++++++++++++++++++++ pages/about.md | 2 +- pages/lectures.md | 2 +- pages/project.md | 13 +- pages/schedule.md | 4 +- website.hs | 421 +++++++++++----------------------------------- 8 files changed, 414 insertions(+), 331 deletions(-) create mode 100644 CCWF.hs create mode 100644 CCWF/Config.hs create mode 100644 CCWF/Impl.hs diff --git a/CCWF.hs b/CCWF.hs new file mode 100644 index 0000000..29f5d0b --- /dev/null +++ b/CCWF.hs @@ -0,0 +1,11 @@ +-- | Chalmers course +module CCWF + ( -- * Configuring and updating pages + Website (..), Info (..), Materials (..) + , URL, Teacher (..), Lecture (..) + + -- * Building the website + , mkWebsite + ) where +import CCWF.Config +import CCWF.Impl diff --git a/CCWF/Config.hs b/CCWF/Config.hs new file mode 100644 index 0000000..522ecf0 --- /dev/null +++ b/CCWF/Config.hs @@ -0,0 +1,75 @@ +-- | Per-website configuration for CCWF. +module CCWF.Config where + +type URL = String + +-- | A course website consists of relatively static course information, and +-- relatively static course materials. +data Website = Website + { courseInfo :: Info + , courseMaterials :: Materials + } + +-- | Lectures, files and news items: course materials that will likely be +-- updated frequently. +data Materials = Materials + { -- | Latest news for the course, in order from newest to latest. + -- Can contain markdown. + newsItems :: [String] + -- | All lectures for the course. These make up the table on the @lectures@ + -- page. + , lectures :: [Lecture] + -- | Files that are related to the course, but not tied to any particular + -- lecture. + , miscFiles :: [FilePath] + } + +-- | Course information: this should change only about once per year. +data Info = Info + { -- | The main teacher of the course. + teacher :: Teacher + -- | The examiner of the course, if different from the main teacher. + , examiner :: Maybe Teacher + -- | Any assistants working on the course. + -- The name, email, etc. of the first assistant in the list are accessible + -- as @assistant@, @assistantemail@, etc. + , assistants :: [Teacher] + -- | URL of the course syllabus. This is mandatory for all Chalmers courses. + , syllabusURL :: URL + -- | URL of the course's Google group, if any. + , googleGroupURL :: Maybe URL + -- | URL of the course's lab submission system, if any. + , submissionURL :: Maybe URL + -- | URL of external schedule for course, if any. + , scheduleURL :: Maybe URL + -- | All lab deadlines. + , labDeadlines :: [String] + } + +-- | Bio for the teacher responsible for the course. +-- Name, phone and email are mandatory. +data Teacher = Teacher + { -- | Full name of teacher. + teacherName :: String + -- | Work email. + , teacherEmail :: String + -- | Work phone. + , teacherPhone :: String + -- | Does the teacher have a biography? + , teacherBioURL :: Maybe URL + -- | Room number of/directions to teacher's office. + , teacherOffice :: Maybe String + -- | Office hours, when the teacher is available for answering questions. + , teacherHours :: Maybe String + } + +-- | An entry in the list of course lectures. +data Lecture = Lecture + { -- | When did the lecture take place? + lectureDate :: String + -- | What was it about? Preferably one (short) sentence. + , lectureDescription :: String + -- | Any auxilliary material associated with the course. + -- These files need to be present in the @files@ directory. + , lectureFiles :: [(String, FilePath)] + } diff --git a/CCWF/Impl.hs b/CCWF/Impl.hs new file mode 100644 index 0000000..86d48bd --- /dev/null +++ b/CCWF/Impl.hs @@ -0,0 +1,217 @@ +{-# LANGUAGE OverloadedStrings, RecordWildCards, TupleSections #-} +module CCWF.Impl where +import Hakyll +import Control.Monad +import Data.Char +import Data.List +import Data.Time +import Data.Function (on) +import System.Directory (doesFileExist) +import System.FilePath (takeBaseName) +import Text.Read +import CCWF.Config + +-- | Build a website from a configuration. +mkWebsite :: Website -> IO () +mkWebsite (Website (Info{..}) (Materials{..})) = do + -- Check that all lecture files are present + (courseYear, _, _) <- toGregorian . utctDay <$> getCurrentTime + let files = [ "./files/" ++ file + | fs <- map lectureFiles lectures + , (_, file) <- fs + ] ++ map ("./files/" ++) miscFiles + files' <- filterM (fmap not . doesFileExist) files + unless (null files') $ do + putStrLn $ "The following essential files could not be found:" + forM_ files' $ putStrLn . (" " ++) + error "Some files were missing from the ./files directory!" + + hakyll $ do + -- CSS files are just compressed + match "css/*" $ do + route idRoute + compile compressCssCompiler + + -- JS files are just copied + match "js/*" $ do + route idRoute + compile copyFileCompiler + + -- Images too + match "images/*" $ do + route idRoute + compile copyFileCompiler + + -- Lectures files are copied into files/ + match "files/*" $ do + route idRoute + compile copyFileCompiler + + -- Don't do anything fancy with templates + match "templates/*" $ do + compile templateCompiler + + -- Add an explicit dependency on this file + match "website.hs" $ do + compile $ makeItem () + hsdep <- makePatternDependency "website.hs" + + -- Pages are read from the @pages@ subdirectory. + rulesExtraDependencies [hsdep] $ match "pages/*.md" $ do + route $ gsubRoute "pages/" (const "") + `composeRoutes` gsubRoute ".md" (const "") + `composeRoutes` customRoute (mkPath . toFilePath) + compile $ do + metas <- getAllMetadata "pages/*.md" + self <- takeBaseName <$> getResourceFilePath + let -- Menu items, sorted by their menuorder metadata entry. + -- Items without menuorder are sorted randomly among themselves, + -- after any entries with a specified order. + menuitems = map snd $ sortBy (\a b -> compare (fst a) (fst b)) $ + [ (menuOrder meta, Item (noIndex ident) title) + | (ident, meta) <- metas + , Just title <- [lookupString "title" meta] + ] + -- Context for the page content; contains metadata set in the + -- markdown file for each page, as well as some info set at the + -- top of this file. + ctx = mconcat + [ constField "year" (show courseYear) + , constField "syllabus" syllabusURL + , constField "group" (maybe "" id googleGroupURL) + , constField "submissions" (maybe "" id submissionURL) + , constField "schedule" (maybe "" id scheduleURL) + , constField "S" "$" + , mconcat + [ constField ("deadline" ++ show num) date + | (date, num) <- zip labDeadlines [1..] + ] + , teacherFields "teacher" teacher + , maybe mempty (teacherFields "examiner") examiner + , case assistants of + (ass:_) -> teacherFields "assistant" ass + _ -> mempty + , listField "assistants" mkTeacherListItemCtx (pure assistantItems) + , listField "menuitems" (mkMenuCtx self) + (pure menuitems) + , listField "lectures" mkLectureCtx + (zipWithM (curry makeItem) [1..] lectures) + , if length newsItems > 3 + then listField "news" defaultContext (pure newsItemItems) + else mempty + , listField "latestnews" defaultContext (pure $ take 3 newsItemItems) + , defaultContext + ] + applyMeAsTemplate ctx ctx + where + newsItemItems = map (Item (fromFilePath "")) newsItems + assistantItems = map (Item (fromFilePath "")) assistants + +-- | Create a list item context for a teacher. +-- Used for the list of assistants. +mkTeacherListItemCtx :: Context Teacher +mkTeacherListItemCtx = mconcat + [ field "full" $ \(Item _ t) -> pure $ teacherName t + , field "email" $ \(Item _ t) -> pure $ teacherEmail t + , field "bio" $ \(Item _ t) -> pure $ maybe "" id $ teacherBioURL t + , field "phone" $ \(Item _ t) -> pure $ teacherPhone t + , field "office" $ \(Item _ t) -> pure $ maybe "" id $ teacherOffice t + , field "hours" $ \(Item _ t) -> pure $ maybe "" id $ teacherHours t + ] + +-- | Create context fields for a teacher. +teacherFields :: String -> Teacher -> Context a +teacherFields prefix (Teacher {..}) = mconcat + [ constField prefix (head $ words teacherName) + , constField (prefix ++ "full") teacherName + , constField (prefix ++ "email") teacherEmail + , constField (prefix ++ "bio") (maybe "" id teacherBioURL) + , constField (prefix ++ "phone") teacherPhone + , maybe mempty (constField (prefix ++ "office")) teacherOffice + , maybe mempty (constField (prefix ++ "hours")) teacherHours + ] + +-- | Get the menu order from a piece of metadata. +menuOrder :: Metadata -> Int +menuOrder meta = + maybe 1000000 + (round :: Double -> Int) + (readMaybe =<< lookupString "menuorder" meta) + +-- | Set the CSS class of a menu item based on its identifier and the +-- identifier of the current page. If the identifier matches the current +-- page, the item should be displayed as selected. +menuItemClass :: String -> Item a -> String +menuItemClass current itm + | current == takeBaseName (toFilePath (itemIdentifier itm)) = + "selected" + | current == "index" && null (toFilePath (itemIdentifier itm)) = + "selected" + | otherwise = + "unselected" + +mkLectureCtx :: Context (Int, Lecture) +mkLectureCtx = mconcat + [ field "date" $ \(Item _ (_, l)) -> do + pure $ lectureDate l + , field "description" $ \(Item _ (_, l)) -> do + pure $ show $ lectureDescription l + , listFieldWith "lecturefiles" defaultContext $ \(Item _ (_, l)) -> do + pure $ [ Item (fromFilePath file) name + | (name, file) <- lectureFiles l] + , field "number" $ \(Item _ (n, _)) -> + pure $ show n + ] + +-- | Build the context for the menu. The @submenuitems@ field will contain all +-- sub-menu items for the currently active page. +-- Note that URLs are relativized in a horrible, hacky way, which needs to +-- be changed if the directory hierarchy of the course homepage changes. +mkMenuCtx :: String + -> Context String +mkMenuCtx self = mconcat + [ field "selected" (pure . menuItemClass self) + , listFieldWith "submenuitems" defaultContext $ \itm -> do + when (takeBaseName (toFilePath (itemIdentifier itm)) /= self) $ do + fail "not my submenu" + submenuitems <- words <$> getMetadataField' (itemIdentifier itm) "submenu" + forM (zip (mkTitles submenuitems) submenuitems) $ \(title, ident) -> do + pure $ Item (fromFilePath $ "#" ++ ident) title + , field "url" $ pure . itemToUrl + , defaultContext + ] + where + itemToUrl = ("/" ++) . takeBaseName . toFilePath . itemIdentifier + mkTitles = map capitalize . map toSpaces + + capitalize (x:xs) = toUpper x : xs + + toSpaces = map toSpace + + toSpace '_' = ' ' + toSpace c = c + +-- | Render the current page as markdown, then use the current page as a +-- template (i.e. it may include substitutions), and render it as markdown +-- again. Finally, apply the default template to the end result and +-- relativize all URLs. +applyMeAsTemplate :: Context String -> Context String -> Compiler (Item String) +applyMeAsTemplate pageCtx topCtx = do + getResourceBody + >>= renderPandoc + >>= applyAsTemplate pageCtx + >>= renderPandoc + >>= loadAndApplyTemplate "templates/default.html" topCtx + >>= relativizeUrls + +-- | Turn a path @p@ (stripped of prefix and extension) into @p/index.html@. +mkPath :: FilePath -> FilePath +mkPath "index" = "index.html" +mkPath p = p ++ "/index.html" + +-- | Turn an identifier into an empty identifier if the identifier is @index@, +-- otherwise return the identifier unchanged. +noIndex :: Identifier -> Identifier +noIndex ident + | takeBaseName (toFilePath ident) == "index" = fromFilePath "" + | otherwise = ident diff --git a/pages/about.md b/pages/about.md index fc53395..67f3f4a 100644 --- a/pages/about.md +++ b/pages/about.md @@ -100,4 +100,4 @@ instructions. Two things should be noted: The second step is to actually submit your solution, by clicking the *Submit* button. **It is not enough to only upload; you must also submit!** -[Go to the submission system](\$fire\$) +[Go to the submission system](\$submissions\$) diff --git a/pages/lectures.md b/pages/lectures.md index 61c6ac5..63667eb 100644 --- a/pages/lectures.md +++ b/pages/lectures.md @@ -9,7 +9,7 @@ Lectures Lectures take place on Tuesdays 13-15 in EL41 and Fridays 13-15 in EL42. Not all available times are used. The complete schedule is available in -[TimeEdit](\$timeedit\$). Note that if you use an iCal or vCal-compatible +[TimeEdit](\$schedule\$). Note that if you use an iCal or vCal-compatible calendar, you can download the course schedule from TimeEdit. The following is a preliminary plan for the lectures. Changes will be announced diff --git a/pages/project.md b/pages/project.md index 9f4f5ea..5347b9f 100644 --- a/pages/project.md +++ b/pages/project.md @@ -25,7 +25,6 @@ submenu: deadlines grading javalette frontend extensions testing code_generation * [x86 generation](#x86) * [Optimization study](#optstudy) * [Other extensions](#moreextensions) -* [Testing](#testing) @@ -1351,7 +1350,7 @@ declare i32 @printf(i8*, ...) nounwind ``` What remains is a definition of the format string `@fstr` as a global constant -(`\0A` is `\n`), the `getelementpointer` instruction that returns a pointer to +(`\\0A` is `\\n`), the `getelementpointer` instruction that returns a pointer to the beginning of the format string and a call to `printf` with the result value. Note that the call to `printInt` has been inlined, i.e. replaced by a call to `printf`; so linking includes optimizations across files. @@ -1360,17 +1359,17 @@ We can now run `a.out.bc` using the just-in-time compiler `lli`. Or, if we prefer, we can produce native assembly code with `llc`. On my x86 machine, this gives -```asm +``` .text .align 4,0x90 .globl _main _main: - subl $$12, %esp - movl $$5040, 4(%esp) - movl $$_fstr, (%esp) + subl $S$12, %esp + movl $S$5040, 4(%esp) + movl $S$_fstr, (%esp) call _printf xorl %eax, %eax - addl $$12, %esp + addl $S$12, %esp ret .cstring _fstr: ## fstr diff --git a/pages/schedule.md b/pages/schedule.md index ebb8bd3..cd068f7 100644 --- a/pages/schedule.md +++ b/pages/schedule.md @@ -3,9 +3,9 @@ title: Schedule menuorder: 5 --- - + Schedule ======== -The official schedule for the course is available on [TimeEdit](\$timeedit\$). +The official schedule for the course is available on [TimeEdit](\$schedule\$). diff --git a/website.hs b/website.hs index 28acf3b..4808229 100644 --- a/website.hs +++ b/website.hs @@ -1,332 +1,113 @@ -{-# LANGUAGE OverloadedStrings, RecordWildCards, TupleSections #-} -import Hakyll -import Control.Monad -import Data.Char -import Data.List -import Data.Time -import System.Directory (doesFileExist) -import System.FilePath (takeBaseName) -import Text.Read +import CCWF ------------------------------------------------------------------------- -- * News, lectures and files. Routine updates should mostly happen here. ------------------------------------------------------------------------- --- | Latest news, in order from newest to latest. Can contain markdown. -newsItems :: [Item String] -newsItems = map (Item (fromFilePath "")) $ - [ "June 28. Responsive design - it now looks *fabulous* on mobile devices!" - , "June 27. Birth of the new course homepage." - ] - --- | All lectures for the course. These make up the table on the @lectures@ --- page. --- --- Make sure to put any files mentioned in the list of lecture files --- into the @files@ subdirectory before rebuilding the course homepage, to --- ensure that they all get included. -lectures :: [Lecture] -lectures = - [ Lecture "April 12" "Introduction, project overview" - [("old", "lect01-6up.pdf")] - , Lecture "April 15" "Software Engineering for Compilers" - [("old", "lect02-6up.pdf"), ("code", "state.tar.gz")] - , Lecture "April 22" "LLVM: tools, language" - [("old", "lect03-6up.pdf")] - , Lecture "April 26" "Code generation for LLVM" - [("old", "lect04-6up.pdf"), ("code", "evenodd.ll")] - , Lecture "May 3" "Project extensions: arrays, dynamic structures, objects" - [("old", "lect05-6up.pdf")] - , Lecture "May 10" "Code generation for x86" - [("old", "lect06-6up.pdf")] - , Lecture "May 13" "Functions" - [("old", "lect07-6up.pdf")] - , Lecture "May 16" "Control flow graphs, data analysis" - [("old", "lect08-6up.pdf")] - , Lecture "May 24" "Guest lecture/project summary" - [("old", "lect09-6up.pdf"), ("guest", "/guest_lecture_myreen-6up.pdf")] - ] - --- | Files we provide that are not tied to any particular lecture. -miscFiles :: [FilePath] -miscFiles = ["runtime.ll", "Javalette.cf", "tester.tar.gz"] - +materials = Materials + { -- | Latest news, in order from newest to latest. Can contain markdown. + newsItems = + [ "June 28. Responsive design - it now looks *fabulous* on mobile devices!" + , "June 27. Birth of the new course homepage." + ] + + -- | All lectures for the course. These make up the table on the @lectures@ + -- page. + -- + -- Make sure to put any files mentioned in the list of lecture files + -- into the @files@ subdirectory before rebuilding the course homepage, to + -- ensure that they all get included. + , lectures = + [ Lecture "April 12" "Introduction, project overview" + [("old", "lect01-6up.pdf")] + , Lecture "April 15" "Software Engineering for Compilers" + [("old", "lect02-6up.pdf"), ("code", "state.tar.gz")] + , Lecture "April 22" "LLVM: tools, language" + [("old", "lect03-6up.pdf")] + , Lecture "April 26" "Code generation for LLVM" + [("old", "lect04-6up.pdf"), ("code", "evenodd.ll")] + , Lecture "May 3" "Project extensions: arrays, dynamic structures, objects" + [("old", "lect05-6up.pdf")] + , Lecture "May 10" "Code generation for x86" + [("old", "lect06-6up.pdf")] + , Lecture "May 13" "Functions" + [("old", "lect07-6up.pdf")] + , Lecture "May 16" "Control flow graphs, data analysis" + [("old", "lect08-6up.pdf")] + , Lecture "May 24" "Guest lecture/project summary" + [("old", "lect09-6up.pdf"), ("guest", "/guest_lecture_myreen-6up.pdf")] + ] + + -- | Files we provide that are not tied to any particular lecture. + , miscFiles = ["runtime.ll", "Javalette.cf", "tester.tar.gz"] + } ---------------------------------------------------------------------------- -- * Course-specific configuration - should only need changing once per year ---------------------------------------------------------------------------- --- | Name of course responsible, plus email. --- This is parameterized to make course handovers easier, since the name --- and/or email of the course responsible pops up here and there throughout --- the project description and course homepage.. --- --- First name is available to templates as @teacher@, full name as --- @teacherfull@, email as @teacheremail@, bio URL as @teacherbio@, phone --- as @teacherphone@, office as @teacheroffice@ and office hours as --- @teacherhours@. --- --- Currently, the examiner and the de facto course responsible are not one --- and the same. If this situation changes, or if the examiner changes, don't --- forget to make the appropriate modifications to @pages/about.md@. --- --- Similarly, if the course should evolve to need more than one assistant, --- or add change the duties of the course responsible or assistant , --- please update @pages/about.md@ as appropriate. Use 'labDeadlines' --- and its use in @pages/exam.md@ as an example of adding a list of things --- to a page. -teacher :: Teacher -teacher = Teacher - { teacherName = "Alex Gerdes" - , teacherEmail = "alexg \"at\" chalmers.se" - , teacherBioURL = Just "http://www.cse.chalmers.se/~alexg" - , teacherPhone = "+46 31 772 6154" - , teacherOffice = Just "Room 6466 in the EDIT building." - , teacherHours = Just "Thursdays 13:15 - 15:00" - } - --- | Same information as for 'teacher'. All fields are available to templates --- the same as for @teacher@, but with the prefix @assistant@ instead of --- @teacher@. -assistant :: Teacher -assistant = Teacher - { teacherName = "Anton Ekblad" - , teacherEmail = "antonek \"at\" chalmers.se" - , teacherPhone = "+46 31 772 1028" - , teacherBioURL = Just "http://ekblad.cc" - , teacherOffice = Just "Room 5463 in the EDIT building." - , teacherHours = Nothing - } - --- | URL of the official course syllabus for 2017. --- This changes every year: don't forget to update! --- Available to templates as @syllabus@. -syllabusURL :: String -syllabusURL = "https://www.student.chalmers.se/sp/course?course_id=24405" - --- | URL of the Google group for this year's instance. Don't forget to update! --- Available to templates as @group@. -googleGroupURL :: String -googleGroupURL = "javascript:alert('No group yet!');" - --- | URL of this year's Fire instance. Don't forget to update! --- Available to templates as @fire@. -fireURL :: String -fireURL = "javascript:alert('No Fire yet!');" - --- | URL of the course's TimeEdit schedule. Available to templates as --- @timeedit@. -timeEditURL :: String -timeEditURL = "https://se.timeedit.net/web/chalmers/db1/public/ri1X50gQ1560YvQQ05Z6779Y0Zy6007331Y50Q089.html" - --- | The deadlines for the three labs. --- Available to templates as @deadline1/2/3@. -labDeadlines :: [String] -labDeadlines = - [ "Sunday, April 24 at 23:59" - , "Sunday, May 15 at 23:59" - , "Sunday, May 29 at 23:59" - ] - - ---------------------------------------------------- --- * Only implementation details beyond this point. ---------------------------------------------------- - -data Teacher = Teacher - { teacherName :: String - , teacherEmail :: String - , teacherPhone :: String - , teacherBioURL :: Maybe String - , teacherOffice :: Maybe String - , teacherHours :: Maybe String - } - -data Lecture = Lecture - { lectureDate :: String - , lectureDescription :: String - , lectureFiles :: [(String, FilePath)] +info = Info + { -- | Name of course responsible, plus email. + -- This is parameterized to make course handovers easier, since the name + -- and/or email of the course responsible pops up here and there throughout + -- the project description and course homepage.. + -- + -- First name is available to templates as @teacher@, full name as + -- @teacherfull@, email as @teacheremail@, bio URL as @teacherbio@, phone + -- as @teacherphone@, office as @teacheroffice@ and office hours as + -- @teacherhours@. + teacher = Teacher + { teacherName = "Alex Gerdes" + , teacherEmail = "alexg \"at\" chalmers.se" + , teacherBioURL = Just "http://www.cse.chalmers.se/~alexg" + , teacherPhone = "+46 31 772 6154" + , teacherOffice = Just "Room 6466 in the EDIT building." + , teacherHours = Just "Thursdays 13:15 - 15:00" + } + + -- | The examiner of the course, if different from the course responsible. + , examiner = Nothing + + -- | Same information as for 'teacher'. All fields of the first assistant + -- are available to templates the same as for @teacher@, but with the + -- prefix @assistant@ instead of @teacher@. + -- The full list of assistants is available as @assistants@. + , assistants = + [ Teacher + { teacherName = "Anton Ekblad" + , teacherEmail = "antonek \"at\" chalmers.se" + , teacherPhone = "+46 31 772 1028" + , teacherBioURL = Just "http://ekblad.cc" + , teacherOffice = Just "Room 5463 in the EDIT building." + , teacherHours = Nothing + } + ] + + -- | URL of the official course syllabus for 2017. + -- This changes every year: don't forget to update! + -- Available to templates as @syllabus@. + , syllabusURL = "https://www.student.chalmers.se/sp/course?course_id=24405" + + -- | URL of the Google group for this year's instance. Don't forget to update! + -- Available to templates as @group@. + , googleGroupURL = Just "javascript:alert('No group yet!');" + + -- | URL of the lab submission system used this year, if any. + -- Don't forget to update! + -- Available to templates as @submissions@. + , submissionURL = Just "javascript:alert('No Fire yet!');" + + -- | URL of the course's TimeEdit schedule. Available to templates as + -- @schedule@. + , scheduleURL = Just "https://se.timeedit.net/web/chalmers/db1/public/ri1X50gQ1560YvQQ05Z6779Y0Zy6007331Y50Q089.html" + + -- | The deadlines for the three labs. + -- Available to templates as @deadline1/2/3@. + , labDeadlines = + [ "Sunday, April 24 at 23:59" + , "Sunday, May 15 at 23:59" + , "Sunday, May 29 at 23:59" + ] } -main = do - -- Check that all lecture files are present - (courseYear, _, _) <- toGregorian . utctDay <$> getCurrentTime - let files = [ "./files/" ++ file - | fs <- map lectureFiles lectures - , (_, file) <- fs - ] ++ map ("./files/" ++) miscFiles - files' <- filterM (fmap not . doesFileExist) files - unless (null files') $ do - putStrLn $ "The following essential files could not be found:" - forM_ files' $ putStrLn . (" " ++) - error "Some files were missing from the ./files directory!" - - hakyll $ do - -- CSS files are just compressed - match "css/*" $ do - route idRoute - compile compressCssCompiler - - -- JS files are just copied - match "js/*" $ do - route idRoute - compile copyFileCompiler - - -- Images too - match "images/*" $ do - route idRoute - compile copyFileCompiler - - -- Lectures files are copied into files/ - match "files/*" $ do - route idRoute - compile copyFileCompiler - - -- Don't do anything fancy with templates - match "templates/*" $ do - compile templateCompiler - - -- Add an explicit dependency on this file - match "website.hs" $ do - compile $ makeItem () - hsdep <- makePatternDependency "website.hs" - - -- Pages are read from the @pages@ subdirectory. - rulesExtraDependencies [hsdep] $ match "pages/*.md" $ do - route $ gsubRoute "pages/" (const "") - `composeRoutes` gsubRoute ".md" (const "") - `composeRoutes` customRoute (mkPath . toFilePath) - compile $ do - metas <- getAllMetadata "pages/*.md" - self <- takeBaseName <$> getResourceFilePath - let -- Menu items, sorted by their menuorder metadata entry. - -- Items without menuorder are sorted randomly among themselves, - -- after any entries with a specified order. - menuitems = map snd $ sortBy (\a b -> compare (fst a) (fst b)) $ - [ (menuOrder meta, Item (noIndex ident) title) - | (ident, meta) <- metas - , Just title <- [lookupString "title" meta] - ] - -- Context for the page content; contains metadata set in the - -- markdown file for each page, as well as some info set at the - -- top of this file. - ctx = mconcat - [ constField "year" (show courseYear) - , constField "syllabus" syllabusURL - , constField "group" googleGroupURL - , constField "fire" fireURL - , constField "timeedit" timeEditURL - , mconcat - [ constField ("deadline" ++ show num) date - | (date, num) <- zip labDeadlines [1..] - ] - , teacherFields "teacher" teacher - , teacherFields "assistant" assistant - , listField "menuitems" (mkMenuCtx self) - (pure menuitems) - , listField "lectures" mkLectureCtx - (mapM (makeItem . fst) (zip [0..] lectures)) - , if length newsItems > 3 - then listField "news" defaultContext (pure newsItems) - else mempty - , listField "latestnews" defaultContext (pure $ take 3 newsItems) - , defaultContext - ] - applyMeAsTemplate ctx ctx - --- | Create context fields for a teacher. -teacherFields :: String -> Teacher -> Context a -teacherFields prefix (Teacher {..}) = mconcat - [ constField prefix (head $ words teacherName) - , constField (prefix ++ "full") teacherName - , constField (prefix ++ "email") teacherEmail - , constField (prefix ++ "bio") (maybe "" id teacherBioURL) - , constField (prefix ++ "phone") teacherPhone - , maybe mempty (constField (prefix ++ "office")) teacherOffice - , maybe mempty (constField (prefix ++ "hours")) teacherHours - ] - --- | Get the menu order from a piece of metadata. -menuOrder :: Metadata -> Int -menuOrder meta = - maybe 1000000 - (round :: Double -> Int) - (readMaybe =<< lookupString "menuorder" meta) - --- | Set the CSS class of a menu item based on its identifier and the --- identifier of the current page. If the identifier matches the current --- page, the item should be displayed as selected. -menuItemClass :: String -> Item a -> String -menuItemClass current itm - | current == takeBaseName (toFilePath (itemIdentifier itm)) = - "selected" - | current == "index" && null (toFilePath (itemIdentifier itm)) = - "selected" - | otherwise = - "unselected" - -mkLectureCtx :: Context Int -mkLectureCtx = mconcat - [ field "date" $ \(Item _ n) -> do - pure $ lectureDate (lectures !! n) - , field "description" $ \(Item _ n) -> do - pure $ show $ lectureDescription (lectures !! n) - , listFieldWith "lecturefiles" defaultContext $ \(Item _ n) -> do - pure $ [ Item (fromFilePath file) name - | (name, file) <- lectureFiles $ lectures !! n] - , field "number" $ \(Item _ n) -> - pure $ show (n+1) - ] - --- | Build the context for the menu. The @submenuitems@ field will contain all --- sub-menu items for the currently active page. --- Note that URLs are relativized in a horrible, hacky way, which needs to --- be changed if the directory hierarchy of the course homepage changes. -mkMenuCtx :: String - -> Context String -mkMenuCtx self = mconcat - [ field "selected" (pure . menuItemClass self) - , listFieldWith "submenuitems" defaultContext $ \itm -> do - when (takeBaseName (toFilePath (itemIdentifier itm)) /= self) $ do - fail "not my submenu" - submenuitems <- words <$> getMetadataField' (itemIdentifier itm) "submenu" - forM (zip (mkTitles submenuitems) submenuitems) $ \(title, ident) -> do - pure $ Item (fromFilePath $ "#" ++ ident) title - , field "url" $ pure . itemToUrl - , defaultContext - ] - where - itemToUrl = ("/" ++) . takeBaseName . toFilePath . itemIdentifier - mkTitles = map capitalize . map toSpaces - - capitalize (x:xs) = toUpper x : xs - - toSpaces = map toSpace - - toSpace '_' = ' ' - toSpace c = c - --- | Render the current page as markdown, then use the current page as a --- template (i.e. it may include substitutions), and render it as markdown --- again. Finally, apply the default template to the end result and --- relativize all URLs. -applyMeAsTemplate :: Context String -> Context String -> Compiler (Item String) -applyMeAsTemplate pageCtx topCtx = do - getResourceBody - >>= renderPandoc - >>= applyAsTemplate pageCtx - >>= renderPandoc - >>= loadAndApplyTemplate "templates/default.html" topCtx - >>= relativizeUrls - --- | Turn a path @p@ (stripped of prefix and extension) into @p/index.html@. -mkPath :: FilePath -> FilePath -mkPath "index" = "index.html" -mkPath p = p ++ "/index.html" - --- | Turn an identifier into an empty identifier if the identifier is @index@, --- otherwise return the identifier unchanged. -noIndex :: Identifier -> Identifier -noIndex ident - | takeBaseName (toFilePath ident) == "index" = fromFilePath "" - | otherwise = ident +main = mkWebsite (Website info materials)