packages feed

kitchen-sink-0.1.0.0: src/KitchenSink/Layout/Blog/Analyses/SkyLine.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}

module KitchenSink.Layout.Blog.Analyses.SkyLine (
    SkyLine (..),
    SkyLineItem (..),
    sectionSkyLine,
) where

import Data.Aeson (FromJSON, ToJSON)
import Data.List qualified as List
import Data.Text qualified as Text
import GHC.Generics (Generic)
import Prelude (succ, (-))

import KitchenSink.Commonmark.Free as CMark
import KitchenSink.Prelude

newtype SkyLine = SkyLine {skylineItems :: [SkyLineItem]}
    deriving (Show)
    deriving (Generic)
    deriving (Semigroup) via [SkyLineItem]
    deriving (Monoid) via [SkyLineItem]
instance ToJSON SkyLine
instance FromJSON SkyLine

data SkyLineItem
    = HeaderMark Text HeadingLevels
    | ImageMark Text Text
    | TextualMark TextWeight HeadingLevels
    deriving (Show)
    deriving (Generic)
instance ToJSON SkyLineItem
instance FromJSON SkyLineItem

type TextWeight = Int -- todo newtype over Sum Int
type HeadingLevels = [Int] -- todo newtype over nonempty list

sectionSkyLine :: CMark.Block -> SkyLine
sectionSkyLine s =
    let u = s : blockUniplate s
     in SkyLine $ List.reverse $ snd $ foldSkylineBlocks [c | b <- u, c <- blockChunks b]
  where
    foldSkylineBlocks :: [CMark.BlockChunk] -> (HeadingLevels, [SkyLineItem])
    foldSkylineBlocks = List.foldl' foldOneBlock ([0], [])

    foldOneBlock :: (HeadingLevels, [SkyLineItem]) -> CMark.BlockChunk -> (HeadingLevels, [SkyLineItem])
    foldOneBlock (hdrs, xs) b =
        let hdrs' = case b of
                Heading n _ -> updateHeaders n hdrs
                _ -> hdrs
            item = case b of
                Heading _ il ->
                    HeaderMark (mconcat $ fmap flattenText $ inlineChunks $ il) hdrs'
                x ->
                    let directInlines = blockChunkInlines x
                        childrenInlines = List.concatMap blockInlines (blockChunkBlocks x)
                        allInlines = childrenInlines <> directInlines
                     in TextualMark (List.sum $ fmap go allInlines) hdrs'
         in (hdrs', item : xs)

    updateHeaders :: Int -> [Int] -> [Int]
    updateHeaders level counters =
        case List.drop (List.length counters - level) counters of
            [] -> 1 : counters
            cnt : levelcounters -> succ cnt : levelcounters

    go il = List.sum [f c | c <- inlineChunks il]
    f x = case x of
        Str t -> wc t
        Link _ t _ -> wc t
        Code t -> wc t
        Entity _ -> 1
        EscapedChar _ -> 1
        _ -> 0
    flattenText x = case x of
        Str t -> t
        Link _ t _ -> t
        Code t -> t
        _ -> mempty

    wc = List.length . List.filter (\x -> not $ List.elem x ["", ":"]) . Text.words