diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b5a03b..dd5f270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ user in the changelog. ## [Unreleased] +### Added + +* Markdown link footnotes and images are now transformed into the appropriate kind of + gopher menu item type if the article is being rendered as a menu + ## [0.27.0.0] - 2024-12-20 ### Added diff --git a/bore.cabal b/bore.cabal index 52fa05a..93d0285 100644 --- a/bore.cabal +++ b/bore.cabal @@ -5,7 +5,7 @@ cabal-version: 2.2 -- see: https://github.com/sol/hpack name: bore -version: 0.26.0.0 +version: 0.27.0.0 synopsis: Build gopherholes. description: Static site builder, but for gopherholes. Manage phlogs with tags, use the Markdown renderer and Mustache templating system. category: Network @@ -82,6 +82,7 @@ library , hashmap , hourglass , liquidhaskell + , mime-types , mtl , mustache , neat-interpolation @@ -145,6 +146,7 @@ executable bore , hashmap , hourglass , liquidhaskell + , mime-types , mtl , mustache , neat-interpolation @@ -206,6 +208,7 @@ test-suite bore-test , hashmap , hourglass , liquidhaskell + , mime-types , mtl , mustache , neat-interpolation diff --git a/package.yaml b/package.yaml index 7db90e7..3542388 100644 --- a/package.yaml +++ b/package.yaml @@ -71,6 +71,7 @@ dependencies: - spacecookie - edit-distance - safe + - mime-types library: source-dirs: src diff --git a/src/Bore/Text/Gophermap.hs b/src/Bore/Text/Gophermap.hs index bedd6b6..5824579 100644 --- a/src/Bore/Text/Gophermap.hs +++ b/src/Bore/Text/Gophermap.hs @@ -16,10 +16,48 @@ specification) -} -module Bore.Text.Gophermap (toGophermap) where +module Bore.Text.Gophermap (toGophermap, imagePathToGopherType, gopherTypeByExt) where import Data.Text (Text) import qualified Data.Text as T +import System.FilePath (takeExtension) +import Network.Mime (defaultMimeMap, mimeByExt) +import qualified Data.ByteString.Char8 as BS +import Data.List (isPrefixOf) + +{- | Return a guess for the gopher item type based on the file extension of the +path/selector passed (assumed to be an image). + +The default is just 'I' for image file. Basically, if it's not a GIF. + +Could be expanded to support non-canonical 'p' (png) type. +-} +imagePathToGopherType + :: FilePath + -- ^ Selector for an image which includes the file extension. + -> Char + -- ^ Gopher item type. +imagePathToGopherType selector = + case takeExtension selector of + ".gif" -> 'g' + _ -> 'I' + +-- could be expanded to handle different things +-- Map MIME types to Gopher menu item types +mimeToGopherType :: BS.ByteString -> Char +mimeToGopherType mime + | "text/" `BS.isPrefixOf` mime = '0' -- Plain text + | "image/" `BS.isPrefixOf` mime = 'I' -- Image + | "audio/" `BS.isPrefixOf` mime = 's' -- Sound + | otherwise = '9' -- Binary or other + +-- fixme: what about http/h +-- Map file extensions to Gopher menu item types +gopherTypeByExt :: BS.ByteString -> FilePath -> Char +gopherTypeByExt defaultType filePath + | "http://" `isPrefixOf` filePath || "https://" `isPrefixOf` filePath = 'h' + | otherwise = + mimeToGopherType $ mimeByExt defaultMimeMap defaultType (T.pack $ "." ++ takeExtension filePath) {- | Transform a regular text file into a Gopher menu. @@ -41,9 +79,10 @@ toGophermap domain port inputText = (T.unlines . map processLine . T.lines $ inp isMenuEntry :: Text -> Bool isMenuEntry t = not (T.null t) && T.head t `elem` gopherFileTypes && "\t" `T.isInfixOf` t - -- Gopher file types as per RFC1436 and spacecookie. + -- Gopher file types as per RFC1436 and spacecookie. Also include 'h' for HTTP. May + -- have to be expanded in future. Potential for complications. gopherFileTypes :: [Char] - gopherFileTypes = "0123456789+gITi" + gopherFileTypes = "0123456789+gITih" -- Add domain and port if they're missing from the line. addDomainAndPort :: Text -> Text diff --git a/src/Bore/Text/Template/Replacements.hs b/src/Bore/Text/Template/Replacements.hs index e75eea0..6cefbe7 100644 --- a/src/Bore/Text/Template/Replacements.hs +++ b/src/Bore/Text/Template/Replacements.hs @@ -103,12 +103,13 @@ initialSubstitutions library maybeFrontMatter = let -- FIXME: why is there this doubling-up on the effort of translating the frontmatter, seemingly? just because it can't handle lists? postFm = substitutionsFromFrontMatter maybeFrontMatter (toSubstitutions . toMustache <$> maybeFrontMatter) + gophermap = fromMaybe False (isGophermap <$> maybeFrontMatter) in postFm ++ [ ("containerize", overText (lambdaContainerize library.containers)), ("figlet", overText (lambdaFiglet library.fonts)), - ("wrapVt320WideMode", overText (wrapMarkdown 132)), - ("wrapVt320StandardMode", overText (wrapMarkdown 80)) + ("wrapVt320WideMode", overText (wrapMarkdown library gophermap 132)), + ("wrapVt320StandardMode", overText (wrapMarkdown library gophermap 80)) ] where -- FIXME; if i want to add tagging support i'd add it here for hardcoding diff --git a/src/Bore/Text/Wrap.hs b/src/Bore/Text/Wrap.hs index 7bf58b1..f3ee3e2 100644 --- a/src/Bore/Text/Wrap.hs +++ b/src/Bore/Text/Wrap.hs @@ -18,10 +18,20 @@ import Data.Text (Text) import qualified Data.Text as T import Text.Wrap (wrapText, defaultWrapSettings) import Control.Monad.State (State, get, modify, runState, gets) +import Data.Maybe (fromMaybe) + +import qualified Bore.Library as Library +import Bore.Text.Gophermap (imagePathToGopherType, gopherTypeByExt) +import Bore.Config -- Configuration for the renderer data WrapConfig = WrapConfig - { lineWidth :: Int -- Configurable line width + { library :: Library.Library + -- ^ Assets and config(s) to use for rendering. + , gopherMap :: Bool + -- ^ Render as a gophermap? Meaning menu links. + , lineWidth :: Int + -- ^ Configurable line width } -- State to hold configuration and footnote data @@ -33,10 +43,9 @@ data RendererState = RendererState newtype WrapRenderer = WrapRenderer { getWrappedText :: State RendererState Text } +-- FIXME: what the heck is going on here? instance Show WrapRenderer where - show (WrapRenderer t) = - let initialState = RendererState (WrapConfig 80) [] 1 - in show (fst $ runState t initialState) + show _ = "Do not use!" instance Rangeable WrapRenderer where ranged _ (WrapRenderer t) = WrapRenderer t @@ -130,18 +139,30 @@ instance IsInline WrapRenderer where let footnoteRef = "[" <> T.pack (show fnNumber) <> "]" return $ content <> footnoteRef - image _ _ (WrapRenderer t) = WrapRenderer t + -- FIXME + image url _ (WrapRenderer t) = WrapRenderer $ do + altText <- t + let + imageGopherType = imagePathToGopherType (T.unpack url) + isGopherMap <- gets (gopherMap . config) + port <- gets (T.pack . show . fromMaybe 70 . listenPort . server . Library.config . library . config) + host <- gets (hostname . server . Library.config . library . config) + if isGopherMap + then return $ (T.pack [imageGopherType]) <> altText <> "\t" <> url <> "\t" <> host <> "\t" <> port + else return $ "!" <> altText <> " => " <> url + escapedChar c = WrapRenderer $ return $ T.singleton c entity t = WrapRenderer $ return t rawInline _ rawContent = WrapRenderer $ return rawContent +-- TODO/FIXME: add option for parsing as menu (for footnotes and images) -- Parse and Render Markdown -wrapMarkdownParagraphs :: Int -> Text -> Either String Text -wrapMarkdownParagraphs width input = +wrapMarkdownParagraphs :: Library.Library -> Bool -> Int -> Text -> Either String Text +wrapMarkdownParagraphs library gopherMap width input = case commonmark "source" input of Left err -> Left $ show err Right (WrapRenderer result) -> - let initialState = RendererState (WrapConfig width) [] 1 + let initialState = RendererState (WrapConfig library gopherMap width) [] 1 (mainText, finalState) = runState result initialState footnotesList = footnotes finalState numberedFootnotes = zip [1 :: Int ..] footnotesList @@ -150,10 +171,33 @@ wrapMarkdownParagraphs width input = else "## Footnotes\n\n" <> T.intercalate "\n" (map formatFootnote numberedFootnotes) in Right $ mainText <> formattedFootnotes where - formatFootnote (n, (text, url)) = "[" <> T.pack (show n) <> "]: " <> text <> ": " <> url + -- FIXME: make into links if gopherMap! need way to determine type of gopherlink in the end. + formatFootnote :: (Int, (Text, Text)) -> Text + formatFootnote (n, (text, url)) = do + let + host = library.config.server.hostname + port = T.pack . show . fromMaybe 70 $ library.config.server.listenPort + if gopherMap + then + case gopherTypeByExt "text/" $ T.unpack url of + 'h' -> "h" <> "[" <> T.pack (show n) <> "]: " <> text <> ": " <> url <> "\tURL:" <> url <> "\t" <> host <> "\t" <> port + otherType -> (T.pack [otherType]) <> "[" <> T.pack (show n) <> "]: " <> text <> ": " <> url <> "\t" <> url <> "\t" <> host <> "\t" <> port + else "[" <> T.pack (show n) <> "]: " <> text <> ": " <> url + +{- | Do special things to Markdown for gopherspace. + +I mostly made this for phlog articles. -wrapMarkdown :: Int -> Text -> Text -wrapMarkdown width input = - case wrapMarkdownParagraphs width input of +-} +wrapMarkdown + :: Library.Library + -- ^ Format the markdown document according to these Assets and config! + -> Bool + -- ^ Output a gophermap isntead of just a regular text document. + -> Int + -> Text + -> Text +wrapMarkdown library gopherMap width input = + case wrapMarkdownParagraphs library gopherMap width input of Left err -> "Error: " <> T.pack err Right output -> output