kb-text-layout-0.1.0.0: src/KB/Text/Layout/Analysis.hs
module KB.Text.Layout.Analysis
( -- * Splitting text into break units
analyze
, Segment (..)
, BreakKind (..)
) where
import Data.Char (isDigit, isSpace)
import Data.List (intersperse)
import Data.Text (Text)
import Data.Text qualified as Text
import Data.Vector qualified as Vector
import KB.Text.Layout.Segmentation qualified as Segmentation
analyze :: Text -> [Segment]
analyze t = go 0 (tailorSoftBreaks t (Segmentation.softBreaks t)) (map toSegment (Text.groupBy sameKind t))
where
sameKind a b = classify a == classify b && groupable (classify a)
toSegment piece = Segment{text = piece, kind = classify (Text.head piece)}
go _ _ [] = []
go off soft (s : rest) =
let
end = off + Text.length s.text
(inside, later) = span (< end) (dropWhile (<= off) soft)
pieces
| s.kind == Word, not (null inside) = intersperse breakOpportunity (cutAt off inside s.text)
| otherwise = [s]
in
pieces <> go end later rest
breakOpportunity = Segment{text = "", kind = ZeroWidthBreak}
cutAt off cuts txt = pieces 0 (map (subtract off) cuts)
where
pieces prev = \case
[] -> [Segment{text = Text.drop prev txt, kind = Word}]
p : rest -> Segment{text = Text.take (p - prev) (Text.drop prev txt), kind = Word} : pieces p rest
data Segment = Segment
{ text :: Text
, kind :: BreakKind
}
deriving stock (Eq, Show)
data BreakKind
= Word
| Space
| PreservedSpace
| Tab
| Glue
| ZeroWidthBreak
| SoftHyphen
| HardBreak
| Atomic
deriving stock (Eq, Ord, Show)
tailorSoftBreaks :: Text -> [Int] -> [Int]
tailorSoftBreaks t soft = merge (filter (not . insideRange) soft) querySplits
where
n = Text.length t
chars = Vector.fromList (Text.unpack t)
at i = chars Vector.! i
insideRange p =
p >= 2
&& p < n
&& (at (p - 1) == '\x2013' || at (p - 1) == '\x2014')
&& isDigit (at (p - 2))
&& isDigit (at p)
slashToken =
Vector.fromList $
concatMap
(\run -> replicate (Text.length run) (not (isSpace (Text.head run)) && Text.any (== '/') run))
(Text.groupBy (\a b -> isSpace a == isSpace b) t)
querySplits =
[ p
| p <- [1 .. n - 1]
, at p == '&' || at p == '=' || at p == '#'
, slashToken Vector.! p
]
merge xs [] = xs
merge [] ys = ys
merge (x : xs) (y : ys)
| x < y = x : merge xs (y : ys)
| x > y = y : merge (x : xs) ys
| otherwise = x : merge xs ys
classify :: Char -> BreakKind
classify = \case
' ' -> Space
'\t' -> Tab
'\n' -> HardBreak
'\r' -> HardBreak
'\xA0' -> Glue
'\x202F' -> Glue
'\x2060' -> Glue
'\xFEFF' -> Glue
'\x200B' -> ZeroWidthBreak
'\xAD' -> SoftHyphen
_ -> Word
groupable :: BreakKind -> Bool
groupable = \case
Word -> True
Space -> True
Glue -> True
Tab -> True
_ -> False