1 {-# LANGUAGE OverloadedLists #-}
2 {-# LANGUAGE OverloadedStrings #-}
3 {-# LANGUAGE RankNTypes #-}
4 {-# LANGUAGE StandaloneDeriving #-}
6 module Language.Pronunciation where
8 import Control.Applicative (Alternative (..))
9 import Control.Monad.Combinators qualified as P
10 import Control.Monad.Trans.Class qualified as MT
11 import Control.Monad.Trans.State qualified as MT
12 import Data.List qualified as List
13 import Data.List.Zipper qualified as LZ
14 import Data.Map.Strict qualified as Map
15 import Data.Set qualified as Set
16 import Data.Text qualified as Text
17 import Data.Text.Short qualified as TextShort
18 import Data.Traversable (traverse)
20 import Paths_worksheets qualified as Self
21 import System.FilePath.Posix ((</>))
22 import System.FilePath.Posix qualified as File
23 import Text.Blaze.Html5.Attributes qualified as HA
24 import Text.Megaparsec qualified as P
25 import Worksheets.Utils.Char qualified as Char
26 import Worksheets.Utils.HTML (className, classes, styles, (!))
27 import Worksheets.Utils.HTML qualified as HTML
28 import Worksheets.Utils.IPA qualified as IPA
29 import Worksheets.Utils.Paper qualified as Paper
30 import Worksheets.Utils.Prelude
32 data Pronunciation = Pronunciation
33 { pronunciationIPABroad :: [IPA.Syllable []]
34 , pronunciationText :: Text
36 deriving (Eq, Ord, Show)
37 instance Semigroup Pronunciation where
40 { pronunciationIPABroad = pronunciationIPABroad x <> pronunciationIPABroad y
42 [pronunciationText x, pronunciationText y]
44 & Text.intercalate "."
46 instance IsList Pronunciation where
47 type Item Pronunciation = IPA.Syllable []
48 toList = pronunciationIPABroad
50 -- fromList :: HasCallStack => [Item Pronunciation] -> Pronunciation
53 { pronunciationIPABroad = ipa
54 , pronunciationText = ipa & foldMap (IPA.toIPA >>> maybe "" IPA.unIPA)
59 & mapButLast (IPA.WithSuprasegmentalFeature IPA.Break)
61 instance IsString Pronunciation where
64 { pronunciationIPABroad = ipa
65 , pronunciationText = ipa & foldMap (IPA.toIPA_ >>> IPA.unIPA)
71 & IPA.parseSyllables @[]
75 data PronunciationKey = PronunciationKey
76 { pronunciationKeyText :: Text
77 , pronunciationKeyPron :: Pronunciation
82 newtype Pronunciations = Pronunciations
83 { unPronunciations :: [(RuleLexemes, Pronunciation)]
86 deriving newtype (Show)
87 deriving newtype (Semigroup)
88 deriving newtype (Monoid)
89 joinPronunciations :: Pronunciations -> Pronunciations
90 joinPronunciations (Pronunciations ps) =
94 { pronunciationIPABroad = ipa
95 , pronunciationText = ipa & foldMap (IPA.toIPA >>> maybe "" IPA.unIPA)
100 ipa :: [IPA.Syllable []]
104 ( \(inp, Pronunciation{pronunciationIPABroad}) (suffix, l, acc) ->
105 case pronunciationIPABroad of
106 [] -> (inp <> suffix, IPA.Syllable [], acc)
107 [syl@(IPA.Syllable [])] -> (inp <> suffix, syl, acc)
108 [syl] -> (inp <> suffix, IPA.Syllable [], (syl <> l) : acc)
109 [sylL, sylR] -> (inp <> suffix, sylL, glueSyllableToTheRight sylR acc)
110 _ -> errorShow pronunciationIPABroad
118 , glueSyllableToTheRight l acc
119 & mapButLast (IPA.setSuprasegmentalFeatures [IPA.Break])
122 glueSyllableToTheRight ::
126 glueSyllableToTheRight x y =
130 yL : yR -> x <> yL : yR
131 instance IsList Pronunciations where
132 type Item Pronunciations = (RuleLexemes, Pronunciation)
133 toList = unPronunciations
134 fromList l = Pronunciations{unPronunciations = l & fromList}
137 instance IsString Pronunciations where
139 "" -> Pronunciations "" [IPA.Syllable [IPA.Zero]]
140 s -> Pronunciations (s & Text.pack) $
142 & IPA.parseSyllables @[]
143 & either errorShow id
145 data ExampleLiteral = ExampleLiteral
146 { exampleLiteralText :: ShortText
147 , exampleLiteralTags :: Set LiteralTag
148 , exampleLiteralMeaning :: ShortText
150 deriving (Eq, Ord, Show)
151 instance IsString ExampleLiteral where
154 { exampleLiteralText = s & fromString
155 , exampleLiteralTags = Set.empty
156 , exampleLiteralMeaning = ""
159 = LiteralTagOccurence
162 deriving (Eq, Ord, Show)
163 exampleLiteralsText :: [ExampleLiteral] -> ShortText
164 exampleLiteralsText ls = ls <&> exampleLiteralText & mconcat
168 { exampleLiteralText = "-"
169 , exampleLiteralTags = [LiteralTagMeta] & Set.fromList
170 , exampleLiteralMeaning = ""
172 occurence lit = lit{exampleLiteralTags = lit & exampleLiteralTags & Set.insert LiteralTagOccurence}
173 silent lit = lit{exampleLiteralTags = lit & exampleLiteralTags & Set.insert LiteralTagSilent}
175 type SyllableText = ShortText
176 type SyllableBroad = IPA.Syllable []
177 type SyllablesTable = Map SyllableText (Map SyllableText [([ExampleLiteral], SyllableBroad)])
183 deriving (Eq, Ord, Show)
185 = VariantDefinition Text
187 deriving (Eq, Ord, Show)
189 { -- , ruleStress :: Bool
190 -- , ruleDefinition :: Maybe Text
192 -- [ "e" := ["ɛ"] , "x" := ["g","z"] , "er" := ["ɛʁ"] , "cice" := ["sis"] ]
193 -- [ "exercice" := ["ɛg.zɛʁ.sis"]
195 rulePron :: Pronunciations
196 , ruleExamples :: Map InputLexemes Pronunciation
198 deriving (Eq, Ord, Show)
202 { rulePron = Pronunciations{unPronunciations = []}
203 , ruleExamples = mempty
209 RuleLexemes -> RuleLexemes
210 word = begining >>> ending
211 begining = after [LexemeBorder]
212 ending = before [LexemeBorder]
213 before ls r = RuleLexemes (unRuleLexemes r <> ls)
214 after ls r = RuleLexemes (ls <> unRuleLexemes r)
215 meaning r d = RuleLexemes (unRuleLexemes r <> [LexemeMeaning d])
217 type Table = Map RuleLexemes Rule
218 examples :: Table -> Map InputLexemes Pronunciation
221 | v <- tbl & Map.elems
223 & Map.unionsWith (\new old -> if new == old then new else errorShow (new, old))
226 { pronInput :: [Lexeme]
233 , sylDependsOnBefore :: Bool
234 , sylDependsOnAfter :: Bool
235 , sylDependsOnMeaning :: Bool
236 , sylSound :: Text -- [IPA.Syllable []]
243 addIndexes :: [[Either Char Pron]] -> [[Syl]]
247 go idx (prons : next) = List.reverse prons' : go idx' next
256 { sylText = Text.singleton c
257 , sylDependsOnAfter = False
258 , sylDependsOnBefore = False
259 , sylDependsOnMeaning = False
267 Right Pron{pronRule = Rule{rulePron = Pronunciations{unPronunciations = ps}}} ->
270 ( \(j, js) (t, Pronunciation{..}) ->
271 let sylText = t & unRuleLexemes & lexemesChars & Text.pack
272 in case pronunciationIPABroad of
274 | not (Text.null pronunciationText) ->
278 , sylSound = pronunciationText
279 , sylDependsOnBefore = t & unRuleLexemes & sylDependsOnBefore
280 , sylDependsOnAfter = t & unRuleLexemes & sylDependsOnAfter
281 , sylDependsOnMeaning = t & unRuleLexemes & sylDependsOnMeaning
289 | Text.null pronunciationText ->
295 , sylDependsOnBefore = t & unRuleLexemes & sylDependsOnBefore
296 , sylDependsOnAfter = t & unRuleLexemes & sylDependsOnAfter
297 , sylDependsOnMeaning = t & unRuleLexemes & sylDependsOnMeaning
304 j0@Syl{sylText = j0t} : jss -> j0{sylText = j0t <> sylText} : jss
307 | (syls & all IPA.isSilent) && Text.null pronunciationText ->
311 , sylDependsOnBefore = t & unRuleLexemes & sylDependsOnBefore
312 , sylDependsOnAfter = t & unRuleLexemes & sylDependsOnAfter
313 , sylDependsOnMeaning = t & unRuleLexemes & sylDependsOnMeaning
325 , sylDependsOnBefore = t & unRuleLexemes & sylDependsOnBefore
326 , sylDependsOnAfter = t & unRuleLexemes & sylDependsOnAfter
327 , sylDependsOnMeaning = t & unRuleLexemes & sylDependsOnMeaning
328 , sylSound = pronunciationText
339 , sylDependsOnBefore = t & unRuleLexemes & sylDependsOnBefore
340 , sylDependsOnAfter = t & unRuleLexemes & sylDependsOnAfter
341 , sylDependsOnMeaning = t & unRuleLexemes & sylDependsOnMeaning
342 , sylSound = pronunciationText
349 syls -> errorShow syls
354 List.reverse >>> \case
355 LexemeBorder : _ -> True
356 LexemeSilent : _ -> True
357 LexemeConsonant : _ -> True
358 LexemeDoubleConsonant : _ -> True
359 LexemeVowel : _ -> True
360 LexemeSemiVowel : _ -> True
364 LexemeBorder : _ -> True
365 LexemeSilent : _ -> True
366 LexemeConsonant : _ -> True
367 LexemeDoubleConsonant : _ -> True
368 LexemeVowel : _ -> True
369 LexemeSemiVowel : _ -> True
371 sylDependsOnMeaning =
372 List.reverse >>> \case
373 LexemeMeaning{} : _ -> True
378 withCapital :: [(RuleLexemes, Rule)] -> [(RuleLexemes, Rule)]
380 foldMap \(RuleLexemes pat, rul) ->
381 [ (RuleLexemes pat, rul)
383 ( RuleLexemes (withCapitalLexemes pat)
385 { rulePron = rul & rulePron & withCapitalPronunciations
391 >>> withCapitalLexemes
398 withCapitalPronunciations (Pronunciations []) = Pronunciations []
399 withCapitalPronunciations (Pronunciations ((t, p) : ps)) =
400 Pronunciations ((RuleLexemes $ withCapitalLexemes $ unRuleLexemes t, p) : ps)
401 withCapitalLexemes (LexemeChar x : xs) = LexemeChar (Char.toUpper x) : xs
402 withCapitalLexemes (x : xs) = x : withCapitalLexemes xs
403 withCapitalLexemes [] = []
405 lexemesChars :: [Lexeme] -> [Char]
416 (P.ParseErrorBundle Text ())
417 (P.ParseErrorBundle [Lexeme] ())
423 & either (Left . Left) \lexs ->
426 & either (Left . Right) Right
428 runParser :: Table -> [Lexeme] -> Either (P.ParseErrorBundle [Lexeme] ()) [Either Char Pron]
429 runParser tbl inp = inp & P.runParser (parser tbl) "input"
436 (P.ParseErrorBundle Text ())
437 (P.ParseErrorBundle [Lexeme] ())
440 parseLiterals rules inp =
443 ( \ExampleLiteral{..} ->
448 [ LexemeMeaning exampleLiteralMeaning
449 | exampleLiteralMeaning & TextShort.null & not
453 & either (Left . Left) \lexs ->
457 & either (Left . Right) Right
459 parser :: Table -> P.Parsec () [Lexeme] [Either Char Pron]
461 res <- P.many $ (Just . Right) <$> parseRules <|> parseChar
463 return $ res & catMaybes
465 -- Match one of the rules, trying longuest first
466 parseRules :: P.Parsec () [Lexeme] Pron
470 | r <- tbl & Map.toDescList
476 rulePat & unRuleLexemes <&> \case
477 LexemeChar c -> LexemeChar (c & Char.toUpper)
481 | (rulePat, curRule) <- tbl & Map.toDescList
484 parseRule (rulePat, curRule@Rule{..}) =
487 pat = rulePat & unRuleLexemes
488 patSep = (`List.elem` list [LexemeVowel, LexemeSemiVowel, LexemeConsonant, LexemeSilent])
489 -- (patEnd, patBegin) = pat & List.reverse & List.span patSep
490 patBegin = pat & List.dropWhileEnd patSep
491 patEnd = pat & List.reverse & List.takeWhile patSep & List.reverse
492 -- parse without the ending Lexeme{Vowel,SemiVowel,Consonant}
494 inpAfterBegin <- P.getInput
495 unless (List.null patEnd) do
496 inpWithAhead <- parseAhead
497 -- traceShowM ("inpWithAhead"::Text, inpWithAhead)
498 P.setInput inpWithAhead
499 P.chunk patEnd & void
500 -- insert the Lexeme{Vowel,SemiVowel,Consonant} from the output of the current rule
509 >>> pronunciationIPABroad
512 >>> maybe [] (IPA.syllableToSegments >>> List.reverse >>> lexemeHeadSound)
514 P.setInput $ lastSound <> inpAfterBegin
515 return Pron{pronInput = pat, pronRule = curRule}
516 parseChar :: P.Parsec () [Lexeme] (Maybe (Either Char Pron))
518 P.anySingle <&> \case
519 LexemeChar c -> Just $ Left c
521 parseAhead :: P.Parsec () [Lexeme] [Lexeme]
523 nextStep <- P.observing $ Right <$> parseRules <|> Left <$> P.anySingle
524 -- traceShowM ("nextStep"::Text, nextStep & either (\err -> Left ()) Right)
526 Right (Right Pron{pronInput, pronRule}) -> do
535 >>> pronunciationIPABroad
537 >>> maybe [] (IPA.syllableToSegments >>> lexemeHeadSound)
540 return $ x <> pronInput <> inp
541 Right (Left lex) -> do
542 parseAhead <&> (lex :)
544 lexemeHeadSound :: [_] -> [Lexeme]
546 headMaybe >>> fmap IPA.dropSegmentalFeatures >>> \case
547 Just IPA.Zero{} -> [LexemeSilent]
548 Just IPA.Vowel{} -> [LexemeVowel]
549 Just (IPA.Consonant consonant) -> do
551 IPA.Pulmonic _phonation _place IPA.Approximant -> [LexemeSemiVowel]
552 IPA.Ejective _place IPA.Approximant -> [LexemeSemiVowel]
553 _ -> [LexemeConsonant]
556 runLexer :: Text -> Either (P.ParseErrorBundle Text ()) [Lexeme]
557 runLexer inp = inp & P.runParser lexer "input"
559 exampleLiteralsLexemes :: [ExampleLiteral] -> [Lexeme]
560 exampleLiteralsLexemes ls =
561 ls & foldMap \ExampleLiteral{..} ->
562 unRuleLexemes (fromString (TextShort.unpack exampleLiteralText))
563 <> [ LexemeMeaning exampleLiteralMeaning
566 lexer :: P.Parsec () Text [Lexeme]
571 [ P.takeWhile1P Nothing Char.isSpace >>= \cs ->
572 return [LexemeChar c | c <- cs & Text.unpack]
574 cs <- P.takeWhile1P Nothing Char.isLetter
575 mean <- (<|> return []) $ P.try do
577 m <- P.takeWhile1P Nothing (/= '}')
579 return [LexemeMeaning (TextShort.fromText m)]
582 : [LexemeChar c | c <- cs & Text.unpack]
585 , P.takeWhile1P Nothing Char.isNumber >>= \cs ->
586 return (LexemeBorder : ([LexemeChar c | c <- cs & Text.unpack] <> [LexemeBorder]))
587 , P.satisfy Char.isSymbol >>= \c ->
588 return [LexemeChar c]
589 , P.satisfy Char.isSeparator >>= \c ->
590 return [LexemeChar c]
591 , P.satisfy Char.isMark >>= \c ->
592 return [LexemeChar c]
593 , P.satisfy Char.isPunctuation >>= \c ->
594 return [LexemeChar c]
599 words :: [Either Char Pron] -> [[Either Char Pron]]
601 words prons = word0 : words next
603 (word0, rest) = prons & List.span (isSep >>> not)
604 (_sep, next) = rest & List.span isSep
606 Left c | c & Char.isSpace -> True
610 case statePats st & Map.lookup k of
612 Just (PatTree pats) ->
613 loop st {statePats = pats, stateBuffer = k : stateBuffer st}
615 loop st { statePats = initPats
617 , stateInput = stateInput st
619 { inpPats = stateBuffer st & List.reverse
620 , inpPronunciations = end
627 parse :: PatTree -> Text -> [Inp]
628 parse initPats input =
629 let inpZip = input & Text.unpack & fmap charToInp & LZ.fromList in
630 runInp [] initPats inpZip & LZ.toList
632 charToInp :: Char -> Inp
634 { inpPats = [PosNext (PatternChar c)]
635 , inpPronunciations = []
637 runInp :: [Pos] -> PatTree -> LZ.Zipper Inp -> LZ.Zipper Inp
639 traceShow ("runInp"::Text, ("oks"::Text) := oks, ("cur"::Text) := LZ.safeCursor inp) $
645 { inpPats = oks & List.reverse
646 , inpPronunciations = end
651 -- the pattern may go on
652 case inp & LZ.safeCursor of
655 & runPat [] oks [PosNext PatternLexicalBorder] pats
658 & runPat [] oks (inpPats cur & List.sort) pats
660 runPat :: [Pos] -> [Pos] -> [Pos] -> Map Pos PatTree -> LZ.Zipper Inp -> LZ.Zipper Inp
661 runPat kos oks todos pats inp =
662 traceShow ( "runPat"::Text
663 , ("kos"::Text) := kos
664 , ("oks"::Text) := oks
665 , ("todos"::Text) := todos
666 , ("cur"::Text) :=LZ.safeCursor inp
669 [] | LZ.endp inp -> inp
670 & runInp kos (PatEnd [])
671 & runInp oks (PatEnd [])
673 -- nothing left to advance the pattern
674 --traceShow ("runPat/[]"::Text) $
676 & (if null kos then id else runInp kos (PatEnd []))
677 & runInp oks (PatTree pats)
679 case pats & Map.lookup k of
682 --traceShow ("runPat/End"::Text) $
685 & (if null kos then id else runInp kos (PatEnd []))
686 & runInp (k:oks) (PatEnd end)
687 -- the pattern advances
688 Just (PatTree nextPats) ->
689 --traceShow ("runPat/Node"::Text) $
690 inp & runPat kos (k:oks) ks nextPats
691 -- the pattern does not advance
693 inp & runPat (k:kos) oks ks pats
701 | LexemeDoubleConsonant
703 | LexemeMeaning ShortText
704 | -- | `LexemeChar` is last to have priority when using `Map.toDescList`
706 deriving (Eq, Ord, Show)
711 -- deriving (Eq, Ord, Show)
714 newtype Lexemes = Lexemes { unLexemes :: [Lexeme] }
715 deriving (Eq, Ord, Show)
716 instance P.Stream Lexemes where
717 type Token Lexemes = Lexeme
718 type Tokens Lexemes = Lexemes
719 tokensToChunk _px = Lexemes
720 chunkToTokens _px = unLexemes
721 chunkLength _px = unLexemes >>> List.length
722 chunkEmpty _px = unLexemes >>> List.null
723 take1_ = unLexemes >>> P.take1_ >>> coerce
724 takeN_ n = unLexemes >>> P.takeN_ n >>> coerce
725 takeWhile_ p = unLexemes >>> P.takeWhile_ p >>> coerce
727 instance IsString Lexemes where
734 ((`appEndo` []) >>> Lexemes)
737 newtype RuleLexemes = RuleLexemes {unRuleLexemes :: [Lexeme]}
738 deriving (Eq, Ord, Show)
739 instance HasTypeDefault RuleLexemes where
740 typeDefault = RuleLexemes typeDefault
741 instance Semigroup RuleLexemes where
742 RuleLexemes x <> RuleLexemes y = RuleLexemes (x <> y)
743 instance Monoid RuleLexemes where
744 mempty = RuleLexemes mempty
745 instance IsList RuleLexemes where
746 type Item RuleLexemes = Lexeme
747 fromList = RuleLexemes
748 toList = unRuleLexemes
749 instance IsString RuleLexemes where
756 ( List.dropWhileEnd (== LexemeBorder)
757 >>> List.dropWhile (== LexemeBorder)
761 newtype InputLexemes = InputLexemes {unInputLexemes :: [Lexeme]}
762 deriving (Eq, Ord, Show)
763 instance HasTypeDefault InputLexemes where
764 typeDefault = InputLexemes typeDefault
765 instance Semigroup InputLexemes where
766 InputLexemes x <> InputLexemes y = InputLexemes (x <> y)
767 instance Monoid InputLexemes where
768 mempty = InputLexemes mempty
769 instance IsList InputLexemes where
770 type Item InputLexemes = Lexeme
771 fromList = InputLexemes
772 toList = unInputLexemes
773 instance IsString InputLexemes where
778 & either errorShow InputLexemes
780 instance P.ShowErrorComponent () where
781 showErrorComponent = show
782 errorComponentLen _ = 2
783 instance P.VisualStream [Lexeme] where
785 tokensLength _s xs = xs <&> (show >>> List.length) & sum
786 instance P.TraversableStream [Lexeme] where
787 reachOffset off pos = (Nothing, pos{P.pstateOffset = P.pstateOffset pos + off})
792 | LexemeTagPunctuation
796 | LexemeTagDefinition
798 deriving (Eq, Ord, Show)
799 deriving instance Ord (IPA.Syllable [])
800 deriving instance Ord IPA.SuprasegmentalFeature
801 deriving instance Ord IPA.SegmentalFeature
802 deriving instance Ord IPA.Sibilance
803 deriving instance Ord IPA.Manner
804 deriving instance Ord IPA.Phonation
805 deriving instance Ord IPA.Roundedness
806 deriving instance Ord IPA.Height
807 deriving instance Ord IPA.Vowel
808 deriving instance Ord IPA.Consonant
809 deriving instance Ord IPA.Segment
812 tableToMatch :: Table -> [Lexeme] -> [Pronunciations]
813 tableToMatch tbl = loop
815 loop prevBorder = \case
818 | (trans, transMach) <- chunk & chunkMachine & machineAlts & Map.toList
819 , let matchingLength = transMatchingLength input trans
820 , 0 < matchingLength || not (isTransConsume trans)
821 , let (inputRead, inputRest) = input & Text.splitAt matchingLength
823 & Map.fromListWith (\new old -> old)
828 tableHtml :: Table -> IO HTML.Html
830 dataPath <- Self.getDataDir <&> File.normalise
831 let title :: String = "LexerDict"
832 let pageOrientation = Paper.PageOrientationPortrait
833 let pageSize = Paper.PageSizeA4
834 let partLangue = LangueFrançais
838 HTML.title $ title & HTML.toHtml
840 ( [ "styles/Paper.css"
841 , "styles/French/Lexer.css"
842 , "styles/Rosetta/Reading.css"
848 ! HA.rel "stylesheet"
849 ! HA.type_ "text/css"
850 ! HA.href (dataPath </> cssFile & HTML.toValue)
851 HTML.styleCSS $ HTML.cssPrintPage pageOrientation pageSize
852 -- HTML.styleCSS $ pageDifficulties & difficultyCSS
854 ! classes ["A4", "french-lexer"]
857 let rulesChunks = tbl & Map.toList & chunksOf 50
858 forM_ rulesChunks \rules ->
866 forM_ (rules & List.zip [1 :: Int ..]) \(ruleIndex, (rulePat, Rule{..})) -> do
871 , if even ruleIndex then "even" else "odd"
880 , "lang-" <> className partLangue
884 -- "grid-template-columns" :=
885 -- (0.5 & cm & HTML.toCSS)
886 -- & List.replicate lexerDictMaxKeyLength
890 forM_ (["model"] :: [String]) \rowKind -> do
891 forM_ (rulePat & unRuleLexemes) \ruleChar -> do
892 -- let uniScript = cellToken & tokenMeta & snd & fromMaybe (UnicodeBlockLatin UnicodeBlockLatin_Basic)
899 -- , "script-" <> className uniScript
906 [ "dict-pronunciation"
909 -- HTML.span ! classes ["arrow"] $ "→"
913 all (snd >>> pronunciationIPABroad >>> all IPA.isSilent) -> True
915 Pronunciations{unPronunciations = is} ->
917 & foldMap (snd >>> pronunciationIPABroad >>> foldMap (IPA.toIPA_ >>> IPA.transcribe IPA.Phonemic))
924 -- HTML.span ! classes ["arrow"] $ "→"
925 forM_ (ruleExamples & Map.toList) \(_inp, Pronunciation{..}) -> do
930 case pronunciationIPABroad of
931 [] -> pronunciationText & HTML.toHtml
932 _ -> pronunciationIPABroad & foldMap (IPA.toIPA_ >>> IPA.transcribe IPA.Phonemic) & HTML.toHtml