packages feed

notion-client-0.8.0.0: src/Notion/V1/Helpers.hs

-- | Helpers for turning Notion URLs into IDs, ported from the official JS SDK.
module Notion.V1.Helpers
  ( extractNotionId,
    extractPageId,
    extractDatabaseId,
    extractBlockId,
  )
where

import Data.Char (isHexDigit)
import Data.List (find)
import Data.Maybe (catMaybes, listToMaybe, mapMaybe)
import Data.Text qualified as Text
import Notion.Prelude
import Notion.V1.Common (UUID (..))

-- | Extract a Notion ID from a URL or an ID in either format. Returns the
-- lowercase, hyphenated form.
--
-- Tried in order: a hyphenated UUID; 32 hex digits; a path segment ending in
-- @-\<32 hex digits\>@ (for example @.../Meeting-Notes-\<id\>@); a @p@,
-- @page_id@ or @database_id@ query parameter; the first run of 32 hex digits.
--
-- @
-- extractNotionId "https://www.notion.so/team/Tasks-abc123def456789012345678901234ab?v=..."
--   == Just (UUID "abc123de-f456-7890-1234-5678901234ab")
-- @
extractNotionId :: Text -> Maybe UUID
extractNotionId input
  | isHyphenatedUuid t = Just (UUID (Text.toLower t))
  | isHex32 t = Just (formatUuid t)
  | otherwise = formatUuid <$> firstJust [pathRule, queryRule, anyRule]
  where
    t = Text.strip input

    pathRule =
      listToMaybe
        [ Text.takeEnd 32 segment
        | rest <- afterEach (== '/') t,
          let segment = Text.takeWhile (`notElem` ("/?#" :: String)) rest,
          Text.length segment >= 33,
          Text.take 1 (Text.takeEnd 33 segment) == "-",
          isHex32 (Text.takeEnd 32 segment)
        ]

    queryRule =
      listToMaybe
        [ candidate
        | rest <- afterEach (`elem` ("?&" :: String)) t,
          prefix <- ["p=", "page_id=", "database_id="],
          Text.toLower (Text.take (Text.length prefix) rest) == prefix,
          let candidate = Text.take 32 (Text.drop (Text.length prefix) rest),
          isHex32 candidate
        ]

    anyRule = find isHex32 (map (Text.take 32) (Text.tails t))

-- | Alias of 'extractNotionId' for page URLs.
extractPageId :: Text -> Maybe UUID
extractPageId = extractNotionId

-- | Alias of 'extractNotionId' for database URLs.
extractDatabaseId :: Text -> Maybe UUID
extractDatabaseId = extractNotionId

-- | Extract a block ID from a URL fragment: @#block-\<id\>@ or @#\<id\>@.
extractBlockId :: Text -> Maybe UUID
extractBlockId input =
  formatUuid
    <$> listToMaybe
      [ candidate
      | rest <- afterEach (== '#') input,
        let afterPrefix =
              if Text.toLower (Text.take 6 rest) == "block-" then Text.drop 6 rest else rest
            candidate = Text.take 32 afterPrefix,
        isHex32 candidate
      ]

-- | The remainder of the text after each character matching the predicate, left to right.
afterEach :: (Char -> Bool) -> Text -> [Text]
afterEach p = mapMaybe after . Text.tails
  where
    after s = case Text.uncons s of
      Just (c, rest) | p c -> Just rest
      _ -> Nothing

firstJust :: [Maybe a] -> Maybe a
firstJust = listToMaybe . catMaybes

isHex32 :: Text -> Bool
isHex32 s = Text.length s == 32 && Text.all isHexDigit s

isHyphenatedUuid :: Text -> Bool
isHyphenatedUuid s =
  Text.length s == 36
    && and (zipWith valid [0 :: Int ..] (Text.unpack s))
  where
    valid i c
      | i `elem` [8, 13, 18, 23] = c == '-'
      | otherwise = isHexDigit c

-- | Lowercase 32 hex digits with hyphens in the 8-4-4-4-12 pattern.
formatUuid :: Text -> UUID
formatUuid hex =
  UUID . Text.intercalate "-" $
    [Text.take 8 l, Text.take 4 (Text.drop 8 l), Text.take 4 (Text.drop 12 l), Text.take 4 (Text.drop 16 l), Text.drop 20 l]
  where
    l = Text.toLower hex