diff --git a/app/gnuplot.hs b/app/gnuplot.hs index c983a2269ea8efb7dd80523af4fa023035d0e3ec..15ea5c6aba5dd25a14c002533013a908414807cb 100644 --- a/app/gnuplot.hs +++ b/app/gnuplot.hs @@ -1,98 +1,107 @@ #!/usr/bin/env runhaskell {-# LANGUAGE OverloadedStrings #-} --- gnuplot.hs +-- gnuplot.hs module Main where -import Text.Pandoc.JSON -import System.Process -import System.IO -import System.Exit -import System.Directory +import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Char8 as B import qualified Data.ByteString.Lazy.Char8 as L import Data.Digest.Pure.MD5 import Data.List import Data.List.Split -import qualified Data.ByteString.Base64 as B64 +import System.Directory +import System.Exit +import System.IO +import System.Process +import Text.Pandoc.JSON -- Compiles an external Gnuplot file to an external PDF file. Returns the name of -- the PDF file or an error message. compileExternal :: String -> String -> IO (Either String String) - compileExternal cmd infile = do let outfile = (Prelude.head (splitOn "." infile)) ++ ".pdf" let prelude = "set terminal pdfcairo; set output '" ++ outfile ++ "';" - (exitCode, _, err) <- readProcessWithExitCode "gnuplot" ["-d", "-e", prelude, infile] "" + (exitCode, _, err) <- + readProcessWithExitCode "gnuplot" ["-d", "-e", prelude, infile] "" if exitCode == ExitSuccess - then return (Right outfile) - else return (Left err) + then return (Right outfile) + else return (Left err) genUniqueFilename :: String -> String -> String -> FilePath genUniqueFilename prefix contents extension = - prefix ++ "-" ++ (take 8 $ show $ md5 $ L.pack contents) ++ "." ++ extension + prefix ++ "-" ++ (take 8 $ show $ md5 $ L.pack contents) ++ "." ++ extension -- Compiles an embedded Gnuplot description to an external PDF file with a generated filename. -- Returns the name of the PDF file or an error message. compileInternal :: String -> String -> IO (Either String String) - compileInternal cmd contents = do let outfile = genUniqueFilename cmd contents "pdf" exists <- doesFileExist outfile if exists - then return (Right outfile) - else do + then return (Right outfile) + else do let prelude = "set terminal pdfcairo; set output '" ++ outfile ++ "';" - (exitCode, _, err) <- readProcessWithExitCode "gnuplot" ["-d"] (prelude ++ contents) + (exitCode, _, err) <- + readProcessWithExitCode "gnuplot" ["-d"] (prelude ++ contents) if exitCode == ExitSuccess - then return (Right outfile) - else return (Left err) + then return (Right outfile) + else return (Left err) -- Creates a Pandoc Image block from the filename or communicates an inline error message. generateBlock :: (Either String String) -> IO Block - generateBlock (Right filename) = do return (Para [Image nullAttr [] (filename, "Generated from code block")]) - generateBlock (Left error) = do hPutStrLn stderr msg return (Para [Str msg]) - where msg = "Error in filter 'gnuplot': " ++ error - --- Compiles gnuplot code from a code block to an image block -compileGnuplot :: Maybe Format -> Block -> IO Block + where + msg = "Error in filter 'gnuplot': " ++ error thrd (_, _, x) = x +-- Compiles gnuplot code from a code block to an image block +compileGnuplot :: Maybe Format -> Block -> IO Block compileGnuplot (Just (Format "latex")) cb@(CodeBlock attribs contents) = - case lookup "gnuplot" (thrd attribs) of - Just "" -> compileInternal "gnuplot" contents >>= generateBlock - Just infile -> compileExternal "gnuplot" infile >>= generateBlock - Nothing -> return cb - -compileGnuplot (Just (Format "revealjs")) cb@(CodeBlock (id, classes, namevals) contents) = + case lookup "gnuplot" (thrd attribs) of + Just "block" -> compileInternal "gnuplot" contents >>= generateBlock + Just infile -> compileExternal "gnuplot" infile >>= generateBlock + Nothing -> return cb +compileGnuplot (Just (Format "revealjs")) cb@(CodeBlock (id, classes, namevals) contents) -- Examine 'dot' attribute. - case lookup "dot" namevals of + = + case lookup "dot" namevals -- Empty file name means 'read from code block'. - Just "" -> do + of + Just "" -- Pipe content to dot, include result via data -- url on an image tag. Otherwise it is difficult to control -- the size if the resulting SVG element with CSS. - (exitCode, svg, err) <- readProcessWithExitCode "dot" ["-Tsvg"] contents - if exitCode == ExitSuccess - then return (Para [Image nullAttr [] (svgDataUrl svg, "Generated from code block")]) + -> do + (exitCode, svg, err) <- readProcessWithExitCode "dot" ["-Tsvg"] contents + if exitCode == ExitSuccess + then return + (Para + [ Image + nullAttr + [] + (svgDataUrl svg, "Generated from code block") + ]) else return (Para [Str $ "Error running 'dot': " ++ err]) - -- Read from file - Just file -> do - (exitCode, svg, err) <- readProcessWithExitCode "dot" ["-Tsvg", file] "" - if exitCode == ExitSuccess - then return (Para [Image nullAttr [] (svgDataUrl svg, "Generated from file " ++ file)]) + Just file -> do + (exitCode, svg, err) <- readProcessWithExitCode "dot" ["-Tsvg", file] "" + if exitCode == ExitSuccess + then return + (Para + [ Image + nullAttr + [] + (svgDataUrl svg, "Generated from file " ++ file) + ]) else return (Para [Str $ "Error running 'dot': " ++ err]) - -- Do nothing - Nothing -> return cb - + Nothing -> return cb compileGnuplot _ cb = return cb main :: IO () @@ -100,4 +109,5 @@ main = toJSONFilter compileGnuplot -- | Encode a svg snippet into a data url for an image element svgDataUrl :: String -> String -svgDataUrl svg = "data:image/svg+xml;base64," ++ (B.unpack (B64.encode (B.pack svg))) +svgDataUrl svg = + "data:image/svg+xml;base64," ++ (B.unpack (B64.encode (B.pack svg))) diff --git a/slides.cabal b/slides.cabal index e782bcd8d31c9bc3c5ca40aae2da75ef3def1283..ff7e0778fbd9be5ca5bb9617cc3a966a3d7dbc6e 100644 --- a/slides.cabal +++ b/slides.cabal @@ -15,7 +15,7 @@ cabal-version: >=1.10 library hs-source-dirs: src - exposed-modules: Watch, Test, Embed, Context, Utilities, Filter, Student, Shuffle + exposed-modules: Watch, Test, Embed, Context, Utilities, Filter, Student, Shuffle, Project build-depends: base , aeson , random @@ -52,6 +52,7 @@ library , fsnotify , vector , scientific + , transformers default-language: Haskell2010 executable decker @@ -119,22 +120,22 @@ executable examiner -- , pureMD5 -- default-language: Haskell2010 --- executable gnuplot-pandoc-filter --- hs-source-dirs: app --- main-is: Gnuplot.hs --- ghc-options: -threaded -rtsopts -with-rtsopts=-N --- build-depends: base --- , slides --- , pandoc-types --- , base64-bytestring --- , bytestring --- , process --- , blaze-markup --- , blaze-html --- , split --- , pureMD5 --- , directory --- default-language: Haskell2010 +executable gnuplot-pandoc-filter + hs-source-dirs: app + main-is: Gnuplot.hs + ghc-options: -threaded -rtsopts -with-rtsopts=-N + build-depends: base + , slides + , pandoc-types + , base64-bytestring + , bytestring + , process + , blaze-markup + , blaze-html + , split + , pureMD5 + , directory + default-language: Haskell2010 -- executable macros-pandoc-filter -- hs-source-dirs: app diff --git a/src/project.hs b/src/project.hs new file mode 100644 index 0000000000000000000000000000000000000000..a787cd4d5bd353834c226449558bd419e16486c7 --- /dev/null +++ b/src/project.hs @@ -0,0 +1,140 @@ +module Project + ( provisionResource + , removeCommonPrefix + , isPrefix + ) where + +import Control.Monad +import Control.Monad.Trans.Class +import Control.Monad.Trans.Maybe +import Data.Maybe +import Network.URI +import System.Directory +import System.FilePath + +data Provisioning + = Copy -- Copy to public and relative link + | SymbolicLink -- Link to public and relative link + | Reference -- Absolute local link + +data ResolvedUrl = ResolvedUrl + { sourceFile :: FilePath -- Absolute Path to source file + , publicFile :: FilePath -- Absolute path to file in public folder + , publicUrl :: FilePath -- Relative URL to served file from base + } + +-- Find the project directory. +-- The project directory is the first upwards directory that contains a .git directory entry. +calcProjectDirectory :: IO FilePath +calcProjectDirectory = do + cwd <- getCurrentDirectory + searchGitRoot cwd + where + searchGitRoot :: FilePath -> IO FilePath + searchGitRoot path = + if isDrive path + then makeAbsolute "." + else do + hasGit <- doesDirectoryExist (path </> ".git") + if hasGit + then makeAbsolute path + else searchGitRoot $ takeDirectory path + +-- Resolves a file path to a concrete verified file system path, or +-- returns Nothing if no file can be found. +resolve :: FilePath -> FilePath -> FilePath -> IO (Maybe FilePath) +resolve project base path = do + let pathes = + if isAbsolute path + then [project </> makeRelative "/" path, path] + else [base </> path, project </> path] + listToMaybe <$> filterM doesFileExist pathes + +resolveUrl :: FilePath -> FilePath -> FilePath -> IO (Maybe ResolvedUrl) +resolveUrl project base url = do + case parseURI url >>= fileOrRelativeUrl of + Nothing -> return Nothing + Just path -> do + file <- resolve project base path + case file of + Nothing -> return Nothing + Just file -> return $ Just $ ResolvedUrl file "" "" + +fileOrRelativeUrl :: URI -> Maybe FilePath +fileOrRelativeUrl (URI "file:" Nothing path _ _) = Just path +fileOrRelativeUrl (URI "" Nothing path _ _) = Just path +fileOrRelativeUrl _ = Nothing + +-- | Determines if a URL can be resolved to a local file. Absolute file URLs +-- are resolved against and copied or linked from +-- - the project root +-- - the local filesystem root +-- Relative file URLs are resolved against and copied or linked from +-- - the directory path of the referencing file +-- - the project root +-- Copy and link operations target the public directory in the project root +-- and recreate the source directory structure. +provisionResource :: + Provisioning -> FilePath -> FilePath -> FilePath -> IO FilePath +provisionResource provisioning project base path = do + let pathes = + if isAbsolute path + then [project </> makeRelative "/" path, path] + else [base </> path, project </> path] + resource <- listToMaybe <$> filterM doesFileExist pathes + case resource of + Nothing -> return path + Just resolved -> do + case provisioning of + Copy -> return resolved + SymbolicLink -> return resolved + Reference -> return resolved + +copyFileChanged :: FilePath -> FilePath -> IO () +copyFileChanged src dst = do + newer <- fileIsNewer src dst + if newer + then do + createDirectoryIfMissing True (takeDirectory dst) + copyFile src dst + else return () + +fileIsNewer a b = do + exists <- doesFileExist b + if exists + then do + at <- getModificationTime a + bt <- getModificationTime b + return (at > bt) + else return True + +-- | Express the second path argument as relative to the first. +-- Both arguments are expected to be absolute pathes. +makeRelativeTo :: FilePath -> FilePath -> FilePath +makeRelativeTo dir file = + let (d, f) = removeCommonPrefix (dir, file) + in normalise $ invertPath d </> f + +invertPath :: FilePath -> FilePath +invertPath fp = joinPath $ map (const "..") $ filter ("." /=) $ splitPath fp + +removeCommonPrefix :: (FilePath, FilePath) -> (FilePath, FilePath) +removeCommonPrefix = + mapTuple joinPath . removeCommonPrefix_ . mapTuple splitDirectories + where + removeCommonPrefix_ :: ([FilePath], [FilePath]) -> ([FilePath], [FilePath]) + removeCommonPrefix_ (al@(a:as), bl@(b:bs)) + | a == b = removeCommonPrefix_ (as, bs) + | otherwise = (al, bl) + removeCommonPrefix_ pathes = pathes + +isPrefix a b = isPrefix_ (splitPath a) (splitPath b) + where + isPrefix_ :: Eq a => [a] -> [a] -> Bool + isPrefix_ al@(a:as) bl@(b:bs) + | a == b = isPrefix_ as bs + | otherwise = False + isPrefix_ [] _ = True + isPrefix_ _ _ = False + +mapTuple f (a, b) = (f a, f b) diff --git a/src/utilities.hs b/src/utilities.hs index 94bae49b9f7843f4f1e6ca3265a565528d994618..b03a26b0c08d91a330c1056e40a8215c551747e3 100644 --- a/src/utilities.hs +++ b/src/utilities.hs @@ -723,6 +723,7 @@ removeCommonPrefix :: [FilePath] -> [FilePath] -> ([FilePath], [FilePath]) removeCommonPrefix al@(a:as) bl@(b:bs) | a == b = removeCommonPrefix as bs | otherwise = (al, bl) +removeCommonPrefix a [] = (a, []) removeCommonPrefix [] b = ([], b) writeExampleProject :: Action () diff --git a/test/Spec.hs b/test/Spec.hs index 0d1bc762f813db42b3c2a0d4c4d6676d1bc49c1d..187896edfc5589dce2c5cdc72666e0b53b33c00c 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -10,6 +10,7 @@ import Data.Text import Data.Text.Encoding import qualified Data.Yaml as Y import NeatInterpolation +import Project as P import Student import System.FilePath import System.FilePath.Glob @@ -35,7 +36,7 @@ main = do it "returns True if URL has http: or https: protocol" $ do isCacheableURI "http://heise.de" `shouldBe` True isCacheableURI "ftp://heise.de" `shouldBe` False - -- + -- describe "adjustLocalUrl" $ it "adjusts URL to be relative to the project root or the provided base directory" $ do @@ -60,7 +61,15 @@ main = do "/Users/henrik/tmp/decker-demo/public" "/Users/henrik/tmp/decker-demo/public/cache/b48cadafb942dc1426316772321dd0c7.png" `shouldBe` "cache/b48cadafb942dc1426316772321dd0c7.png" - -- + -- + describe "removeCommonPrefix" $ + it "Removes the common prefix from two pathes." $ do + P.removeCommonPrefix ("", "") `shouldBe` ("", "") + P.removeCommonPrefix ("fasel/bla", "fasel/bla/lall") `shouldBe` ("", "lall") + P.removeCommonPrefix ("lurgel/hopp", "fasel/bla/lall") `shouldBe` ("lurgel/hopp", "fasel/bla/lall") + P.removeCommonPrefix ("/lurgel/hopp", "fasel/bla/lall") `shouldBe` ("/lurgel/hopp", "fasel/bla/lall") + P.removeCommonPrefix ("/lurgel/hopp", "/fasel/bla/lall") `shouldBe` ("lurgel/hopp", "fasel/bla/lall") + -- describe "cacheRemoteFile" $ it "Stores the data behind a URL locally, if possible. Return the local path to the cached file." $ do @@ -106,89 +115,68 @@ main = do describe "parseStudentData" $ it "Parses student data in YAML format into a nifty data structure." $ parseStudentData projectDir `shouldReturn` Just realData - -- - describe "splitMarkdown" $ - it "Splits markdown into text und meta data." $ do - splitMarkdown "" `shouldBe` ("", Y.Object $ H.fromList []) - splitMarkdown "Hällö" `shouldBe` ("Hällö", Y.Object $ H.fromList []) - splitMarkdown "---\ntext: Hällö\n---" `shouldBe` - ("", Y.Object $ H.fromList [("text", "Hällö")]) - splitMarkdown "---\ntext: Hallo Wach\n---\nSome more text." `shouldBe` - ("Some more text.", Y.Object $ H.fromList [("text", "Hallo Wach")]) - splitMarkdown - "---\ntext: Hallo Wach\n---\nSome more text.\n---\ntoast: Cheese\n---\nSandwich" `shouldBe` - ( "Some more text.\n\nSandwich" - , Y.Object $ H.fromList [("text", "Hallo Wach"), ("toast", "Cheese")]) - -- - describe "toMeta" $ - it "Converts YAML.Value data to Pandoc.Meta data" $ do - toMeta (Y.Object $ H.fromList []) `shouldBe` MetaMap M.empty - toMeta (Y.Object $ H.fromList [("test", "Hallo")]) `shouldBe` - MetaMap (M.fromList [("test", MetaString "Hallo")]) mockData :: Students mockData = - Students $ - H.fromList - [ ( "888888" - , Student "mock" "mock" "mock" "mock" "mock" "mock" "mock" "mock" 1) - , ( "888889" - , Student "mock" "mock" "mock" "mock" "mock" "mock" "mock" "mock" 1) - ] + Students + { stds_course = "Mock" + , stds_semester = "Mock 1234" + , stds_students = + H.fromList + [ ("888888", Student "mock" "mock" "mock" "mock" "mock" "mock" 1) + , ("888889", Student "mock" "mock" "mock" "mock" "mock" "mock" 1) + ] + } realData :: Students realData = Students - (H.fromList - [ ( "836381" - , Student - { std_uid = "s64386" - , std_department = "FB6" - , std_displayName = "Justen, David Alexander" - , std_employeeNumber = "836381" - , std_givenName = "David Alexander" - , std_mail = "s64386@beuth-hochschule.de" - , std_sAMAccountName = "s64386" - , std_sn = "Justen" - , std_track = 1 - }) - , ( "798101" - , Student - { std_uid = "s53445" - , std_department = "FB6" - , std_displayName = "Mahmoud, Hassan" - , std_employeeNumber = "798101" - , std_givenName = "Hassan" - , std_mail = "s53445@beuth-hochschule.de" - , std_sAMAccountName = "s53445" - , std_sn = "Mahmoud" - , std_track = 1 - }) - , ( "814510" - , Student - { std_uid = "s57637" - , std_department = "FB6" - , std_displayName = "Sahli, Hanen" - , std_employeeNumber = "814510" - , std_givenName = "Hanen" - , std_mail = "s57637@beuth-hochschule.de" - , std_sAMAccountName = "s57637" - , std_sn = "Sahli" - , std_track = 1 - }) - , ( "832701" - , Student - { std_uid = "s61660" - , std_department = "FB6" - , std_displayName = "Naci Aydogan" - , std_employeeNumber = "832701" - , std_givenName = "Naci" - , std_mail = "s61660@beuth-hochschule.de" - , std_sAMAccountName = "s61660" - , std_sn = "Aydogan" - , std_track = 1 - }) - ]) + { stds_course = "Mock" + , stds_semester = "Mock 1234" + , stds_students = + (H.fromList + [ ( "836381" + , Student + { std_displayName = "Justen, David Alexander" + , std_employeeNumber = "836381" + , std_givenName = "David Alexander" + , std_mail = "s64386@beuth-hochschule.de" + , std_sAMAccountName = "s64386" + , std_sn = "Justen" + , std_track = 1 + }) + , ( "798101" + , Student + { std_displayName = "Mahmoud, Hassan" + , std_employeeNumber = "798101" + , std_givenName = "Hassan" + , std_mail = "s53445@beuth-hochschule.de" + , std_sAMAccountName = "s53445" + , std_sn = "Mahmoud" + , std_track = 1 + }) + , ( "814510" + , Student + { std_displayName = "Sahli, Hanen" + , std_employeeNumber = "814510" + , std_givenName = "Hanen" + , std_mail = "s57637@beuth-hochschule.de" + , std_sAMAccountName = "s57637" + , std_sn = "Sahli" + , std_track = 1 + }) + , ( "832701" + , Student + { std_displayName = "Naci Aydogan" + , std_employeeNumber = "832701" + , std_givenName = "Naci" + , std_mail = "s61660@beuth-hochschule.de" + , std_sAMAccountName = "s61660" + , std_sn = "Aydogan" + , std_track = 1 + }) + ]) + } genStudentData :: FilePath -> IO () genStudentData dir = diff --git a/test/student-mock-data.yaml b/test/student-mock-data.yaml index 038a44ba9a8751fa02e7545b04289fbcae7148dd..a882c3fd0a5ff91f4dbeeedd3ee9adef06e8df55 100644 --- a/test/student-mock-data.yaml +++ b/test/student-mock-data.yaml @@ -1,20 +1,19 @@ -888888: - department: mock - givenName: mock - track: 1 - uid: mock - sAMAccountName: mock - displayName: mock - sn: mock - employeeNumber: mock - mail: mock -888889: - department: mock - givenName: mock - track: 1 - uid: mock - sAMAccountName: mock - displayName: mock - sn: mock - employeeNumber: mock - mail: mock +semester: Mock 1234 +students: + 888888: + givenName: mock + track: 1 + sAMAccountName: mock + displayName: mock + sn: mock + employeeNumber: mock + mail: mock + 888889: + givenName: mock + track: 1 + sAMAccountName: mock + displayName: mock + sn: mock + employeeNumber: mock + mail: mock +course: Mock diff --git a/test/student-test-data.yaml b/test/student-test-data.yaml index 092b4808795d0583348b34760e10a530254799ca..ff2c11f9589ab10999aa5f939a1e97bc69bd55bf 100644 --- a/test/student-test-data.yaml +++ b/test/student-test-data.yaml @@ -1,46 +1,43 @@ { - "798101": { - "department": "FB6", - "displayName": "Mahmoud, Hassan", - "employeeNumber": "798101", - "givenName": "Hassan", - "mail": "s53445@beuth-hochschule.de", - "sAMAccountName": "s53445", - "sn": "Mahmoud", - "uid": "s53445", - "track": 1 - }, - "814510": { - "department": "FB6", - "displayName": "Sahli, Hanen", - "employeeNumber": "814510", - "givenName": "Hanen", - "mail": "s57637@beuth-hochschule.de", - "sAMAccountName": "s57637", - "sn": "Sahli", - "uid": "s57637", - "track": 1 - }, - "832701": { - "department": "FB6", - "displayName": "Naci Aydogan", - "employeeNumber": "832701", - "givenName": "Naci", - "mail": "s61660@beuth-hochschule.de", - "sAMAccountName": "s61660", - "sn": "Aydogan", - "uid": "s61660", - "track": 1 - }, - "836381": { - "department": "FB6", - "displayName": "Justen, David Alexander", - "employeeNumber": "836381", - "givenName": "David Alexander", - "mail": "s64386@beuth-hochschule.de", - "sAMAccountName": "s64386", - "sn": "Justen", - "uid": "s64386", - "track": 1 + "course": "Mock", + "semester": "Mock 1234", + "students": + { + "798101": { + "displayName": "Mahmoud, Hassan", + "employeeNumber": "798101", + "givenName": "Hassan", + "mail": "s53445@beuth-hochschule.de", + "sAMAccountName": "s53445", + "sn": "Mahmoud", + "track": 1 + }, + "814510": { + "displayName": "Sahli, Hanen", + "employeeNumber": "814510", + "givenName": "Hanen", + "mail": "s57637@beuth-hochschule.de", + "sAMAccountName": "s57637", + "sn": "Sahli", + "track": 1 + }, + "832701": { + "displayName": "Naci Aydogan", + "employeeNumber": "832701", + "givenName": "Naci", + "mail": "s61660@beuth-hochschule.de", + "sAMAccountName": "s61660", + "sn": "Aydogan", + "track": 1 + }, + "836381": { + "displayName": "Justen, David Alexander", + "employeeNumber": "836381", + "givenName": "David Alexander", + "mail": "s64386@beuth-hochschule.de", + "sAMAccountName": "s64386", + "sn": "Justen", + "track": 1 + } } -} +} \ No newline at end of file