1 {-# LANGUAGE OverloadedLists #-}
3 module Rosetta.Matching where
5 import Data.ByteString.Builder (Builder)
7 -- import Data.Char qualified as Char
8 import Data.List qualified as List
9 import Data.Map.Strict qualified as Map
11 -- import Data.Set qualified as Set
12 import Data.Text qualified as Text
13 import System.Random qualified as Random
14 import System.Random.Shuffle as Random
16 -- import Data.Text.Short qualified as ShortText
17 import System.FilePath.Posix ((</>))
18 import System.FilePath.Posix qualified as File
20 import Text.Blaze.Html5 qualified as HTML
21 import Text.Blaze.Html5.Attributes qualified as HA
22 import Text.Blaze.Renderer.Utf8 qualified as Blaze
23 import Prelude (div, even)
26 import Language.Chinese (ChineseDict (..), ChineseDictEntries (..))
27 import Language.Chinese qualified
28 import Language.Chinese qualified as Chinese
29 import Language.English qualified as English
30 import Language.French qualified as French
31 import Language.Pronunciation qualified as Pron
32 import Paths_worksheets qualified as Self
33 import Worksheets.Utils.Char as Char
34 import Worksheets.Utils.HTML (Length, className, classes, cm, fr, styles)
35 import Worksheets.Utils.HTML qualified as HTML
36 import Worksheets.Utils.IPA qualified as IPA
37 import Worksheets.Utils.Paper
38 import Worksheets.Utils.Prelude
40 type Difficulties = Map Langue (Map UnicodeBlock Difficulty)
41 data Difficulty = Difficulty
42 { difficultyCharWidth :: Length
43 , difficultyCharHeight :: Length
44 , difficultyWordSpacing :: Length
45 , difficultyFontSize :: Length
46 , difficultyFontFamily :: Text
47 , difficultyFontVariant :: Text
48 , difficultyColor :: Text
49 , difficultyLineHeight :: Maybe Double
51 deriving (Eq, Show, Generic)
52 instance HasTypeDefault Difficulty where
55 { difficultyCharWidth = 1 & cm
56 , difficultyCharHeight = 1 & cm
57 , difficultyWordSpacing = 0.5 & cm
58 , difficultyFontSize = 1 & cm
59 , difficultyFontFamily = "Arial"
60 , difficultyFontVariant = "small-caps"
61 , difficultyColor = "#000000"
62 , difficultyLineHeight = Nothing
65 difficultyBig :: Char.UnicodeBlock -> Modifier Difficulty
66 difficultyBig ub v = case ub of
67 Char.UnicodeBlockLatin{} ->
69 { difficultyCharWidth = 1 & cm
70 , difficultyCharHeight = 1 & cm
71 , difficultyWordSpacing = 0.5 & cm
72 , difficultyFontSize = 0.90 & cm
74 Char.UnicodeBlockCJK{} ->
76 { difficultyCharWidth = 1.50 & cm
77 , difficultyCharHeight = 1.50 & cm
78 , difficultyWordSpacing = 0.5 & cm
79 , difficultyFontSize = 1.40 & cm
81 Char.UnicodeBlockHalfwidth_and_Fullwidth_Forms{} ->
83 { difficultyCharWidth = 1.50 & cm
84 , difficultyCharHeight = 1.50 & cm
85 , difficultyWordSpacing = 0.5 & cm
86 , difficultyFontSize = 1.40 & cm
92 { matterText :: Pron.InputLexemes
93 , matterLangue :: Langue
96 { matterSyls :: [[Pron.Syl]]
97 , matterLangue :: Langue
100 { matterSyls :: [[Pron.Syl]]
101 , matterLangue :: Langue
104 { matterPicture :: File.FilePath
105 , matterPictureCSS :: HTML.CSSBlock
107 deriving (Eq, Show, Generic)
109 -- deriving (HasTypeDefault) via (Generically Matter)
112 { matchMatters :: [Matter]
114 deriving (Eq, Show, Generic)
116 instance HasTypeDefault Match where
123 { pageMatches :: [Match]
124 , pageMatchesNum :: Natural
125 , pageSize :: PageSize
126 , pageOrientation :: PageOrientation
128 deriving (Eq, Show, Generic)
129 instance HasTypeDefault Page where
132 { pageMatches = mempty
133 , pageMatchesNum = 10
134 , pageSize = typeDefault
135 , pageOrientation = typeDefault
139 { pagesList :: [Page]
140 , pagesDifficulties :: Difficulties
142 deriving (Eq, Show, Generic)
143 instance HasTypeDefault Pages where
147 , pagesDifficulties =
149 [ block := typeDefault
150 | block <- Char.unicodeBlocks & toList
153 | lang <- langues & toList
159 { dictsChinese :: Chinese.ChineseDict
160 , dictsFrench :: Pron.Table
161 , dictsEnglish :: Pron.Table
164 pagesHTML :: Dicts -> Text -> Pages -> IO Builder
165 pagesHTML Dicts{..} title pagesUnchunked = do
166 -- FIXME: this absolute path is not portable out of my system
167 dataPath <- Self.getDataDir <&> File.normalise
168 randomGen <- Random.getStdGen
169 pagesListShuffled :: [[Page]] <-
170 forM (pagesUnchunked & pagesList) \page -> do
171 pagesMatchesShuffled <- page & pageMatches & Random.shuffleM
172 forM (pagesMatchesShuffled & chunksOf (page & pageMatchesNum)) \matches -> do
174 matches <&> \match ->
176 match & matchMatters & foldMap \case
177 MatterText{matterLangue = LangueAnglais, matterText} ->
178 [ MatterSound{matterSyls, matterLangue = LangueAnglais}
179 , MatterSyls{matterSyls, matterLangue = LangueAnglais}
184 & Pron.unInputLexemes
185 & Pron.runParser dictsEnglish
186 & either errorShow id
189 MatterText{matterLangue = LangueFrançais, matterText} ->
190 [ MatterSound{matterSyls, matterLangue = LangueFrançais}
191 , MatterSyls{matterSyls, matterLangue = LangueFrançais}
196 & Pron.unInputLexemes
197 & Pron.runParser dictsFrench
198 & either errorShow id
201 MatterText{matterLangue = LangueMandarin, matterText} ->
202 [ MatterSound{matterSyls, matterLangue = LangueMandarin}
203 , MatterSyls{matterSyls, matterLangue = LangueMandarin}
208 & Pron.unInputLexemes
209 & Chinese.pronunciation dictsChinese
211 x@MatterPicture{} -> [x]
212 x -> errorShow $ "pagesHTML: unsupported Matter: " <> show x
213 pageMatches <- forM (matchesSyls <&> matchMatters & List.transpose) \matters ->
214 Random.shuffleM matters
216 return page{pageMatches = pageMatches & List.transpose <&> Match}
217 return $ Blaze.renderMarkupBuilder do
220 HTML.title $ title & HTML.toHtml
222 ( [ "styles/Paper.css"
223 , "styles/Rosetta/Common.css"
224 , "styles/Rosetta/Matching.css"
230 ! HA.rel "stylesheet"
231 ! HA.type_ "text/css"
232 ! HA.href (dataPath </> cssFile & toValue)
233 -- styleCSS $ cssPrintPage pageOrientation pageSize
234 HTML.styleCSS $ pagesUnchunked & pagesDifficulties & difficultyCSS
236 forM_ pagesListShuffled \pages -> do
238 forM_ pages \page -> do
244 , page & pageOrientation & HTML.cssPageOrientation
249 [ page & pageSize & HTML.cssPageSize
250 , page & pageOrientation & HTML.cssPageOrientation
255 let matchColumns = page & pageMatches & foldMap (matchMatters >>> List.length >>> Max) & getMax
256 let matchRows = page & pageMatches & List.length
258 ! classes ["matches"]
260 [ "grid-template-columns" := "1em 1fr" & List.replicate matchColumns & List.unwords
261 , "grid-template-rows" := "2cm" & List.replicate matchRows & List.unwords
264 let pageMatchNum = page & pageMatches & List.length
265 forM_ (page & pageMatches & ol0) \(matchIdx, match) -> do
267 -- when (matchIdx /= 0) do
268 -- forM_ (match & matchMatters) \_matter -> do
269 -- HTML.div ! classes ["match-links"] $ ""
270 -- HTML.div ! classes ["match-links"] $ ""
272 ! classes ["match-alternatives"]
274 forM_ (match & matchMatters & ol0) \(matterIdx, matter) -> do
277 MatterPicture{} -> []
278 MatterSyls{matterLangue} -> ["lang-model", "lang-" <> className matterLangue]
279 MatterSound{matterLangue} -> ["lang-" <> className matterLangue]
284 ! classes ["match-anchor", "match-anchor-numbered"]
287 ["➀", "➁", "➂", "➃", "➄", "➅ ", "➆ ", "➇ ", "➈ ", "➉"]
292 ! classes ["match-anchor"]
296 ! classes (["match-matter"] <> langClass)
299 MatterSyls{matterSyls} -> do
300 forM_ matterSyls \word -> do
301 HTML.div ! classes ["sentence-horiz"] $ do
302 forM_ word \syl -> do
303 forM_ (syl & Pron.sylText & Text.unpack) \pronChar -> do
304 let uniScript = pronChar & Char.unicodeBlock & fromMaybe (UnicodeBlockLatin UnicodeBlockLatin_Basic)
307 [ "script-" <> className uniScript
310 pronChar & HTML.toHtml
311 MatterSound{matterSyls} -> do
312 forM_ matterSyls \word -> do
313 HTML.div ! classes ["sentence-horiz"] $ do
314 forM_ word \syl -> do
315 let sylTextLength = syl & Pron.sylText & Text.length
318 [ "sentence-syllable"
321 [ "grid-template-columns" :=
323 <> (syl & Pron.sylText & Text.length & show)
327 let sylSoundParts = syl & Pron.sylSound & Text.words & \l -> if null l then [""] else l
328 let sylSoundsSpanRest = sylTextLength - List.length sylSoundParts + 1
329 forM_ (sylSoundParts & List.zip (sylSoundsSpanRest : List.repeat 1)) \(sylSoundSpan, sylSoundPart) -> do
334 , "cell-" <> case syl of
335 Pron.Syl{sylSilent = True} -> "silent"
336 Pron.Syl{sylIndex = i} -> if i & even then "even" else "odd"
337 , if syl & Pron.sylDependsOnAfter then "cell-depends-on-after" else ""
338 , if syl & Pron.sylDependsOnBefore then "cell-depends-on-before" else ""
339 , if syl & Pron.sylDependsOnMeaning then "cell-depends-on-meaning" else ""
340 , if syl & Pron.sylSplit then "cell-split" else ""
343 [ "grid-column-end" := "span " <> show sylSoundSpan
346 sylSoundPart & HTML.toHtml
347 MatterPicture{..} -> do
349 ! classes ["matter-picture"]
350 ! styles ["height" := "2cm"]
352 unless (null matterPicture) do
354 ! styles matterPictureCSS
355 ! HA.title (matterPicture & toValue)
356 ! HA.src ("file://" <> dataPath </> "images" </> matterPicture & toValue)
357 x -> errorShow ("unsupported Matter" :: Text, x)
361 { pageOrientation = PageOrientationPortrait
365 { pageOrientation = PageOrientationLandscape
368 difficultyCSS :: Difficulties -> HTML.CSS
369 difficultyCSS diffs =
370 [ [ [ [".lang-" <> show lang <> " " <> ".script-" <> className uniScript] :=
371 [ "width" := difficultyCharWidth & HTML.toCSS
372 , "height" := difficultyCharHeight & HTML.toCSS
373 , "font-family" := difficultyFontFamily & HTML.toCSS
374 , "font-size" := difficultyFontSize & HTML.toCSS
375 , "font-variant" := difficultyFontVariant & HTML.toCSS
376 , "color" := difficultyColor & HTML.toCSS
377 , "line-height" := difficultyLineHeight & maybe "" HTML.toCSS
379 , [ ".lang-" <> show lang <> ".sentence"
381 := ["column-gap" := difficultyWordSpacing & HTML.toCSS]
383 | (uniScript, Difficulty{..}) <- blocks & toList
385 | (lang, blocks) <- diffs & Map.toList