packages feed

hstratus-notes 0.1.0.2 → 0.1.0.3

raw patch · 6 files changed

+359/−150 lines, 6 filesdep +cerealdep +protobufdep −proto3-wirePVP ok

version bump matches the API change (PVP)

Dependencies added: cereal, protobuf

Dependencies removed: proto3-wire

API changes (from Hackage documentation)

Files

ChangeLog.md view
@@ -2,6 +2,10 @@  `icloud-notes` uses [PVP Versioning][1]. +## 0.1.0.3 -- 2026-07-30++* Add `protobuf` and `cereal` dependencies; replace `proto3-wire` for protobuf decoding.+ ## 0.1.0.2 -- 2026-07-30  * Remove unused `proto3-suite` dependency.
hstratus-notes.cabal view
@@ -1,6 +1,6 @@ cabal-version:      3.0 name:               hstratus-notes-version:            0.1.0.2+version:            0.1.0.3 synopsis:           Access iCloud Notes description:   Browse notes and folders from iCloud Notes using an authenticated session@@ -71,7 +71,8 @@     , http-client          >=0.5 && <0.8     , http-types           >=0.12.1 && <0.13     , hstratus-auth        >=0.1 && <0.3-    , proto3-wire          >=1.0 && <2+    , cereal               >=0.5 && <0.6+    , protobuf             >=0.2 && <0.3     , text                 >=1.2.3 && <2.2     , time                 >=1.9 && <1.15     , zlib                 >=0.6 && <0.8@@ -106,7 +107,8 @@     , hstratus-auth                >=0.1 && <0.3     , hstratus-notes     , hstratus-notes:hstratus-notes-internal-    , proto3-wire                  >=1.0 && <2+    , cereal                       >=0.5 && <0.6+    , protobuf                     >=0.2 && <0.3     , QuickCheck                   >=2.14 && <3     , temporary                    >=1.2 && <1.4     , text                         >=1.2.3 && <2.2
src-internal/Network/HStratus/Internal/Notes/Proto.hs view
@@ -1,46 +1,186 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+ {- | Module      : Network.HStratus.Internal.Notes.Proto Copyright   : (c) 2026 Tim Emiola Maintainer  : Tim Emiola <adetokunbo@emio.la> SPDX-License-Identifier: BSD-3-Clause -Hand-written proto3-wire decoders for the Apple Notes protobuf schema.+Protobuf decoders for the Apple Notes wire schema. -Uses proto3-wire's lower-level @Parser@ API rather than code generation-because the schema has non-consecutive field numbers (e.g. @note_text = 2@,-@attribute_run = 5@); explicit @\`at\` N@ bindings prevent the silent-field-number mismatches that Generic derivation would introduce.-Fields not listed in the decoders are silently ignored by the wire decoder.+Uses 'Data.ProtocolBuffers' with Generic-derived 'Decode' instances for the+wire layer ('Wire*' types), then converts to plain domain types for use in+"Network.HStratus.Internal.Notes.Decode". Fields absent in the wire bytes+default to zero, empty, or 'Nothing', matching proto3 semantics.++The 'Wire*' types are exported so that the test-suite encoder+('HStratus.Notes.TestHelper') can construct fixture bytes via 'Encode'+instances without depending on @proto3-wire@. -} module Network.HStratus.Internal.Notes.Proto-  ( ProtoNote (..)+  ( -- * Decoded note types+    ProtoNote (..)   , ProtoAttributeRun (..)   , ProtoParagraphStyle (..)   , decodeNoteStoreProto++    -- * Wire types+  , WireNoteStoreProto (..)+  , WireDocument (..)+  , WireNote (..)+  , WireAttributeRun (..)+  , WireParagraphStyle (..)+  , WireChecklist (..)+  , WireAttachmentInfo (..)   ) where  import Data.ByteString (ByteString) import Data.Int (Int32)+import Data.Maybe (fromMaybe)+import Data.ProtocolBuffers+  ( Decode+  , Encode+  , Message+  , Optional+  , Repeated+  , Value+  , decodeMessage+  , getField+  )+import Data.Serialize (runGet) import Data.Text (Text) import qualified Data.Text.Lazy as LT-import Proto3.Wire.Decode-  ( Parser-  , RawMessage-  , at-  , embedded-  , embedded'-  , int32-  , one-  , parse-  , repeated-  , text-  )+import GHC.Generics (Generic)  --- Proto types mirror the schema closely; conversion to domain NoteText/NoteRun--- types happens in Decode.hs where gzip decompression also lives.+------------------------------------------------------------------------+-- Wire types (Generic-derived Encode/Decode)+------------------------------------------------------------------------ +-- | Outermost @NoteStoreProto@ message; field 2 holds the nested 'WireDocument'.+data WireNoteStoreProto = WireNoteStoreProto+  { wnspDocument :: Optional 2 (Message WireDocument)+  }+  deriving (Generic, Show)+++instance Encode WireNoteStoreProto+++instance Decode WireNoteStoreProto+++-- | @Document@ message nested inside 'WireNoteStoreProto'; field 3 holds the 'WireNote'.+data WireDocument = WireDocument+  { wdNote :: Optional 3 (Message WireNote)+  }+  deriving (Generic, Show)+++instance Encode WireDocument+++instance Decode WireDocument+++-- | @Note@ message; field 2 is the note text, field 5 is the attribute runs.+data WireNote = WireNote+  { wnNoteText :: Optional 2 (Value Text)+  -- ^ full plain-text content (proto field 2)+  , wnAttributeRuns :: Repeated 5 (Message WireAttributeRun)+  -- ^ attribute runs describing formatting (proto field 5)+  }+  deriving (Generic, Show)+++instance Encode WireNote+++instance Decode WireNote+++-- | @AttributeRun@ message describing a span of formatted text.+data WireAttributeRun = WireAttributeRun+  { warLength :: Optional 1 (Value Int32)+  -- ^ number of UTF-16 code units this run covers+  , warParagraphStyle :: Optional 2 (Message WireParagraphStyle)+  -- ^ paragraph-level formatting; absent when the run carries none+  , warFontWeight :: Optional 5 (Value Int32)+  -- ^ font weight: 1=bold, 2=italic, 3=bold+italic; absent means no weight+  , warUnderlined :: Optional 6 (Value Int32)+  -- ^ non-zero when underlined+  , warStrikethrough :: Optional 7 (Value Int32)+  -- ^ non-zero when strikethrough+  , warLink :: Optional 9 (Value Text)+  -- ^ hyperlink URL; absent means no link+  , warAttachmentInfo :: Optional 12 (Message WireAttachmentInfo)+  -- ^ attachment sub-message; absent when no attachment+  }+  deriving (Generic, Show)+++instance Encode WireAttributeRun+++instance Decode WireAttributeRun+++-- | @ParagraphStyle@ message describing paragraph-level formatting.+data WireParagraphStyle = WireParagraphStyle+  { wpsStyleType :: Optional 1 (Value Int32)+  -- ^ 0=title, 1=heading, 2=subheading, 4=monospaced, 100=bullet, 101=dash, 102=numbered, 103=checklist+  , wpsIndentAmount :: Optional 4 (Value Int32)+  -- ^ indent level; absent means zero+  , wpsChecklist :: Optional 5 (Message WireChecklist)+  -- ^ checklist sub-message; absent when not a checklist paragraph+  , wpsListStart :: Optional 7 (Value Int32)+  -- ^ starting list item number; absent or zero means no explicit start+  , wpsBlockQuote :: Optional 8 (Value Int32)+  -- ^ non-zero when this paragraph is a block quote+  }+  deriving (Generic, Show)+++instance Encode WireParagraphStyle+++instance Decode WireParagraphStyle+++-- | @Checklist@ sub-message (field 5 of 'WireParagraphStyle').+data WireChecklist = WireChecklist+  { wclDone :: Optional 2 (Value Int32)+  -- ^ non-zero when the checklist item is checked+  }+  deriving (Generic, Show)+++instance Encode WireChecklist+++instance Decode WireChecklist+++-- | @AttachmentInfo@ sub-message (field 12 of 'WireAttributeRun').+data WireAttachmentInfo = WireAttachmentInfo+  { waiAttachmentId :: Optional 1 (Value Text)+  -- ^ attachment identifier string+  }+  deriving (Generic, Show)+++instance Encode WireAttachmentInfo+++instance Decode WireAttachmentInfo+++------------------------------------------------------------------------+-- Plain domain types (unchanged; consumed by Decode.hs)+------------------------------------------------------------------------+ -- | Top-level note content decoded from the protobuf payload. data ProtoNote = ProtoNote   { pnNoteText :: Text@@ -76,80 +216,76 @@   { ppsStyleType :: Int32   -- ^ style_type field 1: 0=title, 1=heading, 2=subheading, 4=monospaced, 100=bullet, 101=dash, 102=numbered, 103=checklist   , ppsIndent :: Int32-  -- ^ indent_amount field 4; 0 when absent.+  -- ^ indent_amount field 4; 0 when absent   , ppsChecked :: Maybe Bool-  -- ^ checklist.done field 5 sub-field 2; 'Nothing' when checklist sub-message absent.+  -- ^ checklist.done field 5 sub-field 2; 'Nothing' when checklist sub-message absent   , ppsListStart :: Maybe Int32-  -- ^ starting_list_item_number field 7; 'Nothing' when absent or zero.+  -- ^ starting_list_item_number field 7; 'Nothing' when absent or zero   , ppsBlockQuote :: Bool-  -- ^ block_quote field 8 non-zero.+  -- ^ block_quote field 8 non-zero   }   deriving (Eq, Show)  -{- | Decode a gzip-decompressed protobuf ByteString into a 'ProtoNote'.+------------------------------------------------------------------------+-- Decoding+------------------------------------------------------------------------++{- | Decode a decompressed protobuf 'ByteString' into a 'ProtoNote'. Returns 'Left' with a message if the outer document or note field is absent,-which would indicate a malformed or empty payload rather than a real note.+indicating a malformed or empty payload. -} decodeNoteStoreProto :: ByteString -> Either String ProtoNote-decodeNoteStoreProto bs =-  case parse parseNoteStoreProto bs of-    Left err -> Left (show err)-    Right Nothing -> Left "NoteStoreProto: document field absent"-    Right (Just Nothing) -> Left "Document: note field absent"-    Right (Just (Just note)) -> Right note----- Drill straight through NoteStoreProto (field 2) → Document (field 3) → Note--- without defining a separate Document record type.  Each `embedded` call wraps--- the result in Maybe: Nothing means the field was absent in the wire bytes.-parseNoteStoreProto :: Parser RawMessage (Maybe (Maybe ProtoNote))-parseNoteStoreProto = embedded (embedded parseProtoNote `at` 3) `at` 2+decodeNoteStoreProto bs = do+  outer <- runGet decodeMessage bs+  doc <-+    maybe+      (Left "NoteStoreProto: document field absent")+      Right+      (getField (wnspDocument outer))+  wNote <-+    maybe+      (Left "Document: note field absent")+      Right+      (getField (wdNote doc))+  pure (toProtoNote wNote)  --- `one text LT.empty` reads a singular string field, returning the default--- (empty) when the field is absent.  `fmap LT.toStrict` converts the lazy--- Text that proto3-wire produces to the strict Text used in ProtoNote.--- `repeated (embedded' ...)` collects all occurrences of a length-delimited--- field into a list; embedded' (vs embedded) is used inside repeated because--- the field is known to be present (not optional) at each occurrence.-parseProtoNote :: Parser RawMessage ProtoNote-parseProtoNote =+toProtoNote :: WireNote -> ProtoNote+toProtoNote wn =   ProtoNote-    <$> (fmap LT.toStrict (one text LT.empty) `at` 2)-    <*> (repeated (embedded' parseProtoAttributeRun) `at` 5)+    { pnNoteText = fromMaybe mempty (getField (wnNoteText wn))+    , pnAttributeRuns = map toProtoAttributeRun (getField (wnAttributeRuns wn))+    }  --- Scalar optional fields (font_weight, underlined, strikethrough) use--- `one int32 0` — the proto3 default of 0 means "absent / no effect" for--- all of them (0 = FONT_WEIGHT_UNKNOWN, 0 = not underlined, etc.).--- `embedded` for paragraph_style returns Maybe: Nothing when the run carries--- no paragraph-level formatting.-parseProtoAttributeRun :: Parser RawMessage ProtoAttributeRun-parseProtoAttributeRun =+toProtoAttributeRun :: WireAttributeRun -> ProtoAttributeRun+toProtoAttributeRun war =   ProtoAttributeRun-    <$> (one int32 0 `at` 1)-    <*> (embedded parseParagraphStyle `at` 2)-    <*> (one int32 0 `at` 5) -- font_weight (fields 3 and 4 are absent in schema)-    <*> (one int32 0 `at` 6) -- underlined-    <*> (one int32 0 `at` 7) -- strikethrough-    <*> fmap (>>= \t -> if LT.null t then Nothing else Just t) (embedded parseAttachmentInfo `at` 12)-    <*> (one text LT.empty `at` 9) -- link (field 8 skipped: superscript)---parseAttachmentInfo :: Parser RawMessage LT.Text-parseAttachmentInfo = one text LT.empty `at` 1+    { parLength = fromMaybe 0 (getField (warLength war))+    , parParagraphStyle = fmap toParagraphStyle (getField (warParagraphStyle war))+    , parFontWeight = fromMaybe 0 (getField (warFontWeight war))+    , parUnderlined = fromMaybe 0 (getField (warUnderlined war))+    , parStrikethrough = fromMaybe 0 (getField (warStrikethrough war))+    , parAttachmentId =+        getField (warAttachmentInfo war) >>= \ai ->+          let t = maybe LT.empty LT.fromStrict (getField (waiAttachmentId ai))+           in if LT.null t then Nothing else Just t+    , parLink = maybe LT.empty LT.fromStrict (getField (warLink war))+    }  -parseParagraphStyle :: Parser RawMessage ProtoParagraphStyle-parseParagraphStyle =+toParagraphStyle :: WireParagraphStyle -> ProtoParagraphStyle+toParagraphStyle wps =   ProtoParagraphStyle-    <$> (one int32 0 `at` 1)-    <*> (one int32 0 `at` 4)-    <*> (embedded parseChecklist `at` 5)-    <*> (fmap (\n -> if n == 0 then Nothing else Just n) (one int32 0) `at` 7)-    <*> (fmap (/= 0) (one int32 0) `at` 8)---parseChecklist :: Parser RawMessage Bool-parseChecklist = fmap (/= 0) (one int32 0 `at` 2)+    { ppsStyleType = fromMaybe 0 (getField (wpsStyleType wps))+    , ppsIndent = fromMaybe 0 (getField (wpsIndentAmount wps))+    , ppsChecked =+        fmap+          (\cl -> fromMaybe 0 (getField (wclDone cl)) /= 0)+          (getField (wpsChecklist wps))+    , ppsListStart =+        getField (wpsListStart wps) >>= \n ->+          if n == 0 then Nothing else Just n+    , ppsBlockQuote = fromMaybe 0 (getField (wpsBlockQuote wps)) /= 0+    }
test/HStratus/Notes/DecodeSpec.hs view
@@ -124,23 +124,23 @@   strikethroughFixtureBytes :: ByteString-strikethroughFixtureBytes = mkNoteGzip "hi" [runFields 2 [(7, 1)]]+strikethroughFixtureBytes = mkNoteGzip "hi" [strikethroughRun 2]   bulletIndent1FixtureBytes :: ByteString-bulletIndent1FixtureBytes = mkNoteGzip "hi" [runWith 1 (psStyleType 100 <> psIndentAmount 1)]+bulletIndent1FixtureBytes = mkNoteGzip "hi" [runWith 1 (psIndentAmount 1 (psStyleType 100))]   checklistDoneFixtureBytes :: ByteString-checklistDoneFixtureBytes = mkNoteGzip "hi" [runWith 1 (psStyleType 103 <> psChecklist True)]+checklistDoneFixtureBytes = mkNoteGzip "hi" [runWith 1 (psChecklist True (psStyleType 103))]   checklistUndoneFixtureBytes :: ByteString-checklistUndoneFixtureBytes = mkNoteGzip "hi" [runWith 1 (psStyleType 103 <> psChecklist False)]+checklistUndoneFixtureBytes = mkNoteGzip "hi" [runWith 1 (psChecklist False (psStyleType 103))]   numberedListStart3FixtureBytes :: ByteString-numberedListStart3FixtureBytes = mkNoteGzip "hi" [runWith 1 (psStyleType 102 <> psListStart 3)]+numberedListStart3FixtureBytes = mkNoteGzip "hi" [runWith 1 (psListStart 3 (psStyleType 102))]   blockQuoteFixtureBytes :: ByteString
test/HStratus/Notes/ProtoSpec.hs view
@@ -6,8 +6,7 @@ Maintainer  : Tim Emiola <adetokunbo@emio.la> SPDX-License-Identifier: BSD-3-Clause -Tests for the proto3-wire decoders in-'Network.HStratus.Internal.Notes.Proto'.+Tests for the protobuf decoders in 'Network.HStratus.Internal.Notes.Proto'. -} module HStratus.Notes.ProtoSpec (spec) where @@ -131,19 +130,19 @@   noteWithStrikethroughBytes :: ByteString-noteWithStrikethroughBytes = mkNote "hi" [runFields 2 [(7, 1)]]+noteWithStrikethroughBytes = mkNote "hi" [strikethroughRun 2]   bulletIndent1Bytes :: ByteString-bulletIndent1Bytes = mkNote "hi" [runWith 1 (psStyleType 100 <> psIndentAmount 1)]+bulletIndent1Bytes = mkNote "hi" [runWith 1 (psIndentAmount 1 (psStyleType 100))]   checklistDoneBytes :: ByteString-checklistDoneBytes = mkNote "hi" [runWith 1 (psStyleType 103 <> psChecklist True)]+checklistDoneBytes = mkNote "hi" [runWith 1 (psChecklist True (psStyleType 103))]   checklistUndoneBytes :: ByteString-checklistUndoneBytes = mkNote "hi" [runWith 1 (psStyleType 103 <> psChecklist False)]+checklistUndoneBytes = mkNote "hi" [runWith 1 (psChecklist False (psStyleType 103))]   checklistAbsentBytes :: ByteString@@ -151,7 +150,7 @@   numberedListStart3Bytes :: ByteString-numberedListStart3Bytes = mkNote "hi" [runWith 1 (psStyleType 102 <> psListStart 3)]+numberedListStart3Bytes = mkNote "hi" [runWith 1 (psListStart 3 (psStyleType 102))]   blockQuoteBytes :: ByteString
test/HStratus/Notes/TestHelper.hs view
@@ -7,7 +7,7 @@ SPDX-License-Identifier: BSD-3-Clause  Programmatic proto\/gzip fixture builders for 'ProtoSpec' and 'DecodeSpec':-encodes domain values with @proto3-wire@ and compresses with @zlib@, avoiding+encodes domain values with @protobuf@ and compresses with @zlib@, avoiding hand-crafted byte literals. -} module HStratus.Notes.TestHelper@@ -16,7 +16,8 @@   , mkNoteZlib   , mkEmptyGzip   , runWith-  , runFields+  , plainRun+  , strikethroughRun   , psStyleType   , psIndentAmount   , psChecklist@@ -34,37 +35,54 @@ import Data.ByteString (ByteString) import qualified Data.ByteString.Lazy as LBS import Data.Int (Int32)+import Data.ProtocolBuffers (encodeMessage, putField)+import Data.Serialize (runPut) import Data.Text (Text)-import qualified Data.Text.Lazy as TL import Network.HStratus.Internal.Notes.Note   ( NoteRun (..)   , NoteStyle (..)   , NoteText (..)   )-import Network.HStratus.Internal.Notes.Proto (ProtoParagraphStyle (..))-import qualified Proto3.Wire.Encode as Encode+import Network.HStratus.Internal.Notes.Proto+  ( ProtoParagraphStyle (..)+  , WireAttachmentInfo (..)+  , WireAttributeRun (..)+  , WireChecklist (..)+  , WireDocument (..)+  , WireNote (..)+  , WireNoteStoreProto (..)+  , WireParagraphStyle (..)+  )   {- | Encode a NoteStoreProto with the given note text and attribute runs. Returns raw (uncompressed) proto bytes. -}-mkNote :: Text -> [Encode.MessageBuilder] -> ByteString+mkNote :: Text -> [WireAttributeRun] -> ByteString mkNote noteText runs =-  LBS.toStrict . Encode.toLazyByteString $-    Encode.embedded 2 $-      Encode.embedded 3 $-        Encode.text 2 (TL.fromStrict noteText)-          <> foldMap (Encode.embedded 5) runs+  runPut . encodeMessage $+    WireNoteStoreProto+      { wnspDocument =+          putField . Just $+            WireDocument+              { wdNote =+                  putField . Just $+                    WireNote+                      { wnNoteText = putField (Just noteText)+                      , wnAttributeRuns = putField runs+                      }+              }+      }   -- | Like 'mkNote' but gzip-compressed, suitable for 'decodeNoteBody'.-mkNoteGzip :: Text -> [Encode.MessageBuilder] -> ByteString+mkNoteGzip :: Text -> [WireAttributeRun] -> ByteString mkNoteGzip noteText runs =   LBS.toStrict . GZip.compress . LBS.fromStrict $ mkNote noteText runs   -- | Like 'mkNote' but zlib-compressed, suitable for 'decodeNoteBody'.-mkNoteZlib :: Text -> [Encode.MessageBuilder] -> ByteString+mkNoteZlib :: Text -> [WireAttributeRun] -> ByteString mkNoteZlib noteText runs =   LBS.toStrict . Zlib.compress . LBS.fromStrict $ mkNote noteText runs @@ -76,63 +94,112 @@ mkEmptyGzip = LBS.toStrict (GZip.compress LBS.empty)  --- | An AttributeRun with the given length and a ParagraphStyle sub-message.-runWith :: Int32 -> Encode.MessageBuilder -> Encode.MessageBuilder-runWith len style = Encode.int32 1 len <> Encode.embedded 2 style+-- | An 'WireAttributeRun' with the given length and a 'WireParagraphStyle' sub-message.+runWith :: Int32 -> WireParagraphStyle -> WireAttributeRun+runWith len style =+  WireAttributeRun+    { warLength = putField (Just len)+    , warParagraphStyle = putField (Just style)+    , warFontWeight = putField Nothing+    , warUnderlined = putField Nothing+    , warStrikethrough = putField Nothing+    , warLink = putField Nothing+    , warAttachmentInfo = putField Nothing+    }  -{- | An AttributeRun with the given length and a list of (fieldNumber, value)-pairs for inline fields (font_weight, underlined, strikethrough, etc.).+{- | A 'WireAttributeRun' with the given length and all other fields absent.+Use record-update syntax to set individual scalar fields for targeted tests. -}-runFields :: Int32 -> [(Int32, Int32)] -> Encode.MessageBuilder-runFields len fields =-  Encode.int32 1 len-    <> foldMap (\(fn, v) -> Encode.int32 (fromIntegral fn) v) fields+plainRun :: Int32 -> WireAttributeRun+plainRun len =+  WireAttributeRun+    { warLength = putField (Just len)+    , warParagraphStyle = putField Nothing+    , warFontWeight = putField Nothing+    , warUnderlined = putField Nothing+    , warStrikethrough = putField Nothing+    , warLink = putField Nothing+    , warAttachmentInfo = putField Nothing+    }  --- ParagraphStyle field builders -------------------------------------------+-- | A 'WireAttributeRun' of the given length with the strikethrough field set.+strikethroughRun :: Int32 -> WireAttributeRun+strikethroughRun len = (plainRun len){warStrikethrough = putField (Just 1)} -psStyleType :: Int32 -> Encode.MessageBuilder-psStyleType = Encode.int32 1 +-- ParagraphStyle constructors and modifiers ----------------------------------- -psIndentAmount :: Int32 -> Encode.MessageBuilder-psIndentAmount = Encode.int32 4+-- | A 'WireParagraphStyle' with only the @style_type@ field set.+psStyleType :: Int32 -> WireParagraphStyle+psStyleType n =+  WireParagraphStyle+    { wpsStyleType = putField (Just n)+    , wpsIndentAmount = putField Nothing+    , wpsChecklist = putField Nothing+    , wpsListStart = putField Nothing+    , wpsBlockQuote = putField Nothing+    }  --- | Encode a Checklist sub-message (field 5) with the given done state.-psChecklist :: Bool -> Encode.MessageBuilder-psChecklist done =-  Encode.embedded 5 (Encode.int32 2 (if done then 1 else 0))+-- | Set the @indent_amount@ field on a 'WireParagraphStyle'.+psIndentAmount :: Int32 -> WireParagraphStyle -> WireParagraphStyle+psIndentAmount n ps = ps{wpsIndentAmount = putField (Just n)}  -psListStart :: Int32 -> Encode.MessageBuilder-psListStart = Encode.int32 7+-- | Set the @checklist@ sub-message on a 'WireParagraphStyle'.+psChecklist :: Bool -> WireParagraphStyle -> WireParagraphStyle+psChecklist done ps =+  ps+    { wpsChecklist =+        putField . Just $+          WireChecklist{wclDone = putField (Just (if done then 1 else 0))}+    }  -psBlockQuote :: Encode.MessageBuilder-psBlockQuote = Encode.int32 8 1+-- | Set the @starting_list_item_number@ field on a 'WireParagraphStyle'.+psListStart :: Int32 -> WireParagraphStyle -> WireParagraphStyle+psListStart n ps = ps{wpsListStart = putField (Just n)}  --- Encoder functions -------------------------------------------------------+-- | A 'WireParagraphStyle' with only the @block_quote@ field set.+psBlockQuote :: WireParagraphStyle+psBlockQuote =+  WireParagraphStyle+    { wpsStyleType = putField Nothing+    , wpsIndentAmount = putField Nothing+    , wpsChecklist = putField Nothing+    , wpsListStart = putField Nothing+    , wpsBlockQuote = putField (Just 1)+    } -{- | Encode a 'ProtoParagraphStyle' back to wire bytes, reusing the ps* helpers.++-- Encoder functions -----------------------------------------------------------++{- | Encode a 'ProtoParagraphStyle' as a 'WireParagraphStyle'. Proto3 default values (0 / False / Nothing) are omitted to match wire convention. -}-encodeParagraphStyle :: ProtoParagraphStyle -> Encode.MessageBuilder+encodeParagraphStyle :: ProtoParagraphStyle -> WireParagraphStyle encodeParagraphStyle ProtoParagraphStyle{ppsStyleType, ppsIndent, ppsChecked, ppsListStart, ppsBlockQuote} =-  (if ppsStyleType /= 0 then psStyleType ppsStyleType else mempty)-    <> (if ppsIndent /= 0 then psIndentAmount ppsIndent else mempty)-    <> maybe mempty psChecklist ppsChecked-    <> maybe mempty psListStart ppsListStart-    <> (if ppsBlockQuote then psBlockQuote else mempty)+  WireParagraphStyle+    { wpsStyleType = putField (if ppsStyleType /= 0 then Just ppsStyleType else Nothing)+    , wpsIndentAmount = putField (if ppsIndent /= 0 then Just ppsIndent else Nothing)+    , wpsChecklist =+        putField $+          fmap+            (\c -> WireChecklist{wclDone = putField (Just (if c then 1 else 0))})+            ppsChecked+    , wpsListStart = putField ppsListStart+    , wpsBlockQuote = putField (if ppsBlockQuote then Just 1 else Nothing)+    }  -{- | Encode a 'NoteStyle' as a paragraph_style sub-message.+{- | Encode a 'NoteStyle' as a 'WireParagraphStyle'. Inverse of 'toNoteStyle'; 'StyleBody False' is not in the image of 'toNoteStyle' and should not appear in generated test data. -}-encodeNoteStyle :: NoteStyle -> Encode.MessageBuilder+encodeNoteStyle :: NoteStyle -> WireParagraphStyle encodeNoteStyle style = encodeParagraphStyle $ case style of   StyleTitle -> ProtoParagraphStyle 0 0 Nothing Nothing False   StyleHeading -> ProtoParagraphStyle 1 0 Nothing Nothing False@@ -145,18 +212,18 @@   StyleChecklist i c -> ProtoParagraphStyle 103 (fromIntegral i) (Just c) Nothing False  -{- | Encode a 'NoteRun' as an AttributeRun sub-message.-'nrLink' is always 'Nothing' in generated runs (deferred).--}-encodeNoteRun :: NoteRun -> Encode.MessageBuilder+-- | Encode a 'NoteRun' as a 'WireAttributeRun'.+encodeNoteRun :: NoteRun -> WireAttributeRun encodeNoteRun NoteRun{nrLength, nrStyle, nrBold, nrItalic, nrUnderline, nrStrikethrough, nrAttachmentId, nrLink} =-  Encode.int32 1 nrLength-    <> maybe mempty (Encode.embedded 2 . encodeNoteStyle) nrStyle-    <> (if fw /= 0 then Encode.int32 5 fw else mempty)-    <> (if nrUnderline then Encode.int32 6 1 else mempty)-    <> (if nrStrikethrough then Encode.int32 7 1 else mempty)-    <> maybe mempty (\t -> Encode.embedded 12 (Encode.text 1 (TL.fromStrict t))) nrAttachmentId-    <> maybe mempty (Encode.text 9 . TL.fromStrict) nrLink+  WireAttributeRun+    { warLength = putField (Just nrLength)+    , warParagraphStyle = putField (fmap encodeNoteStyle nrStyle)+    , warFontWeight = putField (if fw /= 0 then Just fw else Nothing)+    , warUnderlined = putField (if nrUnderline then Just 1 else Nothing)+    , warStrikethrough = putField (if nrStrikethrough then Just 1 else Nothing)+    , warLink = putField nrLink+    , warAttachmentInfo = putField (fmap mkAttachmentInfo nrAttachmentId)+    }  where   fw :: Int32   fw = case (nrBold, nrItalic) of@@ -164,6 +231,7 @@     (True, False) -> 1     (False, True) -> 2     (False, False) -> 0+  mkAttachmentInfo t = WireAttachmentInfo{waiAttachmentId = putField (Just t)}   -- | Encode a 'NoteText' to gzip-compressed proto bytes, suitable for 'decodeNoteBody'.