/ src / Automerge.hs
Automerge.hs
  1  {-# LANGUAGE InstanceSigs #-}
  2  {-# LANGUAGE OverloadedStrings #-}
  3  
  4  module Automerge (parseAutomergeSpans, parseAutomergeSpansText, Span (..), BlockMarker (..), Heading (..), HeadingLevel (..), NoteId (..), BlockSpan (..), BlockType (..), TextSpan (..), Mark (..), Link (..), toJSONText, takeUntilNonEmbedBlockSpan, takeUntilNextSameBlockTypeSibling, isTopLevelBlock, isParent, isSiblingListItem) where
  5  
  6  import Data.Aeson (FromJSON (parseJSON), Object, ToJSON (toJSON), Value (Bool, Null, String), eitherDecode, eitherDecodeStrictText, encode, object, withObject, withScientific, withText, (.!=), (.:), (.:?), (.=))
  7  import qualified Data.Aeson.Key as K
  8  import qualified Data.Aeson.KeyMap as KM
  9  import Data.Aeson.Types (Parser)
 10  import qualified Data.ByteString.Lazy as BL
 11  import qualified Data.ByteString.Lazy.Char8 as BSL8
 12  import Data.List (unsnoc)
 13  import qualified Data.Text as T
 14  import Data.Text.Encoding (decodeUtf8)
 15  import Utils.JSON (parseNonEmpty, parseStringifiedObject, stringifyObject)
 16  
 17  -- TODO: Make title optional
 18  data Link = Link {url :: T.Text, title :: T.Text} deriving (Show, Eq)
 19  
 20  instance FromJSON Link where
 21    parseJSON = withObject "Link" $ \v -> do
 22      linkUrl <- v .: "href" >>= parseNonEmpty "href"
 23      linkTitle <- v .: "title"
 24      pure Link {url = linkUrl, title = linkTitle}
 25  
 26  instance ToJSON Link where
 27    toJSON link = object ["href" .= url link, "title" .= title link]
 28  
 29  data Mark
 30    = Strong
 31    | Emphasis
 32    | LinkMark Link
 33    | Code
 34    deriving (Show, Eq)
 35  
 36  newtype HeadingLevel = HeadingLevel Int deriving (Show, Eq)
 37  
 38  instance FromJSON HeadingLevel where
 39    parseJSON = withScientific "HeadingLevel" $ \n -> do
 40      let level = floor n
 41      if level >= 1 && level <= 6
 42        then pure $ HeadingLevel level
 43        else fail "Invalid heading level"
 44  
 45  newtype Heading = Heading HeadingLevel deriving (Show, Eq)
 46  
 47  newtype NoteId = NoteId T.Text deriving (Show, Eq)
 48  
 49  instance FromJSON NoteId where
 50    parseJSON = withText "NoteId" $ \t -> do
 51      noteId <- parseNonEmpty "id" t
 52      pure $ NoteId noteId
 53  
 54  data BlockType
 55    = ParagraphType
 56    | HeadingType
 57    | CodeBlockType
 58    | BlockQuoteType
 59    | OrderedListItemType
 60    | UnorderedListItemType
 61    | ImageType
 62    | NoteRefType
 63    | NoteContentType
 64    deriving (Show, Eq)
 65  
 66  instance FromJSON BlockType where
 67    parseJSON :: Value -> Parser BlockType
 68    parseJSON = withText "BlockType" $ \t -> case t of
 69      "paragraph" -> pure ParagraphType
 70      "heading" -> pure HeadingType
 71      "code-block" -> pure CodeBlockType
 72      "blockquote" -> pure BlockQuoteType
 73      "ordered-list-item" -> pure OrderedListItemType
 74      "unordered-list-item" -> pure UnorderedListItemType
 75      "image" -> pure ImageType
 76      "__ext__note_ref" -> pure NoteRefType
 77      "__ext__note_content" -> pure NoteContentType
 78      _ -> fail "Invalid block type"
 79  
 80  instance ToJSON BlockType where
 81    toJSON :: BlockType -> Value
 82    toJSON bt = case bt of
 83      ParagraphType -> String "paragraph"
 84      HeadingType -> String "heading"
 85      CodeBlockType -> String "code-block"
 86      BlockQuoteType -> String "blockquote"
 87      OrderedListItemType -> String "ordered-list-item"
 88      UnorderedListItemType -> String "unordered-list-item"
 89      ImageType -> String "image"
 90      NoteRefType -> String "__ext__note_ref"
 91      NoteContentType -> String "__ext__note_content"
 92  
 93  data BlockMarker
 94    = ParagraphMarker
 95    | HeadingMarker Heading
 96    | CodeBlockMarker
 97    | BlockQuoteMarker
 98    | OrderedListItemMarker
 99    | UnorderedListItemMarker
100    | ImageBlockMarker
101    | NoteRefMarker NoteId
102    | NoteContentMarker NoteId
103    deriving (Show, Eq)
104  
105  data TextSpan = AutomergeText {value :: T.Text, marks :: [Mark]} deriving (Show, Eq)
106  
107  instance Semigroup TextSpan where
108    (<>) (AutomergeText value1 marks1) (AutomergeText value2 marks2) = AutomergeText (value1 <> value2) (marks1 <> marks2)
109  
110  instance Monoid TextSpan where
111    mempty = AutomergeText T.empty []
112  
113  data BlockSpan = AutomergeBlock {blockMarker :: BlockMarker, parentTypes :: [BlockType], isEmbed :: Bool} deriving (Show, Eq)
114  
115  blockType :: BlockSpan -> BlockType
116  blockType (AutomergeBlock (ParagraphMarker) _ _) = ParagraphType
117  blockType (AutomergeBlock (HeadingMarker _) _ _) = HeadingType
118  blockType (AutomergeBlock (CodeBlockMarker) _ _) = CodeBlockType
119  blockType (AutomergeBlock (BlockQuoteMarker) _ _) = BlockQuoteType
120  blockType (AutomergeBlock (OrderedListItemMarker) _ _) = OrderedListItemType
121  blockType (AutomergeBlock (UnorderedListItemMarker) _ _) = UnorderedListItemType
122  blockType (AutomergeBlock (ImageBlockMarker) _ _) = ImageType
123  blockType (AutomergeBlock (NoteRefMarker _) _ _) = NoteRefType
124  blockType (AutomergeBlock (NoteContentMarker _) _ _) = NoteContentType
125  
126  data Span
127    = BlockSpan BlockSpan
128    | TextSpan TextSpan
129    deriving (Show, Eq)
130  
131  instance FromJSON Span where
132    parseJSON = withObject "AutomergeSpan" $ \v -> do
133      elementType <- (v .: "type" :: Parser String)
134      case elementType of
135        "block" -> parseBlock v
136        "text" -> parseInline v
137        _ -> fail "Unknown span type"
138  
139  parseBlock :: Object -> Parser Span
140  parseBlock v = do
141    blockData <- v .: "value"
142    bt <- (blockData .: "type" :: Parser BlockType)
143    parents <- (blockData .: "parents" :: Parser [BlockType])
144    embed <- (blockData .: "isEmbed" :: Parser Bool)
145    case bt of
146      ParagraphType -> pure $ BlockSpan $ AutomergeBlock ParagraphMarker parents embed
147      HeadingType -> do
148        attrs <- blockData .: "attrs"
149        level <- attrs .: "level"
150        pure $ BlockSpan $ AutomergeBlock (HeadingMarker $ Heading $ HeadingLevel level) parents embed
151      CodeBlockType -> pure $ BlockSpan $ AutomergeBlock CodeBlockMarker parents embed
152      BlockQuoteType -> pure $ BlockSpan $ AutomergeBlock BlockQuoteMarker parents embed
153      OrderedListItemType -> pure $ BlockSpan $ AutomergeBlock OrderedListItemMarker parents embed
154      UnorderedListItemType -> pure $ BlockSpan $ AutomergeBlock UnorderedListItemMarker parents embed
155      ImageType -> pure $ BlockSpan $ AutomergeBlock ImageBlockMarker parents embed
156      NoteRefType -> do
157        attrs <- blockData .: "attrs"
158        noteId <- attrs .: "id"
159        pure $ BlockSpan $ AutomergeBlock (NoteRefMarker noteId) parents embed
160      NoteContentType -> do
161        attrs <- blockData .: "attrs"
162        noteId <- attrs .: "id"
163        pure $ BlockSpan $ AutomergeBlock (NoteContentMarker noteId) parents embed
164  
165  parseInline :: Object -> Parser Span
166  parseInline v = do
167    parsedValue <- v .: "value"
168    marksKeyMap <- v .:? "marks" .!= KM.empty
169    parsedMarks <- parseMarks marksKeyMap
170    pure $ TextSpan $ AutomergeText parsedValue parsedMarks
171  
172  parseMarks :: KM.KeyMap Value -> Parser [Mark]
173  parseMarks = mapM parseMark . filterNonNull . KM.toList
174    where
175      filterNonNull = filter (\(_, v) -> v /= Null)
176  
177  parseMark :: (K.Key, Value) -> Parser Mark
178  parseMark (k, String txt)
179    | K.toText k == "link" = parseStringifiedObject txt >>= (pure . LinkMark)
180  parseMark (k, Bool True) = case K.toText k of
181    "strong" -> pure Strong
182    "em" -> pure Emphasis
183    "code" -> pure Code
184    _ -> fail $ "Unexpected mark with boolean value: " ++ T.unpack (K.toText k)
185  parseMark _ = fail "Invalid format in marks"
186  
187  parseAutomergeSpans :: BL.ByteString -> Either String [Span]
188  parseAutomergeSpans = eitherDecode
189  
190  parseAutomergeSpansText :: T.Text -> Either String [Span]
191  parseAutomergeSpansText = eitherDecodeStrictText
192  
193  instance ToJSON Span where
194    toJSON (BlockSpan (AutomergeBlock marker parents embed)) = case marker of
195      ParagraphMarker ->
196        object
197          [ "type" .= T.pack "block",
198            "value"
199              .= object
200                [ "isEmbed" .= embed,
201                  "parents" .= parents,
202                  "type" .= T.pack "paragraph",
203                  "attrs" .= (KM.empty :: KM.KeyMap T.Text)
204                ]
205          ]
206      HeadingMarker (Heading (HeadingLevel level)) ->
207        object
208          [ "type" .= T.pack "block",
209            "value"
210              .= object
211                [ "isEmbed" .= embed,
212                  "parents" .= parents,
213                  "type" .= T.pack "heading",
214                  "attrs" .= object ["level" .= level]
215                ]
216          ]
217      CodeBlockMarker ->
218        object
219          [ "type" .= T.pack "block",
220            "value"
221              .= object
222                [ "isEmbed" .= embed,
223                  "parents" .= parents,
224                  "type" .= T.pack "code-block",
225                  "attrs" .= (KM.empty :: KM.KeyMap T.Text)
226                ]
227          ]
228      BlockQuoteMarker ->
229        object
230          [ "type" .= T.pack "block",
231            "value"
232              .= object
233                [ "isEmbed" .= embed,
234                  "parents" .= ([] :: [T.Text]),
235                  "type" .= T.pack "blockquote",
236                  "attrs" .= (KM.empty :: KM.KeyMap T.Text)
237                ]
238          ]
239      OrderedListItemMarker ->
240        object
241          [ "type" .= T.pack "block",
242            "value"
243              .= object
244                [ "isEmbed" .= embed,
245                  "parents" .= parents,
246                  "type" .= T.pack "ordered-list-item",
247                  "attrs" .= (KM.empty :: KM.KeyMap T.Text)
248                ]
249          ]
250      UnorderedListItemMarker ->
251        object
252          [ "type" .= T.pack "block",
253            "value"
254              .= object
255                [ "isEmbed" .= embed,
256                  "parents" .= parents,
257                  "type" .= T.pack "unordered-list-item",
258                  "attrs" .= (KM.empty :: KM.KeyMap T.Text)
259                ]
260          ]
261      ImageBlockMarker ->
262        object
263          [ "type" .= T.pack "block",
264            "value"
265              .= object
266                [ "isEmbed" .= embed,
267                  "parents" .= parents,
268                  "type" .= T.pack "image",
269                  "attrs" .= (KM.empty :: KM.KeyMap T.Text)
270                ]
271          ]
272      NoteRefMarker (NoteId noteId) ->
273        object
274          [ "type" .= T.pack "block",
275            "value"
276              .= object
277                [ "isEmbed" .= embed,
278                  "parents" .= parents,
279                  "type" .= T.pack "__ext__note_ref",
280                  "attrs" .= object ["id" .= noteId]
281                ]
282          ]
283      NoteContentMarker (NoteId noteId) ->
284        object
285          [ "type" .= T.pack "block",
286            "value"
287              .= object
288                [ "isEmbed" .= embed,
289                  "parents" .= parents,
290                  "type" .= T.pack "__ext__note_content",
291                  "attrs" .= object ["id" .= noteId]
292                ]
293          ]
294    toJSON (TextSpan (AutomergeText val extractedMarks)) =
295      object $
296        [ "type" .= T.pack "text",
297          "value" .= val
298        ]
299          <> ["marks" .= KM.fromList (map markToKeyVal extractedMarks) | not (null extractedMarks)]
300      where
301        markToKeyVal mark = case mark of
302          Strong -> (K.fromText "strong", Bool True)
303          Emphasis -> (K.fromText "em", Bool True)
304          LinkMark link -> (K.fromText "link", String $ stringifyObject link)
305          Code -> (K.fromText "code", Bool True)
306  
307  toJSONText :: [Span] -> T.Text
308  toJSONText = decodeUtf8 . BSL8.toStrict . encode
309  
310  takeUntilNonEmbedBlockSpan :: [Span] -> [Span]
311  takeUntilNonEmbedBlockSpan [] = []
312  takeUntilNonEmbedBlockSpan (x : xs) = case x of
313    BlockSpan (AutomergeBlock _ _ False) -> []
314    _ -> x : takeUntilNonEmbedBlockSpan xs
315  
316  takeUntilNextSameBlockTypeSibling :: BlockSpan -> [Span] -> [Span]
317  takeUntilNextSameBlockTypeSibling _ [] = []
318  takeUntilNextSameBlockTypeSibling bl (x : xs) = case x of
319    BlockSpan blockSpan | (isSibling blockSpan bl && blockType blockSpan == blockType bl) -> []
320    _ -> x : takeUntilNextSameBlockTypeSibling bl xs
321  
322  isTopLevelBlock :: BlockSpan -> Bool
323  isTopLevelBlock (AutomergeBlock _ parents _) = null parents
324  
325  isParent :: Maybe BlockSpan -> BlockSpan -> Bool
326  isParent (Just parentBlock) potentialChildBlock = lastParentMatches parentBlockType potentialChildParents && isProperPrefix parentParents potentialChildParents
327    where
328      parentBlockType = blockType parentBlock
329      parentParents = parentTypes parentBlock
330      potentialChildParents = parentTypes potentialChildBlock
331  isParent Nothing blockSpan = isTopLevelBlock blockSpan
332  
333  isSibling :: BlockSpan -> BlockSpan -> Bool
334  isSibling (AutomergeBlock _ block1Parents _) (AutomergeBlock _ block2Parents _) = block1Parents == block2Parents
335  
336  lastParentMatches :: BlockType -> [BlockType] -> Bool
337  lastParentMatches parentBlockType potentialChildParents = case unsnoc potentialChildParents of
338    Nothing -> False
339    Just (_, lastParentOfCandidate) -> parentBlockType == lastParentOfCandidate
340  
341  isSiblingListItem :: BlockSpan -> BlockSpan -> Bool
342  isSiblingListItem (AutomergeBlock UnorderedListItemMarker listItem1Parents _) (AutomergeBlock UnorderedListItemMarker listItem2Parents _) = listItem1Parents == listItem2Parents
343  isSiblingListItem (AutomergeBlock OrderedListItemMarker listItem1Parents _) (AutomergeBlock OrderedListItemMarker listItem2Parents _) = listItem1Parents == listItem2Parents
344  isSiblingListItem (AutomergeBlock _ _ _) (AutomergeBlock _ _ _) = False
345  
346  isProperPrefix :: [BlockType] -> [BlockType] -> Bool
347  isProperPrefix _ [] = False
348  isProperPrefix parents potentialChildParents = parents == (init potentialChildParents)