packages feed

nbparts-0.1.0.0: src/Nbparts/Unpack/Sources/Markdown.hs

module Nbparts.Unpack.Sources.Markdown where

import Control.Arrow qualified as Arrow
import Data.Aeson qualified as Aeson
import Data.ByteString.Lazy qualified as LazyByteString
import Data.Function ((&))
import Data.Map qualified as Map
import Data.Maybe qualified as Maybe
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Text.Encoding qualified as Text
import Nbparts.Types
  ( CellMarker (CellMarker),
    CellSource (CellSource),
    CellType (Code, Markdown, Raw),
    UnembeddedMimeAttachments (UnembeddedMimeAttachments),
    UnembeddedMimeBundle (UnembeddedMimeBundle),
    UnembeddedMimeData (BinaryData),
    UnpackError (UnpackParseMarkdownError),
  )
import Nbparts.Util.Map qualified as MapUtil
import Nbparts.Util.Markdown qualified as MarkdownUtil
import Nbparts.Util.Text qualified as TextUtil

sourcesToMarkdown :: Text -> [CellSource] -> Either UnpackError Text
sourcesToMarkdown lang sources = do
  texts <- traverse (sourceToMarkdown lang) sources
  pure $ Text.concat (map (<> "\n\n") texts)

sourceToMarkdown :: Text -> CellSource -> Either UnpackError Text
sourceToMarkdown _ (CellSource cellId cellType@Markdown source maybeAttachments) = do
  -- NOTE: Remember that the elements in `source` have trailing newlines.
  let mdText = Text.concat source
  let mdLines = Text.lines mdText

  mdAst <- MarkdownUtil.parseMarkdown mdText & Arrow.left UnpackParseMarkdownError

  let escapesReplacements = MarkdownUtil.commentChangesWith escapeComments mdLines mdAst
  let attachmentReplacements = case maybeAttachments of
        Just attachments ->
          MarkdownUtil.attachmentChangesWith
            (fmap Text.pack . lookupAttachmentFilePath attachments)
            mdLines
            mdAst
        Nothing -> []
  let textReplacements = escapesReplacements <> attachmentReplacements

  -- Safety: The replacements do not overlap.
  let fixedMdText = Maybe.fromJust $ TextUtil.replaceSlices mdText textReplacements

  pure $ mkCellMarkerComment (CellMarker cellId cellType maybeAttachments) <> "\n" <> fixedMdText
sourceToMarkdown _ (CellSource cellId cellType@Raw source _) =
  pure $
    Text.intercalate
      "\n"
      [ mkCellMarkerComment (CellMarker cellId cellType Nothing),
        "```",
        Text.concat source,
        "```"
      ]
sourceToMarkdown lang (CellSource cellId cellType@Code source _) =
  pure $
    Text.intercalate
      "\n"
      [ mkCellMarkerComment (CellMarker cellId cellType Nothing),
        "```" <> lang,
        Text.concat source,
        "```"
      ]

mkCellMarkerComment :: CellMarker -> Text
mkCellMarkerComment cm =
  Text.intercalate
    " "
    [ "<!--",
      "nbparts:cell",
      escapeCellMarkerContent $ Text.decodeUtf8 $ LazyByteString.toStrict $ Aeson.encode cm,
      "-->"
    ]

escapeCellMarkerContent :: Text -> Text
escapeCellMarkerContent = Text.replace "-->" "-\\->" . Text.replace "\\" "\\\\"

escapeComments :: Text -> Text
escapeComments = Text.replace "nbparts:cell" "\\nbparts:cell" . Text.replace "\\" "\\\\"

lookupAttachmentFilePath :: UnembeddedMimeAttachments -> Text -> Maybe FilePath
lookupAttachmentFilePath (UnembeddedMimeAttachments attachments) target = do
  attachmentName <- Text.stripPrefix "attachment:" target

  -- TODO: Should warn if the attachment can't be found.
  (UnembeddedMimeBundle mimeBundle) <- Map.lookup attachmentName attachments

  -- The mime bundle should only have 1 entry, but just in case it doesn't,
  -- we find the first entry whose mime type starts with "image".
  mimedata <- MapUtil.lookupByKeyPrefix "image" mimeBundle
  case mimedata of
    BinaryData fp -> Just fp
    _ -> Nothing