From ef96dd27ddb7ba9b09f45fe2f0526a616156e9bc Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Fri, 17 Nov 2023 17:36:55 +1100 Subject: [PATCH 01/26] Allow backreferences to occur in the target (with limitations) --- Documentation.md | 6 ++++++ src/Brassica/SoundChange/Parse.hs | 6 ++---- src/Brassica/SoundChange/Types.hs | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/Documentation.md b/Documentation.md index e04c2bb..07fc9d3 100644 --- a/Documentation.md +++ b/Documentation.md @@ -603,6 +603,12 @@ This may be written as: └──────────────────────┘ ``` +Backreferences in the environment have a similar meaning to backreferences in the target. +However, they have a limitation: + categories after the target are counted separately from categories before the target. +Thus, it is invalid to use an environment such as `V _ @1 V`, since this backreference stretches across the target. +This limitation should be lifted in a future version of Brassica. + ### Sporadic rules and multiple results Sometimes we might want a sound change rule with more than one possible output. diff --git a/src/Brassica/SoundChange/Parse.hs b/src/Brassica/SoundChange/Parse.hs index a04ef35..250dc8b 100644 --- a/src/Brassica/SoundChange/Parse.hs +++ b/src/Brassica/SoundChange/Parse.hs @@ -184,10 +184,7 @@ parseKleene l = (Kleene l <$ symbol "*") <|> pure l parseMultiple :: Parser (Lexeme 'Replacement) parseMultiple = Multiple <$> (symbol "@?" *> parseCategory') -parseBackreference - :: forall a. - (OneOf a 'Target 'Replacement, ParseLexeme a) - => Parser (Lexeme a) +parseBackreference :: forall a. ParseLexeme a => Parser (Lexeme a) parseBackreference = Backreference <$> (symbol "@" *> nonzero) @@ -227,6 +224,7 @@ instance ParseLexeme 'Env where , parseOptional , parseGeminate , parseWildcard + , parseBackreference , parseGraphemeOrCategory ] >>= parseKleene diff --git a/src/Brassica/SoundChange/Types.hs b/src/Brassica/SoundChange/Types.hs index 9220bd7..c757710 100644 --- a/src/Brassica/SoundChange/Types.hs +++ b/src/Brassica/SoundChange/Types.hs @@ -126,7 +126,7 @@ data Lexeme (a :: LexemeType) where -- | In Brassica sound-change syntax, specified as @~@ Discard :: Lexeme 'Replacement -- | In Brassica sound-change syntax, specified as \@i before a category - Backreference :: OneOf a 'Target 'Replacement => Int -> [Grapheme] -> Lexeme a + Backreference :: Int -> [Grapheme] -> Lexeme a -- | In Brassica sound-change syntax, specified as \@? before a category Multiple :: [Grapheme] -> Lexeme 'Replacement From d958d61fa00b84788543b16e8c1fb784b2e667f9 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Sat, 9 Dec 2023 19:54:03 +1100 Subject: [PATCH 02/26] Allow lexeme sequences in categories Closes #2. --- ChangeLog.md | 4 ++ Documentation.md | 7 +++ src/Brassica/SoundChange/Apply/Internal.hs | 45 ++++++++-------- src/Brassica/SoundChange/Category.hs | 22 ++++---- src/Brassica/SoundChange/Parse.hs | 61 +++++++++++++++------- src/Brassica/SoundChange/Types.hs | 20 +++++-- 6 files changed, 104 insertions(+), 55 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 58b5e1c..eb68346 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,9 @@ # Brassica changelog +## Unreleased changes + +- Allow lexeme sequences in categories using `{…}` syntax + ## v0.1.1 - Rewrote executables with a client/server architecture for better Windows support. diff --git a/Documentation.md b/Documentation.md index 07fc9d3..d3e2044 100644 --- a/Documentation.md +++ b/Documentation.md @@ -150,6 +150,13 @@ Thus, to delete ⟨ʔ⟩ before a stop or the end of a word, the following rule ʔ / / _ [p t k b d g #] ``` +Sometimes you might want to match a sequence of zero or more graphemes. +Do this in a category by surrounding them with curly brackets `{`/`}`, as in: +``` +[eː oː] / [{j ə} {w ə}] +[{ŋ g} ŋ] → [ŋ {}] / _# +``` + You can also use multiple categories in the replacement. In this case each is matched up one-to-one with a category in the target: the first in the replacement with the first in the target, diff --git a/src/Brassica/SoundChange/Apply/Internal.hs b/src/Brassica/SoundChange/Apply/Internal.hs index 09ebde1..b252f1a 100644 --- a/src/Brassica/SoundChange/Apply/Internal.hs +++ b/src/Brassica/SoundChange/Apply/Internal.hs @@ -58,7 +58,6 @@ module Brassica.SoundChange.Apply.Internal import Control.Applicative ((<|>)) import Control.Category ((>>>)) import Data.Containers.ListUtils (nubOrd) -import Data.Function ((&)) import Data.Functor ((<&>)) import Data.Maybe (maybeToList, fromMaybe, listToMaybe, mapMaybe) import GHC.Generics (Generic) @@ -128,10 +127,14 @@ appendGrapheme out g = modifyMatchedGraphemes (++[g]) out instance Semigroup MatchOutput where (MatchOutput a1 b1 c1) <> (MatchOutput a2 b2 c2) = MatchOutput (a1++a2) (b1++b2) (c1++c2) +zipWith' :: [a] -> [b] -> (a -> b -> c) -> [c] +zipWith' xs ys f = zipWith f xs ys + -- | Match a single 'Lexeme' against a 'MultiZipper', and advance the -- 'MultiZipper' past the match. For each match found, returns the -- 'MatchOutput' tupled with the updated 'MultiZipper'. -match :: OneOf a 'Target 'Env +match :: forall a t. + OneOf a 'Target 'Env => MatchOutput -- ^ The previous 'MatchOutput' -> Maybe Grapheme -- ^ The previously-matched grapheme, if any. (Used to match a 'Geminate'.) -> Lexeme a -- ^ The lexeme to match. @@ -152,26 +155,21 @@ match out prev k@(Kleene l) mz = case match out prev l mz of [] -> error "match: Kleene should never fail" r' -> r' match out _ (Grapheme g) mz = (out <> MatchOutput [] [] [g],) <$> maybeToList (matchGrapheme g mz) -match out _ (Category gs) mz = - gs - -- Attempt to match each option in category... - & fmap (matchCategoryEl mz) - -- ...get the index of each match... - & zipWith (\i m -> fmap (i,) m) [0..] - -- ...and take all matches - & (>>= maybeToList) - & fmap (\(i, (g, mz')) -> (out <> MatchOutput [i] [] g, mz')) +match out prev (Category gs) mz = + concat $ zipWith' gs [0..] $ \e i -> + first (<> MatchOutput [i] [] []) <$> + case e of + Left g -> match out prev (Grapheme g :: Lexeme a) mz + Right ls -> matchMany out prev ls mz match out prev Geminate mz = case prev of Nothing -> [] Just prev' -> (out <> MatchOutput [] [] [prev'],) <$> maybeToList (matchGrapheme prev' mz) -match out _prev (Backreference i gs) mz = maybeToList $ do - catIx <- matchedCatIxs out !? (i-1) - g <- gs !? catIx - (matched, mz') <- matchCategoryEl mz g - pure (modifyMatchedGraphemes (++matched) out, mz') - -matchCategoryEl :: MultiZipper t Grapheme -> Grapheme -> Maybe ([Grapheme], MultiZipper t Grapheme) -matchCategoryEl mz g = ([g],) <$> matchGrapheme g mz +match out prev (Backreference i gs) mz = do + e <- maybeToList $ + (gs !?) =<< matchedCatIxs out !? (i-1) + case e of + Left g -> match out prev (Grapheme g :: Lexeme a) mz + Right ls -> matchMany out prev ls mz matchGrapheme :: Grapheme -> MultiZipper t Grapheme -> Maybe (MultiZipper t Grapheme) matchGrapheme g = matchGraphemeP (==g) @@ -269,14 +267,17 @@ mkReplacement out = \ls -> fmap (fst . snd) . go startIxs ls . (,Nothing) -> Maybe Grapheme -> [(ReplacementIndices, (MultiZipper t Grapheme, Maybe Grapheme))] replaceLex ixs (Grapheme g) mz _prev = [(ixs, (insert g mz, Just g))] - replaceLex ixs (Category gs) mz _prev = + replaceLex ixs (Category gs) mz prev = case advanceCategory ixs numCatsMatched of (CategoryNumber ci, ixs') -> let i = matchedCatIxs out !! ci in case gs !? i of - Just g -> [(ixs', (insert g mz, Just g))] + Just (Left g) -> [(ixs', (insert g mz, Just g))] + Just (Right ls) -> go ixs' ls (mz, prev) Nothing -> [(ixs', (insert (GMulti "\xfffd") mz, Nothing))] -- Unicode replacement character - (Nondeterministic, ixs') -> gs <&> \g -> (ixs', (insert g mz, Just g)) + (Nondeterministic, ixs') -> gs >>= \case + Left g -> [(ixs', (insert g mz, Just g))] + Right ls -> go ixs' ls (mz, prev) replaceLex ixs (Optional ls) mz prev = let (co, ixs') = advanceOptional ixs in case matchedOptionals out !? co of diff --git a/src/Brassica/SoundChange/Category.hs b/src/Brassica/SoundChange/Category.hs index 771dfd6..5df3ed5 100644 --- a/src/Brassica/SoundChange/Category.hs +++ b/src/Brassica/SoundChange/Category.hs @@ -11,19 +11,20 @@ module Brassica.SoundChange.Category -- * Category expansion , Categories , Brassica.SoundChange.Category.lookup - , mapCategories + , mapCrossrefs , expand -- * Obtaining values , bake , values ) where +import Data.Bifunctor (first) import Data.Coerce +import Data.Containers.ListUtils (nubOrd) import Data.List (intersect) import Data.Maybe (fromMaybe) import qualified Data.Map.Strict as M -import Data.Containers.ListUtils (nubOrd) -- | Type-level tag for 'Category'. When parsing a category definition -- from a string, usually categories will refer to other @@ -59,15 +60,15 @@ data Category (s :: CategoryState) a -- | A map from names to the (expanded) categories they -- reference. Used to resolve cross-references between categories. -type Categories a = M.Map a (Category 'Expanded a) +type Categories a b = M.Map a (Category 'Expanded (Either a b)) -- | @Data.Map.Strict.'Data.Map.Strict.lookup'@, specialised to 'Categories'. -lookup :: Ord a => a -> Categories a -> Maybe (Category 'Expanded a) +lookup :: Ord a => a -> Categories a b -> Maybe (Category 'Expanded (Either a b)) lookup = M.lookup --- | Map a function over all the values in a set of 'Categories'. -mapCategories :: Ord b => (a -> b) -> Categories a -> Categories b -mapCategories f = M.map (fmap f) . M.mapKeys f +-- | Map a function over all the cross-references in a set of 'Categories'. +mapCrossrefs :: Ord b => (a -> b) -> Categories a c -> Categories b c +mapCrossrefs f = M.map (fmap $ first f) . M.mapKeys f -- | Given a list of values, return a 'Category' which matches only -- those values. (This is a simple wrapper around 'Node' and @@ -77,9 +78,10 @@ categorise = UnionOf . fmap Node -- | Expand an 'Unexpanded' category by inlining its references. The -- references should only be to categories in the given 'Categories'. -expand :: Ord a => Categories a -> Category 'Unexpanded a -> Category 'Expanded a +expand :: Ord a => Categories a b -> Category 'Unexpanded (Either a b) -> Category 'Expanded (Either a b) expand _ Empty = Empty -expand cs n@(Node a) = fromMaybe (coerce n) $ M.lookup a cs +expand cs n@(Node (Left a)) = fromMaybe (coerce n) $ M.lookup a cs +expand _ (Node (Right b)) = Node (Right b) expand cs (UnionOf u) = UnionOf $ expand cs <$> u expand cs (Intersect a b) = Intersect (expand cs a) (expand cs b) expand cs (Subtract a b) = Subtract (expand cs a) (expand cs b) @@ -100,7 +102,7 @@ bake (Subtract a b) = bake a `difference` bake b -- 'Intersect'ed or 'Subtract'ed out: e.g. given 'Categories' -- including @[a b -a]@, this will return a list including -- @["a","b"]@, not just @["b"]@. -values :: Ord a => Categories a -> [a] +values :: (Ord a, Ord b) => Categories a b -> [Either a b] values = nubOrd . concatMap go . M.elems where go Empty = [] diff --git a/src/Brassica/SoundChange/Parse.hs b/src/Brassica/SoundChange/Parse.hs index 6b5096b..5d69d23 100644 --- a/src/Brassica/SoundChange/Parse.hs +++ b/src/Brassica/SoundChange/Parse.hs @@ -15,9 +15,10 @@ module Brassica.SoundChange.Parse ) where import Data.Char (isSpace) +import Data.Either (isRight) import Data.Foldable (asum) import Data.List (transpose) -import Data.Maybe (isNothing, isJust, fromJust) +import Data.Maybe (isNothing, isJust, fromJust, mapMaybe) import Data.Void (Void) import Control.Applicative.Permutations @@ -32,7 +33,7 @@ import Brassica.SoundChange.Types import qualified Brassica.SoundChange.Category as C newtype Config = Config - { categories :: C.Categories Grapheme + { categories :: C.Categories Grapheme [Lexeme 'AnyPart] } type Parser = ParsecT Void String (State Config) @@ -84,12 +85,12 @@ parseGrapheme = lexeme $ parseBoundary <|> parseMulti parseGrapheme' :: Parser Grapheme parseGrapheme' = lexeme $ GMulti <$> takeWhile1P Nothing (not . ((||) <$> isSpace <*> (=='='))) -data CategoryModification - = Union Grapheme - | Intersect Grapheme - | Subtract Grapheme +data CategoryModification a + = Union (Either Grapheme [Lexeme a]) + | Intersect (Either Grapheme [Lexeme a]) + | Subtract (Either Grapheme [Lexeme a]) -parseGraphemeOrCategory :: ParseLexeme a => Parser (Lexeme a) +parseGraphemeOrCategory :: Parser (Lexeme a) parseGraphemeOrCategory = do (g, isntCat) <- parseGrapheme if isntCat @@ -98,19 +99,20 @@ parseGraphemeOrCategory = do cats <- gets categories return $ case C.lookup g cats of Nothing -> Grapheme g - Just c -> Category $ C.bake c + Just c -> Category $ (fmap.fmap.fmap) generalise $ C.bake c parseCategory :: ParseLexeme a => Parser (Lexeme a) parseCategory = Category <$> parseCategory' -parseCategory' :: Parser [Grapheme] +parseCategory' :: ParseLexeme a => Parser [Either Grapheme [Lexeme a]] parseCategory' = do mods <- symbol "[" *> someTill parseCategoryModification (symbol "]") cats <- gets categories return $ C.bake $ - C.expand cats (toCategory mods) + C.expand ((fmap.fmap.fmap.fmap) generalise cats) (toCategory mods) -parseCategoryStandalone :: Parser (Grapheme, C.Category 'C.Expanded Grapheme) +parseCategoryStandalone + :: Parser (Grapheme, C.Category 'C.Expanded (Either Grapheme [Lexeme 'AnyPart])) parseCategoryStandalone = do g <- parseGrapheme' _ <- symbol "=" @@ -129,7 +131,9 @@ categoriesDeclParse = do _ <- some $ parseFeature <|> parseCategoryDecl _ <- symbol "end" <* scn Config catsNew <- get - return $ CategoriesDecl (C.values catsNew) + return $ CategoriesDecl $ + mapMaybe (\case Left g -> Just g; _ -> Nothing) $ + C.values catsNew where parseFeature = do _ <- symbol "feature" @@ -138,9 +142,14 @@ categoriesDeclParse = do cats <- gets categories let plainCat = C.expand cats $ toCategory modsPlain plain = C.bake plainCat + when (any isRight plain) $ fail "Cannot use {…} as a base grapheme" modifiedCats <- some (symbol "/" *> parseCategoryStandalone) <* scn let modified = C.bake . snd <$> modifiedCats - syns = zipWith (\a b -> (a, C.UnionOf [C.Node a, C.categorise b])) plain $ transpose modified + syns :: [(Grapheme, C.Category 'C.Expanded (Either Grapheme [Lexeme 'AnyPart]))] + syns = + zipWith (\a b -> (a, C.UnionOf [C.Node (Left a), C.categorise b])) + (fromLeft' <$> plain) + (transpose modified) modify $ \(Config cs) -> Config $ M.unions [ M.fromList syns , M.fromList modifiedCats @@ -153,15 +162,21 @@ categoriesDeclParse = do (k, c) <- try parseCategoryStandalone <* scn modify $ \(Config cs) -> Config (M.insert k c cs) -parseCategoryModification :: Parser CategoryModification -parseCategoryModification = parsePrefix <*> (fst <$> parseGrapheme) + fromLeft' :: Either a b -> a + fromLeft' (Left a) = a + fromLeft' _ = error "fromLeft': unexpected case!" + +parseCategoryModification :: ParseLexeme a => Parser (CategoryModification a) +parseCategoryModification = parsePrefix <*> + ( (Right <$> (symbol "{" *> manyTill parseLexeme (symbol "}"))) + <|> (Left . fst <$> parseGrapheme)) where parsePrefix = (Intersect <$ char '+') <|> (Subtract <$ char '-') <|> pure Union -toCategory :: [CategoryModification] -> C.Category 'C.Unexpanded Grapheme +toCategory :: [CategoryModification a] -> C.Category 'C.Unexpanded (Either Grapheme [Lexeme a]) toCategory = go C.Empty where go c [] = c @@ -198,7 +213,7 @@ parseBackreference = <$> (symbol "@" *> nonzero) <*> (parseCategory' <|> parseGraphemeCategory) where - parseGraphemeCategory :: Parser [Grapheme] + parseGraphemeCategory :: Parser [Either Grapheme [Lexeme a]] parseGraphemeCategory = label "category" $ try $ (parseGraphemeOrCategory @a) >>= \case Category gs -> pure gs @@ -236,6 +251,13 @@ instance ParseLexeme 'Env where , parseGraphemeOrCategory ] >>= parseKleene +instance ParseLexeme 'AnyPart where + parseLexeme = asum + [ parseCategory + , parseOptional + , parseGraphemeOrCategory + ] + parseLexemes :: ParseLexeme a => Parser [Lexeme a] parseLexemes = many parseLexeme @@ -287,7 +309,10 @@ parseRule = parseRuleWithCategories M.empty -- | Same as 'parseRule', but also allows passing in some predefined -- categories to substitute. -parseRuleWithCategories :: C.Categories Grapheme -> String -> Either (ParseErrorBundle String Void) Rule +parseRuleWithCategories + :: C.Categories Grapheme [Lexeme 'AnyPart] + -> String + -> Either (ParseErrorBundle String Void) Rule parseRuleWithCategories cs s = flip evalState (Config cs) $ runParserT (scn *> ruleParser <* eof) "" s -- | Parse a list of 'SoundChanges'. diff --git a/src/Brassica/SoundChange/Types.hs b/src/Brassica/SoundChange/Types.hs index c757710..402da2f 100644 --- a/src/Brassica/SoundChange/Types.hs +++ b/src/Brassica/SoundChange/Types.hs @@ -28,6 +28,7 @@ module Brassica.SoundChange.Types , Lexeme(..) , pattern Boundary , LexemeType(..) + , generalise -- * Rules , Rule(..) , Environment @@ -100,8 +101,8 @@ concatWithBoundary = go . removeBoundaries GBoundary -> "#" -- | The part of a 'Rule' in which a 'Lexeme' may occur: either the --- target, the replacement or the environment. -data LexemeType = Target | Replacement | Env +-- target, the replacement or the environment, or in any of those. +data LexemeType = Target | Replacement | Env | AnyPart -- | A 'Lexeme' is the smallest part of a sound change. Both matches -- and replacements are made up of 'Lexeme's: the phantom type @@ -112,7 +113,7 @@ data Lexeme (a :: LexemeType) where -- or a word boundary specified as @#@ Grapheme :: Grapheme -> Lexeme a -- | In Brassica sound-change syntax, delimited by square brackets - Category :: [Grapheme] -> Lexeme a + Category :: [Either Grapheme [Lexeme a]] -> Lexeme a -- | In Brassica sound-change syntax, delimited by parentheses Optional :: [Lexeme a] -> Lexeme a -- | In Brassica sound-change syntax, specified as @\@ @@ -126,15 +127,24 @@ data Lexeme (a :: LexemeType) where -- | In Brassica sound-change syntax, specified as @~@ Discard :: Lexeme 'Replacement -- | In Brassica sound-change syntax, specified as \@i before a category - Backreference :: Int -> [Grapheme] -> Lexeme a + Backreference :: Int -> [Either Grapheme [Lexeme a]] -> Lexeme a -- | In Brassica sound-change syntax, specified as \@? before a category - Multiple :: [Grapheme] -> Lexeme 'Replacement + Multiple :: [Either Grapheme [Lexeme 'Replacement]] -> Lexeme 'Replacement + +generalise :: Lexeme 'AnyPart -> Lexeme a +generalise (Grapheme g) = Grapheme g +generalise (Category es) = Category $ (fmap.fmap.fmap) generalise es +generalise (Optional ls) = Optional $ generalise <$> ls +generalise Geminate = Geminate +generalise (Backreference i es) = Backreference i $ (fmap.fmap.fmap) generalise es -- | A 'Lexeme' matching a single word boundary, specified as @#@ in Brassica syntax. pattern Boundary :: Lexeme a pattern Boundary = Grapheme GBoundary deriving instance Show (Lexeme a) +deriving instance Eq (Lexeme a) +deriving instance Ord (Lexeme a) instance NFData (Lexeme a) where rnf (Grapheme g) = rnf g From fdcd7ac63e8cee5377c6af06c934147c6544416d Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Fri, 15 Dec 2023 14:33:22 +1100 Subject: [PATCH 03/26] Refactor: move category expansion to outside parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There are now two representations for sound changes: the immediate result after parsing, and the result after all categories have been inlined and expanded to lists of graphemes. This greatly simplifies the code (since there’s now better separation of concerns), and should allow it to become more flexible in the future. --- brassica.cabal | 4 +- cli/Main.hs | 8 +- cli/Server.hs | 4 + .../src/BrassicaInterop.hs | 5 + src/Brassica/SoundChange/Apply/Internal.hs | 78 +++--- src/Brassica/SoundChange/Category.hs | 227 +++++++++------- src/Brassica/SoundChange/Frontend/Internal.hs | 59 +++-- src/Brassica/SoundChange/Parse.hs | 243 ++++++------------ src/Brassica/SoundChange/Tokenise.hs | 6 +- src/Brassica/SoundChange/Types.hs | 176 +++++++++---- test/Spec.hs | 12 +- test/proto21e-log.golden | 2 +- 12 files changed, 457 insertions(+), 367 deletions(-) diff --git a/brassica.cabal b/brassica.cabal index 760821f..f9a737d 100644 --- a/brassica.cabal +++ b/brassica.cabal @@ -45,10 +45,10 @@ library , containers >=0.6 && <0.7 , deepseq >=1.4 && <1.5 , megaparsec >=8.0 && <9.3 - , mtl >=2.2 && <2.3 + , mtl >=2.2 && <2.4 , parser-combinators >=1.2 && <1.3 , split >=0.2 && <0.3 - , transformers >=0.5 && <0.6 + , transformers >=0.5 && <0.7 default-language: Haskell2010 diff --git a/cli/Main.hs b/cli/Main.hs index d13ee42..a729366 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -83,7 +83,7 @@ data Options = Options processWords :: (MonadIO m, MonadThrow m) => Bool -- split into lines? - -> SoundChanges + -> SoundChanges CategorySpec Directive -> InputLexiconFormat -> ApplicationMode -> ConduitT B.ByteString B.ByteString m () @@ -101,10 +101,14 @@ processWords incr rules wordsFormat outMode = Left e -> throwM e Right r -> yield r - processApplicationOutput :: ApplicationOutput PWord Statement -> Either ParseException Text + processApplicationOutput :: ApplicationOutput PWord (Statement Expanded [Grapheme]) -> Either ParseException Text processApplicationOutput (HighlightedWords cs) = Right $ pack $ detokeniseWords $ (fmap.fmap) fst cs processApplicationOutput (AppliedRulesTable is) = Right $ pack $ unlines $ reportAsText plaintext' <$> is processApplicationOutput (ParseError e) = Left $ ParseException $ errorBundlePretty e + processApplicationOutput (ExpandError e) = Left $ ParseException $ case e of + (NotFound s) -> "Could not find category: " ++ s + InvalidBaseValue -> "Invalid value used as base grapheme in feature definition" + MismatchedLengths -> "Mismatched lengths in feature definition" newtype ParseException = ParseException String deriving Show diff --git a/cli/Server.hs b/cli/Server.hs index 3873f96..5cd5daa 100644 --- a/cli/Server.hs +++ b/cli/Server.hs @@ -108,6 +108,10 @@ parseTokeniseAndApplyRulesWrapper ReqRules{..} = (escape $ detokeniseWords' highlightWord result) AppliedRulesTable items -> RespRules Nothing $ surroundTable $ concatMap (reportAsHtmlRows plaintext') items + ExpandError err -> RespError $ ("
"++) $ (++"
") $ case err of + (NotFound s) -> "Could not find category: " ++ s + InvalidBaseValue -> "Invalid value used as base grapheme in feature definition" + MismatchedLengths -> "Mismatched lengths in feature definition" where highlightWord (s, False) = concatWithBoundary s highlightWord (s, True) = "" ++ concatWithBoundary s ++ "" diff --git a/gui/brassica-interop-wasm/src/BrassicaInterop.hs b/gui/brassica-interop-wasm/src/BrassicaInterop.hs index d7ce282..69bd62e 100644 --- a/gui/brassica-interop-wasm/src/BrassicaInterop.hs +++ b/gui/brassica-interop-wasm/src/BrassicaInterop.hs @@ -78,6 +78,11 @@ parseTokeniseAndApplyRules_hs writeIORef prevRef Nothing newStableCStringLen $ surroundTable $ concatMap (reportAsHtmlRows plaintext') items + ExpandError err -> do + newStableCStringLen $ ("
"++) $ (++"
") $ case err of + (NotFound s) -> "Could not find category: " ++ s + InvalidBaseValue -> "Invalid value used as base grapheme in feature definition" + MismatchedLengths -> "Mismatched lengths in feature definition" where highlightWord (s, False) = concatWithBoundary s highlightWord (s, True) = "" ++ concatWithBoundary s ++ "" diff --git a/src/Brassica/SoundChange/Apply/Internal.hs b/src/Brassica/SoundChange/Apply/Internal.hs index b252f1a..cdf01b0 100644 --- a/src/Brassica/SoundChange/Apply/Internal.hs +++ b/src/Brassica/SoundChange/Apply/Internal.hs @@ -57,6 +57,7 @@ module Brassica.SoundChange.Apply.Internal import Control.Applicative ((<|>)) import Control.Category ((>>>)) +import Control.Monad ((>=>), join) -- needed for mtl>=2.3 import Data.Containers.ListUtils (nubOrd) import Data.Functor ((<&>)) import Data.Maybe (maybeToList, fromMaybe, listToMaybe, mapMaybe) @@ -137,7 +138,7 @@ match :: forall a t. OneOf a 'Target 'Env => MatchOutput -- ^ The previous 'MatchOutput' -> Maybe Grapheme -- ^ The previously-matched grapheme, if any. (Used to match a 'Geminate'.) - -> Lexeme a -- ^ The lexeme to match. + -> Lexeme Expanded a -- ^ The lexeme to match. -> MultiZipper t Grapheme -- ^ The 'MultiZipper' to match against. -> [(MatchOutput, MultiZipper t Grapheme)] -- ^ The output: a tuple @(g, mz)@ as described below. @@ -155,20 +156,20 @@ match out prev k@(Kleene l) mz = case match out prev l mz of [] -> error "match: Kleene should never fail" r' -> r' match out _ (Grapheme g) mz = (out <> MatchOutput [] [] [g],) <$> maybeToList (matchGrapheme g mz) -match out prev (Category gs) mz = +match out prev (Category (FromElements gs)) mz = concat $ zipWith' gs [0..] $ \e i -> first (<> MatchOutput [i] [] []) <$> case e of - Left g -> match out prev (Grapheme g :: Lexeme a) mz + Left g -> match out prev (Grapheme g :: Lexeme Expanded a) mz Right ls -> matchMany out prev ls mz match out prev Geminate mz = case prev of Nothing -> [] Just prev' -> (out <> MatchOutput [] [] [prev'],) <$> maybeToList (matchGrapheme prev' mz) -match out prev (Backreference i gs) mz = do +match out prev (Backreference i (FromElements gs)) mz = do e <- maybeToList $ (gs !?) =<< matchedCatIxs out !? (i-1) case e of - Left g -> match out prev (Grapheme g :: Lexeme a) mz + Left g -> match out prev (Grapheme g :: Lexeme Expanded a) mz Right ls -> matchMany out prev ls mz matchGrapheme :: Grapheme -> MultiZipper t Grapheme -> Maybe (MultiZipper t Grapheme) @@ -184,7 +185,7 @@ matchGraphemeP p mz = value mz >>= \cs -> if p cs then fwd mz else Nothing matchMany :: OneOf a 'Target 'Env => MatchOutput -> Maybe Grapheme - -> [Lexeme a] + -> [Lexeme Expanded a] -> MultiZipper t Grapheme -> [(MatchOutput, MultiZipper t Grapheme)] matchMany out _ [] mz = [(out, mz)] @@ -195,7 +196,7 @@ matchMany out prev (l:ls) mz = -- | 'matchMany' without any previous match output. matchMany' :: OneOf a 'Target 'Env => Maybe Grapheme - -> [Lexeme a] + -> [Lexeme Expanded a] -> MultiZipper t Grapheme -> [(MatchOutput, MultiZipper t Grapheme)] matchMany' = matchMany (MatchOutput [] [] []) @@ -241,7 +242,7 @@ forceCategory i ixs = ixs { forcedCategory = Just i } -- possible replacements and apply them to the given input. mkReplacement :: MatchOutput -- ^ The result of matching against the target - -> [Lexeme 'Replacement] -- ^ The 'Lexeme's specifying the replacement. + -> [Lexeme Expanded 'Replacement] -- ^ The 'Lexeme's specifying the replacement. -> MultiZipper t Grapheme -> [MultiZipper t Grapheme] mkReplacement out = \ls -> fmap (fst . snd) . go startIxs ls . (,Nothing) @@ -250,7 +251,7 @@ mkReplacement out = \ls -> fmap (fst . snd) . go startIxs ls . (,Nothing) go :: ReplacementIndices - -> [Lexeme 'Replacement] + -> [Lexeme Expanded 'Replacement] -> (MultiZipper t Grapheme, Maybe Grapheme) -> [(ReplacementIndices, (MultiZipper t Grapheme, Maybe Grapheme))] go ixs [] (mz, prev) = [(ixs, (mz, prev))] @@ -262,12 +263,12 @@ mkReplacement out = \ls -> fmap (fst . snd) . go startIxs ls . (,Nothing) replaceLex :: ReplacementIndices - -> Lexeme 'Replacement + -> Lexeme Expanded 'Replacement -> MultiZipper t Grapheme -> Maybe Grapheme -> [(ReplacementIndices, (MultiZipper t Grapheme, Maybe Grapheme))] replaceLex ixs (Grapheme g) mz _prev = [(ixs, (insert g mz, Just g))] - replaceLex ixs (Category gs) mz prev = + replaceLex ixs (Category (FromElements gs)) mz prev = case advanceCategory ixs numCatsMatched of (CategoryNumber ci, ixs') -> let i = matchedCatIxs out !! ci in @@ -305,7 +306,10 @@ mkReplacement out = \ls -> fmap (fst . snd) . go startIxs ls . (,Nothing) -- 'exception' of that rule (if any) applies starting at the current -- position of the 'MultiZipper'; if it does, returns the index of the -- first element of each matching 'target'. -exceptionAppliesAtPoint :: [Lexeme 'Target] -> Environment -> MultiZipper RuleTag Grapheme -> [Int] +exceptionAppliesAtPoint + :: [Lexeme Expanded 'Target] + -> Environment Expanded + -> MultiZipper RuleTag Grapheme -> [Int] exceptionAppliesAtPoint target (ex1, ex2) mz = fmap fst $ flip runRuleAp mz $ do _ <- RuleAp $ matchMany' Nothing ex1 pos <- gets curPos @@ -319,8 +323,8 @@ exceptionAppliesAtPoint target (ex1, ex2) mz = fmap fst $ flip runRuleAp mz $ do -- t'Grapheme's, and @is@ is a list of indices, one for each -- 'Category' lexeme matched. matchRuleAtPoint - :: [Lexeme 'Target] - -> Environment + :: [Lexeme Expanded 'Target] + -> Environment Expanded -> MultiZipper RuleTag Grapheme -> [(MatchOutput, MultiZipper RuleTag Grapheme)] matchRuleAtPoint target (env1,env2) mz = flip runRuleAp mz $ do @@ -339,7 +343,7 @@ matchRuleAtPoint target (env1,env2) mz = flip runRuleAp mz $ do -- | Given a 'Rule', determine if the rule matches at the current -- point; if so, apply the rule, adding appropriate tags. -applyOnce :: Rule -> StateT (MultiZipper RuleTag Grapheme) [] Bool +applyOnce :: Rule Expanded -> StateT (MultiZipper RuleTag Grapheme) [] Bool applyOnce r@Rule{target, replacement, exception} = modify (tag AppStart) >> go (environment r) where @@ -364,7 +368,11 @@ applyOnce r@Rule{target, replacement, exception} = -- | Remove tags and advance the current index to the next t'Grapheme' -- after the rule application. -setupForNextApplication :: Bool -> Rule -> MultiZipper RuleTag Grapheme -> Maybe (MultiZipper RuleTag Grapheme) +setupForNextApplication + :: Bool + -> Rule Expanded + -> MultiZipper RuleTag Grapheme + -> Maybe (MultiZipper RuleTag Grapheme) setupForNextApplication success r@Rule{flags=Flags{applyDirection}} = fmap untag . case applyDirection of RTL -> seek AppStart >=> bwd @@ -380,7 +388,7 @@ setupForNextApplication success r@Rule{flags=Flags{applyDirection}} = -- | Apply a 'Rule' to a 'MultiZipper'. The application will start at -- the beginning of the 'MultiZipper', and will be repeated as many -- times as possible. Returns all valid results. -applyRule :: Rule -> MultiZipper RuleTag Grapheme -> [MultiZipper RuleTag Grapheme] +applyRule :: Rule Expanded -> MultiZipper RuleTag Grapheme -> [MultiZipper RuleTag Grapheme] applyRule r = \mz -> -- use a lambda so mz isn't shadowed in the where block let startingPos = case applyDirection $ flags r of LTR -> toBeginning mz @@ -404,23 +412,26 @@ applyRule r = \mz -> -- use a lambda so mz isn't shadowed in the where block -- | Check that the 'MultiZipper' contains only graphemes listed in -- the given 'CategoriesDecl', replacing all unlisted graphemes with -- U+FFFD. -checkGraphemes :: CategoriesDecl -> MultiZipper RuleTag Grapheme -> MultiZipper RuleTag Grapheme -checkGraphemes (CategoriesDecl gs) = fmap $ \case +checkGraphemes :: [Grapheme] -> MultiZipper RuleTag Grapheme -> MultiZipper RuleTag Grapheme +checkGraphemes gs = fmap $ \case GBoundary -> GBoundary g -> if g `elem` gs then g else GMulti "\xfffd" -- | Apply a 'Statement' to a 'MultiZipper'. This is a simple wrapper -- around 'applyRule' and 'checkGraphemes'. -applyStatement :: Statement -> MultiZipper RuleTag Grapheme -> [MultiZipper RuleTag Grapheme] +applyStatement + :: Statement Expanded [Grapheme] + -> MultiZipper RuleTag Grapheme + -> [MultiZipper RuleTag Grapheme] applyStatement (RuleS r) mz = applyRule r mz -applyStatement (CategoriesDeclS gs) mz = [checkGraphemes gs mz] +applyStatement (DirectiveS gs) mz = [checkGraphemes gs mz] -- | Apply a single 'Rule' to a word. -- -- Note: duplicate outputs from this function are removed. To keep -- duplicates, use the lower-level internal function 'applyRule' -- directly. -applyRuleStr :: Rule -> PWord -> [PWord] +applyRuleStr :: Rule Expanded -> PWord -> [PWord] -- Note: 'fromJust' is safe here as 'apply' should always succeed applyRuleStr r s = nubOrd $ fmap toList $ applyRule r $ fromListStart s @@ -429,7 +440,7 @@ applyRuleStr r s = nubOrd $ fmap toList $ applyRule r $ fromListStart s -- Note: as with 'applyRuleStr', duplicate outputs from this function -- are removed. To keep duplicates, use the lower-level internal -- function 'applyStatement' directly. -applyStatementStr :: Statement -> PWord -> [PWord] +applyStatementStr :: Statement Expanded [Grapheme] -> PWord -> [PWord] applyStatementStr st = addBoundaries >>> fromListStart @@ -518,14 +529,20 @@ reportAsText render item = unlines $ -- | Apply a single 'Statement' to a word. Returns a 'LogItem' for -- each possible result, or @[]@ if the rule does not apply and the -- input is returned unmodified. -applyStatementWithLog :: Statement -> PWord -> [LogItem Statement] +applyStatementWithLog + :: Statement Expanded [Grapheme] + -> PWord + -> [LogItem (Statement Expanded [Grapheme])] applyStatementWithLog st w = case applyStatementStr st w of [w'] -> if w' == w then [] else [ActionApplied st w w'] r -> ActionApplied st w <$> r -- | Apply 'SoundChanges' to a word. For each possible result, returns -- a 'LogItem' for each 'Statement' which altered the input. -applyChangesWithLog :: SoundChanges -> PWord -> [[LogItem Statement]] +applyChangesWithLog + :: SoundChanges Expanded [Grapheme] + -> PWord + -> [[LogItem (Statement Expanded [Grapheme])]] applyChangesWithLog [] _ = [[]] applyChangesWithLog (st:sts) w = case applyStatementWithLog st w of @@ -535,11 +552,14 @@ applyChangesWithLog (st:sts) w = -- | Apply 'SoundChanges' to a word, returning an 'PWordLog' -- for each possible result. -applyChangesWithLogs :: SoundChanges -> PWord -> [PWordLog Statement] +applyChangesWithLogs + :: SoundChanges Expanded [Grapheme] + -> PWord + -> [PWordLog (Statement Expanded [Grapheme])] applyChangesWithLogs scs w = mapMaybe toPWordLog $ applyChangesWithLog scs w -- | Apply a set of 'SoundChanges' to a word. -applyChanges :: SoundChanges -> PWord -> [PWord] +applyChanges :: SoundChanges Expanded [Grapheme] -> PWord -> [PWord] applyChanges sts w = lastOutput <$> applyChangesWithLog sts w where @@ -550,11 +570,11 @@ applyChanges sts w = -- well as a boolean value indicating whether the word should be -- highlighted in a UI due to changes from its initial value. (Note -- that this accounts for 'highlightChanges' values.) -applyChangesWithChanges :: SoundChanges -> PWord -> [(PWord, Bool)] +applyChangesWithChanges :: SoundChanges Expanded [Grapheme] -> PWord -> [(PWord, Bool)] applyChangesWithChanges sts w = applyChangesWithLog sts w <&> \case [] -> (w, False) logs -> (output $ last logs, hasChanged logs) where hasChanged = any $ \case ActionApplied{action=RuleS rule} -> highlightChanges $ flags rule - ActionApplied{action=CategoriesDeclS _} -> True + ActionApplied{action=DirectiveS _} -> True diff --git a/src/Brassica/SoundChange/Category.hs b/src/Brassica/SoundChange/Category.hs index 5df3ed5..324a7ae 100644 --- a/src/Brassica/SoundChange/Category.hs +++ b/src/Brassica/SoundChange/Category.hs @@ -1,112 +1,151 @@ -{-# LANGUAGE DataKinds #-} -{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE GADTs #-} {-# LANGUAGE KindSignatures #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE TupleSections #-} module Brassica.SoundChange.Category - ( - -- * Category construction - Category(..) - , CategoryState(..) - , categorise - -- * Category expansion - , Categories + ( Categories , Brassica.SoundChange.Category.lookup - , mapCrossrefs - , expand - -- * Obtaining values - , bake , values + , ExpandError(..) + , expand + , expandRule + , extend + , expandSoundChanges ) where -import Data.Bifunctor (first) -import Data.Coerce +import Prelude hiding (lookup) +import Control.DeepSeq (NFData) +import Control.Monad (foldM, unless) +import Control.Monad.State.Strict (StateT, evalStateT, lift, get, put) import Data.Containers.ListUtils (nubOrd) -import Data.List (intersect) -import Data.Maybe (fromMaybe) +import Data.List (intersect, (\\), transpose, foldl') +import Data.Maybe (mapMaybe) +import GHC.Generics (Generic) import qualified Data.Map.Strict as M --- | Type-level tag for 'Category'. When parsing a category definition --- from a string, usually categories will refer to other --- categories. This is the 'Unexpanded' state. Once 'Expanded', these --- references will have been inlined, and the category no longer --- depends on other categories. -data CategoryState = Unexpanded | Expanded - --- | A set of values (usually representing phonemes) which behave the --- same way in a sound change. A 'Category' is constructed using the --- set operations supplied as constructors, possibly referencing other --- 'Category's; these references can then be 'expand'ed, allowing the --- 'Category' to be 'bake'd to a list of matching values. --- --- Note that Brassica makes no distinction between ad-hoc categories --- and predefined categories beyond the sound change parser; the --- latter is merely syntax sugar for the former, and both are --- represented using the same 'Category' type. In practise this is not --- usually a problem, since 'Category's are still quite convenient to --- construct manually. -data Category (s :: CategoryState) a - = Empty - -- ^ The empty category (@[]@ in Brassica syntax) - | Node a - -- ^ A single value (@[a]@) - | UnionOf [Category s a] - -- ^ The union of multiple categories (@[Ca Cb Cc]@) - | Intersect (Category s a) (Category s a) - -- ^ The intersection of two categories (@[Ca +Cb]@) - | Subtract (Category s a) (Category s a) - -- ^ The second category subtracted from the first (@[Ca -Cb]@) - deriving (Show, Eq, Ord, Functor) +import Brassica.SoundChange.Types +import Data.Traversable (for) -- | A map from names to the (expanded) categories they -- reference. Used to resolve cross-references between categories. -type Categories a b = M.Map a (Category 'Expanded (Either a b)) - --- | @Data.Map.Strict.'Data.Map.Strict.lookup'@, specialised to 'Categories'. -lookup :: Ord a => a -> Categories a b -> Maybe (Category 'Expanded (Either a b)) -lookup = M.lookup - --- | Map a function over all the cross-references in a set of 'Categories'. -mapCrossrefs :: Ord b => (a -> b) -> Categories a c -> Categories b c -mapCrossrefs f = M.map (fmap $ first f) . M.mapKeys f - --- | Given a list of values, return a 'Category' which matches only --- those values. (This is a simple wrapper around 'Node' and --- 'UnionOf'.) -categorise :: Ord a => [a] -> Category 'Expanded a -categorise = UnionOf . fmap Node - --- | Expand an 'Unexpanded' category by inlining its references. The --- references should only be to categories in the given 'Categories'. -expand :: Ord a => Categories a b -> Category 'Unexpanded (Either a b) -> Category 'Expanded (Either a b) -expand _ Empty = Empty -expand cs n@(Node (Left a)) = fromMaybe (coerce n) $ M.lookup a cs -expand _ (Node (Right b)) = Node (Right b) -expand cs (UnionOf u) = UnionOf $ expand cs <$> u -expand cs (Intersect a b) = Intersect (expand cs a) (expand cs b) -expand cs (Subtract a b) = Subtract (expand cs a) (expand cs b) - --- | Given an 'Expanded' category, return the list of values which it +type Categories = M.Map String (Expanded 'AnyPart) + +-- | Lookup a category name in 'Categories'. +lookup :: String -> Categories -> Maybe (Expanded a) +lookup = (fmap generaliseExpanded .) . M.lookup + +-- | Returns a list of every value mentioned in a set of +-- 'Categories' +values :: Categories -> [Either Grapheme [Lexeme Expanded 'AnyPart]] +values = nubOrd . concatMap elements . M.elems + +-- Errors which can be emitted while inlining or expanding category +-- definitions. +data ExpandError + = NotFound String + -- ^ A category with that name was not found + | InvalidBaseValue + -- ^ A 'Lexeme' was used as a base value in a feature + | MismatchedLengths + -- ^ A 'FeatureSpec' contained a mismatched number of values + deriving (Show, Generic, NFData) + +-- | Given a category, return the list of values which it -- matches. -bake :: Eq a => Category 'Expanded a -> [a] -bake Empty = [] -bake (Node a) = [a] -bake (UnionOf u) = concatMap bake u -bake (Intersect a b) = bake a `intersect` bake b -bake (Subtract a b) = bake a `difference` bake b +expand :: Categories -> CategorySpec a -> Either ExpandError (Expanded a) +expand cs (MustInline g) = maybe (Left $ NotFound g) Right $ lookup g cs +expand cs (CategorySpec spec) = FromElements <$> foldM go [] spec where - difference l m = filter (not . (`elem` m)) l + go es (modifier, e) = do + new <- case e of + Left (GMulti g) + | Just (FromElements c) <- lookup g cs + -> pure c + | otherwise -> pure [Left (GMulti g)] + Left GBoundary -> pure [Left GBoundary] + Right ls -> pure . Right <$> traverse (expandLexeme cs) ls + pure $ case modifier of + Union -> es ++ new + Intersect -> es `intersect` new + Subtract -> es \\ new --- | Returns a list of every value mentioned in a set of --- 'Categories'. This includes all values, even those which are --- 'Intersect'ed or 'Subtract'ed out: e.g. given 'Categories' --- including @[a b -a]@, this will return a list including --- @["a","b"]@, not just @["b"]@. -values :: (Ord a, Ord b) => Categories a b -> [Either a b] -values = nubOrd . concatMap go . M.elems +expandLexeme :: Categories -> Lexeme CategorySpec a -> Either ExpandError (Lexeme Expanded a) +expandLexeme cs (Grapheme (GMulti g)) = Right $ + case lookup g cs of + Just c -> Category c + Nothing -> Grapheme (GMulti g) +expandLexeme _ (Grapheme GBoundary) = Right $ Grapheme GBoundary +expandLexeme cs (Category c) = Category <$> expand cs c +expandLexeme cs (Optional ls) = Optional <$> traverse (expandLexeme cs) ls +expandLexeme _ Metathesis = Right Metathesis +expandLexeme _ Geminate = Right Geminate +expandLexeme cs (Wildcard l) = Wildcard <$> expandLexeme cs l +expandLexeme cs (Kleene l) = Kleene <$> expandLexeme cs l +expandLexeme _ Discard = Right Discard +expandLexeme cs (Backreference i c) = Backreference i <$> expand cs c +expandLexeme cs (Multiple c) = Multiple <$> expand cs c + +expandRule :: Categories -> Rule CategorySpec -> Either ExpandError (Rule Expanded) +expandRule cs r = Rule + <$> traverse (expandLexeme cs) (target r) + <*> traverse (expandLexeme cs) (replacement r) + <*> traverse expandEnvironment (environment r) + <*> traverse expandEnvironment (exception r) + <*> pure (flags r) + <*> pure (plaintext r) + where + expandEnvironment (e1, e2) = (,) + <$> traverse (expandLexeme cs) e1 + <*> traverse (expandLexeme cs) e2 + +extend :: Categories -> Directive -> Either ExpandError Categories +extend cs' (Categories overwrite defs) = + foldM go (if overwrite then M.empty else cs') defs + where + go :: Categories -> CategoryDefinition -> Either ExpandError Categories + go cs (DefineCategory name val) = flip (M.insert name) cs <$> expand cs val + go cs (DefineFeature spec) = do + baseValues <- expand cs $ featureBaseValues spec + derivedCats <- traverse (traverse $ expand cs) $ featureDerived spec + + baseValues' <- for (elements baseValues) $ \case + Left (GMulti g) -> Right g + _ -> Left InvalidBaseValue + let baseLen = length baseValues' + derivedValues = elements . snd <$> derivedCats + unless (all ((==baseLen) . length) derivedValues) $ + Left MismatchedLengths + + let features = zipWith + (\base ds -> (base, FromElements $ Left (GMulti base) : ds)) + baseValues' + (transpose derivedValues) + newCats = + maybe [] (pure . (,baseValues)) (featureBaseName spec) + ++ derivedCats + ++ features + Right $ foldl' (flip $ uncurry M.insert) cs newCats + +expandSoundChanges + :: SoundChanges CategorySpec Directive + -> Either ExpandError (SoundChanges Expanded [Grapheme]) +expandSoundChanges = flip evalStateT M.empty . traverse go where - go Empty = [] - go (Node a) = [a] - go (UnionOf u) = concatMap go u - go (Intersect a b) = go a ++ go b - go (Subtract a b) = go a ++ go b + go :: Statement CategorySpec Directive + -> StateT Categories (Either ExpandError) (Statement Expanded [Grapheme]) + go (RuleS r) = do + cs <- get + lift $ RuleS <$> expandRule cs r + go (DirectiveS d) = do + cs <- get + cs' <- lift $ extend cs d + put cs' + pure $ DirectiveS $ mapMaybe left $ values cs' + + left (Left l) = Just l + left (Right _) = Nothing diff --git a/src/Brassica/SoundChange/Frontend/Internal.hs b/src/Brassica/SoundChange/Frontend/Internal.hs index 53b13ef..9c808ee 100644 --- a/src/Brassica/SoundChange/Frontend/Internal.hs +++ b/src/Brassica/SoundChange/Frontend/Internal.hs @@ -21,6 +21,7 @@ import Text.Megaparsec (ParseErrorBundle) import Brassica.MDF (MDF, parseMDFWithTokenisation, componentiseMDF, componentiseMDFWordsOnly, duplicateEtymologies) import Brassica.SoundChange.Apply import Brassica.SoundChange.Apply.Internal (applyChangesWithLog, toPWordLog) +import Brassica.SoundChange.Category import Brassica.SoundChange.Tokenise import Brassica.SoundChange.Types @@ -87,6 +88,7 @@ data ApplicationOutput a r = HighlightedWords [Component (a, Bool)] | AppliedRulesTable [PWordLog r] | ParseError (ParseErrorBundle String Void) + | ExpandError ExpandError deriving (Show, Generic, NFData) -- | Kind of input: either a raw wordlist, or an MDF file. @@ -122,7 +124,7 @@ intersperseWords _ [] = [] tokeniseAccordingToInputFormat :: InputLexiconFormat -> TokenisationMode - -> SoundChanges + -> SoundChanges Expanded [Grapheme] -> String -> Either (ParseErrorBundle String Void) (ParseOutput PWord) tokeniseAccordingToInputFormat Raw _ cs = @@ -142,37 +144,40 @@ getParsedWords (ParsedMDF mdf) = getWords $ componentiseMDF mdf -- wordlist and a list of sound changes, returns the result of running -- the changes in the specified mode. parseTokeniseAndApplyRules - :: SoundChanges -- ^ changes + :: SoundChanges CategorySpec Directive -- ^ changes -> String -- ^ words -> InputLexiconFormat -> ApplicationMode -> Maybe [Component PWord] -- ^ previous results - -> ApplicationOutput PWord Statement + -> ApplicationOutput PWord (Statement Expanded [Grapheme]) parseTokeniseAndApplyRules statements ws intype mode prev = - let tmode = tokenisationModeFor mode in - case tokeniseAccordingToInputFormat intype tmode statements ws of - Left e -> ParseError e - Right toks - | ws' <- getParsedWords toks - -> case mode of - ReportRulesApplied -> - AppliedRulesTable $ mapMaybe toPWordLog $ concat $ - getWords $ componentise WordsOnlyOutput [] $ - applyChangesWithLog statements <$> toks - ApplyRules DifferentToLastRun mdfout -> - let result = concatMap (splitMultipleResults " ") $ - componentise mdfout (fmap pure ws') $ applyChanges statements <$> toks - in HighlightedWords $ - zipWithComponents result (fromMaybe [] prev) [] $ \thisWord prevWord -> - (thisWord, thisWord /= prevWord) - ApplyRules DifferentToInput mdfout -> - HighlightedWords $ concatMap (splitMultipleResults " ") $ - componentise mdfout (fmap (pure . (,False)) ws') $ - applyChangesWithChanges statements <$> toks - ApplyRules NoHighlight mdfout -> - HighlightedWords $ (fmap.fmap) (,False) $ concatMap (splitMultipleResults " ") $ - componentise mdfout (fmap pure ws') $ - applyChanges statements <$> toks + case expandSoundChanges statements of + Left e -> ExpandError e + Right statements' -> + let tmode = tokenisationModeFor mode in + case tokeniseAccordingToInputFormat intype tmode statements' ws of + Left e -> ParseError e + Right toks + | ws' <- getParsedWords toks + -> case mode of + ReportRulesApplied -> + AppliedRulesTable $ mapMaybe toPWordLog $ concat $ + getWords $ componentise WordsOnlyOutput [] $ + applyChangesWithLog statements' <$> toks + ApplyRules DifferentToLastRun mdfout -> + let result = concatMap (splitMultipleResults " ") $ + componentise mdfout (fmap pure ws') $ applyChanges statements' <$> toks + in HighlightedWords $ + zipWithComponents result (fromMaybe [] prev) [] $ \thisWord prevWord -> + (thisWord, thisWord /= prevWord) + ApplyRules DifferentToInput mdfout -> + HighlightedWords $ concatMap (splitMultipleResults " ") $ + componentise mdfout (fmap (pure . (,False)) ws') $ + applyChangesWithChanges statements' <$> toks + ApplyRules NoHighlight mdfout -> + HighlightedWords $ (fmap.fmap) (,False) $ concatMap (splitMultipleResults " ") $ + componentise mdfout (fmap pure ws') $ + applyChanges statements' <$> toks where -- Zips two tokenised input strings. Compared to normal 'zipWith' -- this has two special properties: diff --git a/src/Brassica/SoundChange/Parse.hs b/src/Brassica/SoundChange/Parse.hs index 5d69d23..b094730 100644 --- a/src/Brassica/SoundChange/Parse.hs +++ b/src/Brassica/SoundChange/Parse.hs @@ -1,44 +1,35 @@ {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE KindSignatures #-} -{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE TypeApplications #-} module Brassica.SoundChange.Parse ( parseRule - , parseRuleWithCategories , parseSoundChanges -- ** Re-export , errorBundlePretty ) where import Data.Char (isSpace) -import Data.Either (isRight) import Data.Foldable (asum) -import Data.List (transpose) -import Data.Maybe (isNothing, isJust, fromJust, mapMaybe) +import Data.Maybe (isNothing, isJust, fromJust) import Data.Void (Void) import Control.Applicative.Permutations -import Control.Monad.State -import qualified Data.Map.Strict as M +import Control.Monad (void, guard) import Text.Megaparsec hiding (State) import Text.Megaparsec.Char import qualified Text.Megaparsec.Char.Lexer as L import Brassica.SoundChange.Types -import qualified Brassica.SoundChange.Category as C -newtype Config = Config - { categories :: C.Categories Grapheme [Lexeme 'AnyPart] - } -type Parser = ParsecT Void String (State Config) +type Parser = Parsec Void String class ParseLexeme (a :: LexemeType) where - parseLexeme :: Parser (Lexeme a) + parseLexeme :: Parser (Lexeme CategorySpec a) -- space consumer which does not match newlines sc :: Parser () @@ -67,198 +58,140 @@ nonzero = label "nonzero postive number" $ try $ do guard $ n>0 pure n -parseGrapheme :: Parser (Grapheme, Bool) -parseGrapheme = lexeme $ parseBoundary <|> parseMulti - where - parseBoundary = (GBoundary,False) <$ char '#' - - parseMulti = (,) - <$> fmap GMulti - (withStar $ takeWhile1P Nothing (not . ((||) <$> isSpace <*> (`elem` keyChars)))) - <*> (isJust <$> optional (char '~')) - - withStar :: Parser String -> Parser String - withStar p = optional (char '*') >>= \case - Just _ -> ('*':) <$> p - Nothing -> p - -parseGrapheme' :: Parser Grapheme -parseGrapheme' = lexeme $ GMulti <$> takeWhile1P Nothing (not . ((||) <$> isSpace <*> (=='='))) - -data CategoryModification a - = Union (Either Grapheme [Lexeme a]) - | Intersect (Either Grapheme [Lexeme a]) - | Subtract (Either Grapheme [Lexeme a]) - -parseGraphemeOrCategory :: Parser (Lexeme a) -parseGraphemeOrCategory = do - (g, isntCat) <- parseGrapheme - if isntCat - then return $ Grapheme g - else do - cats <- gets categories - return $ case C.lookup g cats of - Nothing -> Grapheme g - Just c -> Category $ (fmap.fmap.fmap) generalise $ C.bake c - -parseCategory :: ParseLexeme a => Parser (Lexeme a) -parseCategory = Category <$> parseCategory' - -parseCategory' :: ParseLexeme a => Parser [Either Grapheme [Lexeme a]] -parseCategory' = do - mods <- symbol "[" *> someTill parseCategoryModification (symbol "]") - cats <- gets categories - return $ C.bake $ - C.expand ((fmap.fmap.fmap.fmap) generalise cats) (toCategory mods) +parseGrapheme :: Parser Grapheme +parseGrapheme = lexeme $ + GBoundary <$ char '#' + <|> GMulti <$> parseGrapheme' + +parseGrapheme' :: Parser String +parseGrapheme' = lexeme $ do + star <- optional (char '*') + rest <- takeWhile1P Nothing (not . ((||) <$> isSpace <*> (`elem` keyChars))) + nocat <- optional (char '~') + pure . + maybe id (const ('*':)) star . + maybe id (const (++"~")) nocat + $ rest + +parseExplicitCategory :: ParseLexeme a => Parser (Lexeme CategorySpec a) +parseExplicitCategory = Category <$> parseExplicitCategory' + +parseExplicitCategory' :: ParseLexeme a => Parser (CategorySpec a) +parseExplicitCategory' = + CategorySpec <$> (symbol "[" *> someTill parseCategoryModification (symbol "]")) + +-- This is unused currently, but convenient to keep around just in case +-- parseCategory :: ParseLexeme a => Parser (Lexeme CategorySpec a) +-- parseCategory = Category <$> parseCategory' + +parseCategory' :: ParseLexeme a => Parser (CategorySpec a) +parseCategory' = parseExplicitCategory' <|> MustInline <$> parseGrapheme' parseCategoryStandalone - :: Parser (Grapheme, C.Category 'C.Expanded (Either Grapheme [Lexeme 'AnyPart])) + :: Parser (String, CategorySpec 'AnyPart) parseCategoryStandalone = do g <- parseGrapheme' _ <- symbol "=" - -- Use Target here because it only allows graphemes, not boundaries mods <- some parseCategoryModification - cats <- gets categories - return (g, C.expand cats $ toCategory mods) - -categoriesDeclParse :: Parser CategoriesDecl -categoriesDeclParse = do - overwrite <- isJust <$> optional (symbol "new") - when overwrite $ put $ Config M.empty - _ <- symbol "categories" <* scn - -- parse category declarations, adding to the set of known - -- categories as each is parsed - _ <- some $ parseFeature <|> parseCategoryDecl - _ <- symbol "end" <* scn - Config catsNew <- get - return $ CategoriesDecl $ - mapMaybe (\case Left g -> Just g; _ -> Nothing) $ - C.values catsNew - where - parseFeature = do - _ <- symbol "feature" - namePlain <- optional $ try $ parseGrapheme' <* symbol "=" - modsPlain <- some parseCategoryModification - cats <- gets categories - let plainCat = C.expand cats $ toCategory modsPlain - plain = C.bake plainCat - when (any isRight plain) $ fail "Cannot use {…} as a base grapheme" - modifiedCats <- some (symbol "/" *> parseCategoryStandalone) <* scn - let modified = C.bake . snd <$> modifiedCats - syns :: [(Grapheme, C.Category 'C.Expanded (Either Grapheme [Lexeme 'AnyPart]))] - syns = - zipWith (\a b -> (a, C.UnionOf [C.Node (Left a), C.categorise b])) - (fromLeft' <$> plain) - (transpose modified) - modify $ \(Config cs) -> Config $ M.unions - [ M.fromList syns - , M.fromList modifiedCats - , case namePlain of - Nothing -> M.empty - Just n -> M.singleton n plainCat - , cs - ] - parseCategoryDecl = do - (k, c) <- try parseCategoryStandalone <* scn - modify $ \(Config cs) -> Config (M.insert k c cs) - - fromLeft' :: Either a b -> a - fromLeft' (Left a) = a - fromLeft' _ = error "fromLeft': unexpected case!" - -parseCategoryModification :: ParseLexeme a => Parser (CategoryModification a) -parseCategoryModification = parsePrefix <*> - ( (Right <$> (symbol "{" *> manyTill parseLexeme (symbol "}"))) - <|> (Left . fst <$> parseGrapheme)) + return (g, CategorySpec mods) + +parseFeature :: Parser FeatureSpec +parseFeature = do + _ <- symbol "feature" + featureBaseName <- optional $ try $ parseGrapheme' <* symbol "=" + featureBaseValues <- CategorySpec <$> some parseCategoryModification + featureDerived <- some (symbol "/" *> parseCategoryStandalone) <* scn + pure FeatureSpec { featureBaseName, featureBaseValues, featureDerived } + +parseCategoryModification + :: ParseLexeme a + => Parser (CategoryModification, Either Grapheme [Lexeme CategorySpec a]) +parseCategoryModification = (,) + <$> parsePrefix + <*> ( (Right <$> (symbol "{" *> manyTill parseLexeme (symbol "}"))) + <|> (Left <$> parseGrapheme)) where parsePrefix = (Intersect <$ char '+') <|> (Subtract <$ char '-') <|> pure Union -toCategory :: [CategoryModification a] -> C.Category 'C.Unexpanded (Either Grapheme [Lexeme a]) -toCategory = go C.Empty - where - go c [] = c - go c (Union e :es) = go (C.UnionOf [c, C.Node e]) es - go c (Intersect e:es) = go (C.Intersect c (C.Node e)) es - go c (Subtract e :es) = go (C.Subtract c (C.Node e)) es +parseDirective :: Parser Directive +parseDirective = do + overwrite <- isJust <$> optional (symbol "new") + _ <- symbol "categories" <* scn + cs <- some $ + DefineFeature <$> parseFeature <|> + uncurry DefineCategory <$> (try parseCategoryStandalone <* scn) + _ <- symbol "end" <* scn + pure $ Categories overwrite cs -parseOptional :: ParseLexeme a => Parser (Lexeme a) +parseOptional :: ParseLexeme a => Parser (Lexeme CategorySpec a) parseOptional = Optional <$> between (symbol "(") (symbol ")") (some parseLexeme) -parseGeminate :: Parser (Lexeme a) +parseGeminate :: Parser (Lexeme CategorySpec a) parseGeminate = Geminate <$ symbol ">" -parseMetathesis :: Parser (Lexeme 'Replacement) +parseMetathesis :: Parser (Lexeme CategorySpec 'Replacement) parseMetathesis = Metathesis <$ symbol "\\" -parseWildcard :: (ParseLexeme a, OneOf a 'Target 'Env) => Parser (Lexeme a) +parseWildcard :: (ParseLexeme a, OneOf a 'Target 'Env) => Parser (Lexeme CategorySpec a) parseWildcard = Wildcard <$> (symbol "^" *> parseLexeme) -parseDiscard :: Parser (Lexeme 'Replacement) +parseDiscard :: Parser (Lexeme CategorySpec 'Replacement) parseDiscard = Discard <$ symbol "~" -parseKleene :: OneOf a 'Target 'Env => Lexeme a -> Parser (Lexeme a) +parseKleene :: OneOf a 'Target 'Env => Lexeme CategorySpec a -> Parser (Lexeme CategorySpec a) parseKleene l = try (lexeme $ Kleene l <$ char '*' <* notFollowedBy parseGrapheme') <|> pure l -parseMultiple :: Parser (Lexeme 'Replacement) +parseMultiple :: Parser (Lexeme CategorySpec 'Replacement) parseMultiple = Multiple <$> (symbol "@?" *> parseCategory') -parseBackreference :: forall a. ParseLexeme a => Parser (Lexeme a) -parseBackreference = - Backreference - <$> (symbol "@" *> nonzero) - <*> (parseCategory' <|> parseGraphemeCategory) - where - parseGraphemeCategory :: Parser [Either Grapheme [Lexeme a]] - parseGraphemeCategory = label "category" $ try $ - (parseGraphemeOrCategory @a) >>= \case - Category gs -> pure gs - _ -> empty +parseBackreference :: forall a. ParseLexeme a => Parser (Lexeme CategorySpec a) +parseBackreference = Backreference <$> (symbol "@" *> nonzero) <*> parseCategory' instance ParseLexeme 'Target where parseLexeme = asum - [ parseCategory + [ parseExplicitCategory , parseOptional , parseGeminate , parseWildcard , parseBackreference - , parseGraphemeOrCategory + , Grapheme <$> parseGrapheme ] >>= parseKleene instance ParseLexeme 'Replacement where parseLexeme = asum - [ parseCategory + [ parseExplicitCategory , parseOptional , parseMetathesis , parseDiscard , parseGeminate , parseMultiple , parseBackreference - , parseGraphemeOrCategory + , Grapheme <$> parseGrapheme ] instance ParseLexeme 'Env where parseLexeme = asum - [ parseCategory + [ parseExplicitCategory , parseOptional , parseGeminate , parseWildcard , parseBackreference - , parseGraphemeOrCategory + , Grapheme <$> parseGrapheme ] >>= parseKleene instance ParseLexeme 'AnyPart where parseLexeme = asum - [ parseCategory + [ parseExplicitCategory , parseOptional - , parseGraphemeOrCategory + , Grapheme <$> parseGrapheme ] -parseLexemes :: ParseLexeme a => Parser [Lexeme a] +parseLexemes :: ParseLexeme a => Parser [Lexeme CategorySpec a] parseLexemes = many parseLexeme parseFlags :: Parser Flags @@ -268,7 +201,7 @@ parseFlags = runPermutation $ Flags <*> toPermutation (isJust <$> optional (symbol "-1")) <*> toPermutation (isJust <$> optional (symbol "-?")) -ruleParser :: Parser Rule +ruleParser :: Parser (Rule CategorySpec) ruleParser = do -- This is an inlined version of 'match' from @megaparsec@; -- 'match' itself would be tricky to use here, since it would need @@ -304,21 +237,13 @@ ruleParser = do -- 'Rule'. Returns 'Left' if the input string is malformed. -- -- For details on the syntax, refer to . -parseRule :: String -> Either (ParseErrorBundle String Void) Rule -parseRule = parseRuleWithCategories M.empty - --- | Same as 'parseRule', but also allows passing in some predefined --- categories to substitute. -parseRuleWithCategories - :: C.Categories Grapheme [Lexeme 'AnyPart] - -> String - -> Either (ParseErrorBundle String Void) Rule -parseRuleWithCategories cs s = flip evalState (Config cs) $ runParserT (scn *> ruleParser <* eof) "" s +parseRule :: String -> Either (ParseErrorBundle String Void) (Rule CategorySpec) +parseRule = runParser (scn *> ruleParser <* eof) "" -- | Parse a list of 'SoundChanges'. -parseSoundChanges :: String -> Either (ParseErrorBundle String Void) SoundChanges -parseSoundChanges s = flip evalState (Config M.empty) $ runParserT (scn *> parser <* eof) "" s +parseSoundChanges :: String -> Either (ParseErrorBundle String Void) (SoundChanges CategorySpec Directive) +parseSoundChanges = runParser (scn *> parser <* eof) "" where parser = many $ - CategoriesDeclS <$> categoriesDeclParse + DirectiveS <$> parseDirective <|> RuleS <$> ruleParser diff --git a/src/Brassica/SoundChange/Tokenise.hs b/src/Brassica/SoundChange/Tokenise.hs index e23a070..4eccdb3 100644 --- a/src/Brassica/SoundChange/Tokenise.hs +++ b/src/Brassica/SoundChange/Tokenise.hs @@ -154,8 +154,8 @@ detokeniseWords = detokeniseWords' concatWithBoundary -- | Given a list of sound changes, extract the list of multigraphs -- defined in the first categories declaration of the 'SoundChange's. -findFirstCategoriesDecl :: SoundChanges -> [String] -findFirstCategoriesDecl (CategoriesDeclS (CategoriesDecl gs):_) = +findFirstCategoriesDecl :: SoundChanges c [Grapheme] -> [String] +findFirstCategoriesDecl (DirectiveS gs:_) = mapMaybe (\case GBoundary -> Nothing; GMulti m -> Just m) gs @@ -166,5 +166,5 @@ findFirstCategoriesDecl [] = [] -- like @'withFirstCategoriesDecl' 'tokeniseWords' changes words@ (to -- tokenise using the graphemes from the first categories declaration) -- and so on. -withFirstCategoriesDecl :: ([String] -> t) -> SoundChanges -> t +withFirstCategoriesDecl :: ([String] -> t) -> SoundChanges c [Grapheme] -> t withFirstCategoriesDecl tok ss = tok (findFirstCategoriesDecl ss) diff --git a/src/Brassica/SoundChange/Types.hs b/src/Brassica/SoundChange/Types.hs index 402da2f..090ff73 100644 --- a/src/Brassica/SoundChange/Types.hs +++ b/src/Brassica/SoundChange/Types.hs @@ -10,6 +10,7 @@ {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE QuantifiedConstraints #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE TypeFamilies #-} @@ -29,17 +30,27 @@ module Brassica.SoundChange.Types , pattern Boundary , LexemeType(..) , generalise + -- * Categories + , mapCategory + , mapCategoryA + , Expanded(..) + , generaliseExpanded -- * Rules , Rule(..) , Environment , Direction(..) , Flags(..) , defFlags - -- * Categories and statements - , CategoriesDecl(..) + -- * Statements , Statement(..) , plaintext' , SoundChanges + -- * Directives + , CategoryModification(..) + , CategorySpec(..) + , FeatureSpec(..) + , CategoryDefinition(..) + , Directive(..) -- * Utility , OneOf ) where @@ -106,47 +117,89 @@ data LexemeType = Target | Replacement | Env | AnyPart -- | A 'Lexeme' is the smallest part of a sound change. Both matches -- and replacements are made up of 'Lexeme's: the phantom type --- variable specifies where each different variety of 'Lexeme' may --- occur. -data Lexeme (a :: LexemeType) where +-- variable @a@ specifies where each different variety of 'Lexeme' may +-- occur. 'Lexeme's are also parameterised by their category type, +-- which may be 'Expanded' or something else. +data Lexeme category (a :: LexemeType) where -- | In Brassica sound-change syntax, one or more letters without intervening whitespace, -- or a word boundary specified as @#@ - Grapheme :: Grapheme -> Lexeme a + Grapheme :: Grapheme -> Lexeme category a -- | In Brassica sound-change syntax, delimited by square brackets - Category :: [Either Grapheme [Lexeme a]] -> Lexeme a + Category :: category a -> Lexeme category a -- | In Brassica sound-change syntax, delimited by parentheses - Optional :: [Lexeme a] -> Lexeme a + Optional :: [Lexeme category a] -> Lexeme category a -- | In Brassica sound-change syntax, specified as @\@ - Metathesis :: Lexeme 'Replacement + Metathesis :: Lexeme category 'Replacement -- | In Brassica sound-change syntax, specified as @>@ - Geminate :: Lexeme a + Geminate :: Lexeme category a -- | In Brassica sound-change syntax, specified as @^@ before another 'Lexeme' - Wildcard :: OneOf a 'Target 'Env => Lexeme a -> Lexeme a + Wildcard :: OneOf a 'Target 'Env => Lexeme category a -> Lexeme category a -- | In Brassica sound-change syntax, specified as @*@ after another 'Lexeme' - Kleene :: OneOf a 'Target 'Env => Lexeme a -> Lexeme a + Kleene :: OneOf a 'Target 'Env => Lexeme category a -> Lexeme category a -- | In Brassica sound-change syntax, specified as @~@ - Discard :: Lexeme 'Replacement + Discard :: Lexeme category 'Replacement -- | In Brassica sound-change syntax, specified as \@i before a category - Backreference :: Int -> [Either Grapheme [Lexeme a]] -> Lexeme a + Backreference :: Int -> category a -> Lexeme category a -- | In Brassica sound-change syntax, specified as \@? before a category - Multiple :: [Either Grapheme [Lexeme 'Replacement]] -> Lexeme 'Replacement + Multiple :: category 'Replacement -> Lexeme category 'Replacement -generalise :: Lexeme 'AnyPart -> Lexeme a -generalise (Grapheme g) = Grapheme g -generalise (Category es) = Category $ (fmap.fmap.fmap) generalise es -generalise (Optional ls) = Optional $ generalise <$> ls -generalise Geminate = Geminate -generalise (Backreference i es) = Backreference i $ (fmap.fmap.fmap) generalise es +mapCategory :: (forall x. c x -> c' x) -> Lexeme c a -> Lexeme c' a +mapCategory _ (Grapheme g) = Grapheme g +mapCategory f (Category c) = Category (f c) +mapCategory f (Optional ls) = Optional (mapCategory f <$> ls) +mapCategory _ Metathesis = Metathesis +mapCategory _ Geminate = Geminate +mapCategory f (Wildcard l) = Wildcard (mapCategory f l) +mapCategory f (Kleene l) = Kleene (mapCategory f l) +mapCategory _ Discard = Discard +mapCategory f (Backreference i c) = Backreference i (f c) +mapCategory f (Multiple c) = Multiple (f c) + +mapCategoryA + :: Applicative t + => (forall x. c x -> t (c' x)) + -> Lexeme c a + -> t (Lexeme c' a) +mapCategoryA _ (Grapheme g) = pure $ Grapheme g +mapCategoryA f (Category c) = Category <$> f c +mapCategoryA f (Optional ls) = Optional <$> traverse (mapCategoryA f) ls +mapCategoryA _ Metathesis = pure Metathesis +mapCategoryA _ Geminate = pure Geminate +mapCategoryA f (Wildcard l) = Wildcard <$> mapCategoryA f l +mapCategoryA f (Kleene l) = Kleene <$> mapCategoryA f l +mapCategoryA _ Discard = pure Discard +mapCategoryA f (Backreference i c) = Backreference i <$> f c +mapCategoryA f (Multiple c) = Multiple <$> f c + +-- | The type of a category after expansion. +newtype Expanded a = FromElements { elements :: [Either Grapheme [Lexeme Expanded a]] } + deriving (Eq, Ord, Show) + +instance Semigroup (Expanded a) where + (FromElements es) <> (FromElements es') = FromElements (es <> es') + +instance Monoid (Expanded a) where + mempty = FromElements [] + +generalise :: (c 'AnyPart -> c a) -> Lexeme c 'AnyPart -> Lexeme c a +generalise _ (Grapheme g) = Grapheme g +generalise f (Category es) = Category $ f es +generalise f (Optional ls) = Optional $ generalise f <$> ls +generalise _ Geminate = Geminate +generalise f (Backreference i es) = Backreference i $ f es + +generaliseExpanded :: Expanded 'AnyPart -> Expanded a +generaliseExpanded = FromElements . (fmap.fmap.fmap) (generalise generaliseExpanded) . elements -- | A 'Lexeme' matching a single word boundary, specified as @#@ in Brassica syntax. -pattern Boundary :: Lexeme a +pattern Boundary :: Lexeme c a pattern Boundary = Grapheme GBoundary -deriving instance Show (Lexeme a) -deriving instance Eq (Lexeme a) -deriving instance Ord (Lexeme a) +deriving instance (forall x. Show (c x)) => Show (Lexeme c a) +deriving instance (forall x. Eq (c x)) => Eq (Lexeme c a) +deriving instance (forall x. Ord (c x)) => Ord (Lexeme c a) -instance NFData (Lexeme a) where +instance (forall x. NFData (c x)) => NFData (Lexeme c a) where rnf (Grapheme g) = rnf g rnf (Category cs) = rnf cs rnf (Optional ls) = rnf ls @@ -162,7 +215,7 @@ instance NFData (Lexeme a) where -- corresponding to a ‘/ before _ after’ component of a sound change. -- -- Note that an empty environment is just @([], [])@. -type Environment = ([Lexeme 'Env], [Lexeme 'Env]) +type Environment c = ([Lexeme c 'Env], [Lexeme c 'Env]) -- | Specifies application direction of rule — either left-to-right or right-to-left. data Direction = LTR | RTL @@ -202,33 +255,62 @@ defFlags = Flags -- | A single sound change rule: in Brassica sound-change syntax with all elements specified, -- @-flags target / replacement \/ environment1 | environment2 | … \/ exception@. -- (And usually the 'plaintext' of the rule will contain a 'String' resembling that pattern.) -data Rule = Rule - { target :: [Lexeme 'Target] - , replacement :: [Lexeme 'Replacement] - , environment :: [Environment] - , exception :: Maybe Environment +data Rule c = Rule + { target :: [Lexeme c 'Target] + , replacement :: [Lexeme c 'Replacement] + , environment :: [Environment c] + , exception :: Maybe (Environment c) , flags :: Flags , plaintext :: String - } deriving (Show, Generic, NFData) + } deriving (Generic) --- | Corresponds to a category declaration in a set of sound --- changes. Category declarations are mostly desugared away by the --- parser, but for rule application we still need to be able to filter --- out all unknown t'Grapheme's; thus, a 'CategoriesDecl' lists the --- t'Grapheme's which are available at a given point. -newtype CategoriesDecl = CategoriesDecl { graphemes :: [Grapheme] } - deriving (Show, Generic, NFData) +deriving instance (forall a. Show (c a)) => Show (Rule c) +deriving instance (forall a. NFData (c a)) => NFData (Rule c) -- | A 'Statement' can be either a single sound change rule, or a --- category declaration. -data Statement = RuleS Rule | CategoriesDeclS CategoriesDecl - deriving (Show, Generic, NFData) +-- directive (e.g. category definition). +data Statement c decl = RuleS (Rule c) | DirectiveS decl + deriving (Generic) + +deriving instance (forall a. Show (c a), Show decl) => Show (Statement c decl) +deriving instance (forall a. NFData (c a), NFData decl) => NFData (Statement c decl) -- | A simple wrapper around 'plaintext' for 'Statement's. Returns --- @"categories … end"@ for all 'CategoriesDecl' inputs. -plaintext' :: Statement -> String +-- @""@ for all 'DirectiveS' inputs. +plaintext' :: Statement c decl -> String plaintext' (RuleS r) = plaintext r -plaintext' (CategoriesDeclS _) = "categories … end" +plaintext' (DirectiveS _) = "" -- | A set of 'SoundChanges' is simply a list of 'Statement's. -type SoundChanges = [Statement] +type SoundChanges c decl = [Statement c decl] + +-- | The individual operations used to construct a category in +-- Brassica sound-change syntax. +data CategoryModification = Union | Intersect | Subtract + deriving (Show, Eq, Ord) + +-- | The specification of a category in Brassica sound-change syntax. +data CategorySpec a + = CategorySpec [(CategoryModification, Either Grapheme [Lexeme CategorySpec a])] + | MustInline String -- ^ A single grapheme assumed to have been specified earlier as a category + deriving (Show, Eq, Ord) + +-- | The specification of a suprasegmental feature in Brassica +-- sound-change syntax. +data FeatureSpec = FeatureSpec + { featureBaseName :: Maybe String + , featureBaseValues :: CategorySpec 'AnyPart + , featureDerived :: [(String, CategorySpec 'AnyPart)] + } + deriving (Show, Eq, Ord) + +-- | A definition of a new category, either directly or via features. +data CategoryDefinition + = DefineCategory String (CategorySpec 'AnyPart) + | DefineFeature FeatureSpec + deriving (Show, Eq, Ord) + +-- | A directive used in Brassica sound-change syntax: currently only +-- @categories … end@ or @new categories … end@ +data Directive = Categories Bool [CategoryDefinition] + deriving (Show, Eq, Ord) diff --git a/test/Spec.hs b/test/Spec.hs index dbeed85..d3c3e79 100644 --- a/test/Spec.hs +++ b/test/Spec.hs @@ -14,9 +14,10 @@ import qualified Data.ByteString.UTF8 as B8 import qualified Data.Text as T import Brassica.SoundChange (applyChanges, splitMultipleResults, applyChangesWithLogs, reportAsText) +import Brassica.SoundChange.Category (expandSoundChanges) import Brassica.SoundChange.Parse (parseSoundChanges, errorBundlePretty) import Brassica.SoundChange.Tokenise (tokeniseWords, detokeniseWords, withFirstCategoriesDecl, Component, getWords) -import Brassica.SoundChange.Types (SoundChanges, PWord, plaintext') +import Brassica.SoundChange.Types (SoundChanges, PWord, plaintext', Expanded, Grapheme) main :: IO () main = defaultMain $ testGroup "brassica-tests" @@ -29,7 +30,7 @@ main = defaultMain $ testGroup "brassica-tests" showLogs logs = unlines $ fmap (reportAsText plaintext') $ concat $ getWords logs proto21eTest - :: (SoundChanges -> PWord -> [a]) + :: (SoundChanges Expanded [Grapheme] -> PWord -> [a]) -> ([Component [a]] -> String) -> String -> FilePath @@ -41,11 +42,16 @@ proto21eTest applyFn showWord testName outName goldenName = in goldenVsFile testName goldenName' outName' $ withFile outName' WriteMode $ \outFile -> fmap (either id id) . runExceptT $ do soundChangesData <- B8.toString <$> liftIO (B.readFile "test/proto21e.bsc") - soundChanges <- catchEither (parseSoundChanges soundChangesData) $ \err -> do + soundChanges' <- catchEither (parseSoundChanges soundChangesData) $ \err -> do liftIO $ putStrLn $ "Cannot parse the SCA file because:\n" ++ errorBundlePretty err throwE () + soundChanges <- catchEither (expandSoundChanges soundChanges') $ \err -> do + liftIO $ putStrLn $ + "Cannot expand the SCA file because:\n" ++ + show err + throwE () let output pre = B8.fromString . (++"\n") . (pre ++) prettyError = output "SCA Error:" . errorBundlePretty prettyOutput = output "SCA Output: " . showWord diff --git a/test/proto21e-log.golden b/test/proto21e-log.golden index dd25ad3..a58250f 100644 --- a/test/proto21e-log.golden +++ b/test/proto21e-log.golden @@ -202,7 +202,7 @@ qaŋeth -> æŋə ([a aa ə i ii u] / [æ ɔ ə e i u]) thaŋ, - -> thaŋ� (categories … end) + -> thaŋ� () -> saŋ� (Astp / Xfrc) -> sæŋ� ([a aa ə i ii u] / [æ ɔ ə e i u]) From c88b1e088fac1170e153b3ff6026062caddb6235 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 11:42:06 +1100 Subject: [PATCH 04/26] Allow backreferences to work across environment parts --- src/Brassica/SoundChange/Apply/Internal.hs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Brassica/SoundChange/Apply/Internal.hs b/src/Brassica/SoundChange/Apply/Internal.hs index cdf01b0..f5efa43 100644 --- a/src/Brassica/SoundChange/Apply/Internal.hs +++ b/src/Brassica/SoundChange/Apply/Internal.hs @@ -311,10 +311,10 @@ exceptionAppliesAtPoint -> Environment Expanded -> MultiZipper RuleTag Grapheme -> [Int] exceptionAppliesAtPoint target (ex1, ex2) mz = fmap fst $ flip runRuleAp mz $ do - _ <- RuleAp $ matchMany' Nothing ex1 + ex1Out <- RuleAp $ matchMany' Nothing ex1 pos <- gets curPos MatchOutput{matchedGraphemes} <- RuleAp $ matchMany' Nothing target - _ <- RuleAp $ matchMany' (listToMaybe matchedGraphemes) ex2 + _ <- RuleAp $ matchMany ex1Out (listToMaybe matchedGraphemes) ex2 return pos -- | Given a target and environment, determine if they rule @@ -328,7 +328,7 @@ matchRuleAtPoint -> MultiZipper RuleTag Grapheme -> [(MatchOutput, MultiZipper RuleTag Grapheme)] matchRuleAtPoint target (env1,env2) mz = flip runRuleAp mz $ do - _ <- RuleAp $ matchMany' Nothing env1 + env1Out <- RuleAp $ matchMany' Nothing env1 -- start of target needs to be INSIDE 'MultiZipper'! -- otherwise get weird things like /x/#_ resulting in -- #abc#→#xabd#x when it should be #abc#→#xabc# @@ -338,7 +338,7 @@ matchRuleAtPoint target (env1,env2) mz = flip runRuleAp mz $ do modify $ tag TargetStart matchResult <- RuleAp $ matchMany' Nothing target modify $ tag TargetEnd - _ <- RuleAp $ matchMany' (listToMaybe $ matchedGraphemes matchResult) env2 + _ <- RuleAp $ matchMany env1Out (listToMaybe $ matchedGraphemes matchResult) env2 return matchResult -- | Given a 'Rule', determine if the rule matches at the current From 4238a0e0a5a07cb240b9597b7d1f696970bcb879 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 15:15:59 +1100 Subject: [PATCH 05/26] Allow user to choose separator between multiple results --- cli/Main.hs | 30 ++++++++++--------- cli/Server.hs | 3 +- gui/brassica-gui/brassicaprocess.cpp | 4 ++- gui/brassica-gui/brassicaprocess.h | 2 +- gui/brassica-gui/mainwindow.cpp | 18 ++++++++++- gui/brassica-gui/mainwindow.h | 2 ++ .../src/BrassicaInterop.hs | 9 +++++- gui/brassica-web/index.js | 27 ++++++++++------- gui/brassica-web/static/index.html | 4 +++ src/Brassica/SoundChange/Frontend/Internal.hs | 16 +++++----- 10 files changed, 77 insertions(+), 38 deletions(-) diff --git a/cli/Main.hs b/cli/Main.hs index a729366..bece0e9 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -42,20 +42,22 @@ main = execParser opts >>= \case (metavar "RULES" <> help "File containing sound changes") <*> flag Raw MDF (long "mdf" <> help "Parse input words in MDF format") - <*> asum - [ flag' ReportRulesApplied - (long "report" <> help "Report rules applied rather than outputting words") - , flag' (ApplyRules NoHighlight MDFOutput) - (long "mdf-out" <> help "With --mdf, output MDF dictionary") - , flag' (ApplyRules NoHighlight MDFOutputWithEtymons) - (long "etymons" <> help "With --mdf, output MDF dictionary with etymologies") - , flag' (ApplyRules NoHighlight WordsWithProtoOutput) - (long "show-input" <> help "Output an input→output wordlist") - , flag - (ApplyRules NoHighlight WordsOnlyOutput) - (ApplyRules NoHighlight WordsOnlyOutput) - (long "wordlist" <> help "Output only a list of the derived words (default)") - ] + <*> (asum + [ flag' (const ReportRulesApplied) + (long "report" <> help "Report rules applied rather than outputting words") + , flag' (ApplyRules NoHighlight MDFOutput) + (long "mdf-out" <> help "With --mdf, output MDF dictionary") + , flag' (ApplyRules NoHighlight MDFOutputWithEtymons) + (long "etymons" <> help "With --mdf, output MDF dictionary with etymologies") + , flag' (ApplyRules NoHighlight WordsWithProtoOutput) + (long "show-input" <> help "Output an input→output wordlist") + , flag + (ApplyRules NoHighlight WordsOnlyOutput) + (ApplyRules NoHighlight WordsOnlyOutput) + (long "wordlist" <> help "Output only a list of the derived words (default)") + ] + <*> strOption + (long "separator" <> short 's' <> value "/" <> help "Separator between multiple results (default: /)")) <*> optional (strOption (long "in" <> short 'i' <> help "File containing input words (if not specified will read from stdin)")) <*> optional (strOption diff --git a/cli/Server.hs b/cli/Server.hs index 5cd5daa..0150d39 100644 --- a/cli/Server.hs +++ b/cli/Server.hs @@ -35,6 +35,7 @@ data Request , hlMode :: HighlightMode , outMode :: OutputMode , prev :: Maybe [Component PWord] + , sep :: String } | ReqParadigm { pText :: String @@ -95,7 +96,7 @@ parseTokeniseAndApplyRulesWrapper ReqRules{..} = let mode = if report then ReportRulesApplied - else ApplyRules hlMode outMode + else ApplyRules hlMode outMode sep in case parseSoundChanges changes of Left e -> RespError $ "
" ++ errorBundlePretty e ++ "
" Right statements -> diff --git a/gui/brassica-gui/brassicaprocess.cpp b/gui/brassica-gui/brassicaprocess.cpp index fa2af25..d06674a 100644 --- a/gui/brassica-gui/brassicaprocess.cpp +++ b/gui/brassica-gui/brassicaprocess.cpp @@ -40,7 +40,8 @@ QString BrassicaProcess::parseTokeniseAndApplyRules( InputLexiconFormat inFmt, HighlightMode hlMode, OutputMode outMode, - QJsonValue *&prev) + QJsonValue *&prev, + QString sep) { QJsonObject req = QJsonObject(); req.insert("method", "Rules"); @@ -51,6 +52,7 @@ QString BrassicaProcess::parseTokeniseAndApplyRules( req.insert("hlMode", toJson(hlMode)); req.insert("outMode", toJson(outMode)); req.insert("prev", *prev); + req.insert("sep", sep); QJsonObject obj = request(QJsonDocument(req)).object(); QString method = obj.value("method").toString(); diff --git a/gui/brassica-gui/brassicaprocess.h b/gui/brassica-gui/brassicaprocess.h index d6b3e9b..e69a81d 100644 --- a/gui/brassica-gui/brassicaprocess.h +++ b/gui/brassica-gui/brassicaprocess.h @@ -40,7 +40,7 @@ class BrassicaProcess : public QObject InputLexiconFormat inFmt, HighlightMode hlMode, OutputMode outMode, - QJsonValue *&prev); + QJsonValue *&prev, QString sep); QString parseAndBuildParadigm(QString paradigm, QString roots); private: diff --git a/gui/brassica-gui/mainwindow.cpp b/gui/brassica-gui/mainwindow.cpp index 45971e7..ebdf0da 100644 --- a/gui/brassica-gui/mainwindow.cpp +++ b/gui/brassica-gui/mainwindow.cpp @@ -147,6 +147,21 @@ void MainWindow::setupWidgets(QWidget *central) synchroniseScrolls->setChecked(true); midLayout->addWidget(synchroniseScrolls); + QHBoxLayout *multiResultLayout = new QHBoxLayout(); + multiResultLayout->setAlignment(Qt::AlignLeft); + midLayout->addLayout(multiResultLayout); + + QLabel *multiResultLabel = new QLabel("Multiple result separator:"); + multiResultLayout->addWidget(multiResultLabel); + + multiResultSep = new QLineEdit("/"); + multiResultSep->setMaximumSize( + 100, + multiResultSep->maximumSize().height()); + multiResultLayout->addWidget(multiResultSep); + + multiResultLayout->addStretch(); + QLabel *outputLbl = new QLabel("Output lexicon:"); outputEdit = new QTextEdit; outputEdit->setReadOnly(true); @@ -236,7 +251,8 @@ void MainWindow::applySoundChanges(bool live, bool reportRules) infmt, checkedHl, outMode, - prev); + prev, + multiResultSep->text()); blockScrollTrackingEvent = true; outputEdit->setHtml(output); diff --git a/gui/brassica-gui/mainwindow.h b/gui/brassica-gui/mainwindow.h index 618cb08..a2b6f75 100644 --- a/gui/brassica-gui/mainwindow.h +++ b/gui/brassica-gui/mainwindow.h @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -41,6 +42,7 @@ class MainWindow : public QMainWindow QPushButton *reportRulesBtn; QCheckBox *viewLive; QCheckBox *synchroniseScrolls; + QLineEdit *multiResultSep; QTextEdit *outputEdit; QScrollBar *outputEditVScroll; diff --git a/gui/brassica-interop-wasm/src/BrassicaInterop.hs b/gui/brassica-interop-wasm/src/BrassicaInterop.hs index 69bd62e..553bc47 100644 --- a/gui/brassica-interop-wasm/src/BrassicaInterop.hs +++ b/gui/brassica-interop-wasm/src/BrassicaInterop.hs @@ -35,6 +35,8 @@ parseTokeniseAndApplyRules_hs -> Int -- ^ length of changes -> CString -- ^ words -> Int -- ^ length of words + -> CString -- ^ separator + -> Int -- ^ length of separator -> CBool -- ^ report rules applied? -> CInt -- ^ input format -> CInt -- ^ highlighting mode @@ -46,6 +48,8 @@ parseTokeniseAndApplyRules_hs changesRawLen wsRaw wsRawLen + sepRaw + sepRawLen (CBool report) infmtC hlModeC @@ -54,6 +58,7 @@ parseTokeniseAndApplyRules_hs = do changesText <- GHC.peekCStringLen utf8 (changesRaw, changesRawLen) wsText <- GHC.peekCStringLen utf8 (wsRaw, wsRawLen) + sepText <- GHC.peekCStringLen utf8 (sepRaw, sepRawLen) prevRef <- deRefStablePtr prevPtr prev <- readIORef prevRef @@ -64,7 +69,7 @@ parseTokeniseAndApplyRules_hs mode = if report == 1 then ReportRulesApplied - else ApplyRules hlMode outMode + else ApplyRules hlMode outMode sepText case parseSoundChanges changesText of Left e -> newStableCStringLen $ "
" ++ errorBundlePretty e ++ "
" @@ -104,6 +109,8 @@ foreign export ccall parseTokeniseAndApplyRules_hs -> Int -> CString -> Int + -> CString + -> Int -> CBool -> CInt -> CInt diff --git a/gui/brassica-web/index.js b/gui/brassica-web/index.js index e4b7af7..73d4cb0 100644 --- a/gui/brassica-web/index.js +++ b/gui/brassica-web/index.js @@ -44,9 +44,10 @@ const decoder = new TextDecoder(); const results = hs.initResults(); // NB: not const on Haskell side! -function applyChanges(changes, words, reportRules, highlightMode, outputMode) { +function applyChanges(changes, words, sep, reportRules, highlightMode, outputMode) { const inputChanges = encoder.encode(changes); const inputWords = encoder.encode(words); + const sepEncoded = encoder.encode(sep); const reportRulesC = reportRules ? 1 : 0; @@ -64,15 +65,18 @@ function applyChanges(changes, words, reportRules, highlightMode, outputMode) { var output = ""; withBytesPtr(inputChanges, (inputChangesPtr, inputChangesLen) => { withBytesPtr(inputWords, (inputWordsPtr, inputWordsLen) => { - try { - const outputStableCStringLen = hs.parseTokeniseAndApplyRules_hs( - inputChangesPtr, inputChangesLen, - inputWordsPtr, inputWordsLen, - reportRules, 0, hlModeC, outModeC, results); - output = decodeStableCStringLen(outputStableCStringLen); - } catch (err) { - output = err; - } + withBytesPtr(sepEncoded, (sepPtr, sepLen) => { + try { + const outputStableCStringLen = hs.parseTokeniseAndApplyRules_hs( + inputChangesPtr, inputChangesLen, + inputWordsPtr, inputWordsLen, + sepPtr, sepLen, + reportRules, 0, hlModeC, outModeC, results); + output = decodeStableCStringLen(outputStableCStringLen); + } catch (err) { + output = err; + } + }); }); }); return output; @@ -207,10 +211,11 @@ function updateForm(reportRules, needsLive) { const data = new FormData(form); const rules = rulesEditor.getValue(); const words = data.get("words"); + const sep = data.get("sep"); const highlightMode = data.get("highlightMode"); const outputFormat = data.get("outputFormat"); - const output = applyChanges(rules, words, reportRules, highlightMode, outputFormat); + const output = applyChanges(rules, words, sep, reportRules, highlightMode, outputFormat); document.getElementById("results").innerHTML = output; } diff --git a/gui/brassica-web/static/index.html b/gui/brassica-web/static/index.html index 65ff3c6..b5b94cd 100644 --- a/gui/brassica-web/static/index.html +++ b/gui/brassica-web/static/index.html @@ -239,6 +239,10 @@

+
+ + +
diff --git a/src/Brassica/SoundChange/Frontend/Internal.hs b/src/Brassica/SoundChange/Frontend/Internal.hs index 9c808ee..7e9e8d9 100644 --- a/src/Brassica/SoundChange/Frontend/Internal.hs +++ b/src/Brassica/SoundChange/Frontend/Internal.hs @@ -27,7 +27,7 @@ import Brassica.SoundChange.Types -- | Rule application mode of the SCA. data ApplicationMode - = ApplyRules HighlightMode OutputMode + = ApplyRules HighlightMode OutputMode String | ReportRulesApplied deriving (Show, Eq) @@ -78,7 +78,7 @@ instance Enum TokenisationMode where toEnum _ = undefined tokenisationModeFor :: ApplicationMode -> TokenisationMode -tokenisationModeFor (ApplyRules _ MDFOutputWithEtymons) = AddEtymons +tokenisationModeFor (ApplyRules _ MDFOutputWithEtymons _) = AddEtymons tokenisationModeFor _ = Normal -- | Output of a single application of rules to a wordlist: either a @@ -164,18 +164,18 @@ parseTokeniseAndApplyRules statements ws intype mode prev = AppliedRulesTable $ mapMaybe toPWordLog $ concat $ getWords $ componentise WordsOnlyOutput [] $ applyChangesWithLog statements' <$> toks - ApplyRules DifferentToLastRun mdfout -> - let result = concatMap (splitMultipleResults " ") $ + ApplyRules DifferentToLastRun mdfout sep -> + let result = concatMap (splitMultipleResults sep) $ componentise mdfout (fmap pure ws') $ applyChanges statements' <$> toks in HighlightedWords $ zipWithComponents result (fromMaybe [] prev) [] $ \thisWord prevWord -> (thisWord, thisWord /= prevWord) - ApplyRules DifferentToInput mdfout -> - HighlightedWords $ concatMap (splitMultipleResults " ") $ + ApplyRules DifferentToInput mdfout sep -> + HighlightedWords $ concatMap (splitMultipleResults sep) $ componentise mdfout (fmap (pure . (,False)) ws') $ applyChangesWithChanges statements' <$> toks - ApplyRules NoHighlight mdfout -> - HighlightedWords $ (fmap.fmap) (,False) $ concatMap (splitMultipleResults " ") $ + ApplyRules NoHighlight mdfout sep -> + HighlightedWords $ (fmap.fmap) (,False) $ concatMap (splitMultipleResults sep) $ componentise mdfout (fmap pure ws') $ applyChanges statements' <$> toks where From 1fc6432a76c99f690ab6dae2f224c44594f1a60d Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 15:41:00 +1100 Subject: [PATCH 06/26] Bugfix: reinstate behaviour of final tilde --- src/Brassica/SoundChange/Category.hs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Brassica/SoundChange/Category.hs b/src/Brassica/SoundChange/Category.hs index 324a7ae..e1dc97a 100644 --- a/src/Brassica/SoundChange/Category.hs +++ b/src/Brassica/SoundChange/Category.hs @@ -75,10 +75,19 @@ expand cs (CategorySpec spec) = FromElements <$> foldM go [] spec Subtract -> es \\ new expandLexeme :: Categories -> Lexeme CategorySpec a -> Either ExpandError (Lexeme Expanded a) -expandLexeme cs (Grapheme (GMulti g)) = Right $ - case lookup g cs of - Just c -> Category c - Nothing -> Grapheme (GMulti g) +expandLexeme cs (Grapheme (GMulti g)) + | Just (g', '~') <- unsnoc g + = Right $ Grapheme $ GMulti g' + | otherwise = Right $ + case lookup g cs of + Just c -> Category c + Nothing -> Grapheme (GMulti g) + where + -- taken from base-4.19 + unsnoc :: [a] -> Maybe ([a], a) + unsnoc = foldr (\x -> Just . maybe ([], x) (\(~(a, b)) -> (x : a, b))) Nothing + {-# INLINABLE unsnoc #-} + expandLexeme _ (Grapheme GBoundary) = Right $ Grapheme GBoundary expandLexeme cs (Category c) = Category <$> expand cs c expandLexeme cs (Optional ls) = Optional <$> traverse (expandLexeme cs) ls From aee7d4da1977784f08bb2d2b2ed1074cd27bf060 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 15:51:56 +1100 Subject: [PATCH 07/26] Fix backreference documentation --- Documentation.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/Documentation.md b/Documentation.md index d3e2044..a67ee47 100644 --- a/Documentation.md +++ b/Documentation.md @@ -587,7 +587,7 @@ For instance, the following rule will cause a nasal to assimilate in place of ar ╚══════════════════════╝ ``` -Backreferences can also be used in the target, in which case they match repeated or corresponding graphemes. +Backreferences can also be used in the target or environment, in which case they match repeated or corresponding graphemes. For instance, consider a rule deleting ⟨ə⟩ between identical consonants. This may be written: ``` @@ -610,12 +610,6 @@ This may be written as: └──────────────────────┘ ``` -Backreferences in the environment have a similar meaning to backreferences in the target. -However, they have a limitation: - categories after the target are counted separately from categories before the target. -Thus, it is invalid to use an environment such as `V _ @1 V`, since this backreference stretches across the target. -This limitation should be lifted in a future version of Brassica. - ### Sporadic rules and multiple results Sometimes we might want a sound change rule with more than one possible output. From d28b0699853bccd19cc9a296ba6ba9a8c501b16f Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 15:52:05 +1100 Subject: [PATCH 08/26] Remember to add entries to changelog --- ChangeLog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ChangeLog.md b/ChangeLog.md index eb68346..9dcce3a 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,9 @@ ## Unreleased changes - Allow lexeme sequences in categories using `{…}` syntax +- Allow backreferences to occur in the environment +- Allow user to choose separator used between multiple results (previously a space) +- Internal refactor: category expansion is now separate from parsing ## v0.1.1 From 7043e529a4f42e1a37a60f098e6e2a907e608f02 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 17:08:28 +1100 Subject: [PATCH 09/26] Add `--version` command-line option --- ChangeLog.md | 1 + cli/Main.hs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/ChangeLog.md b/ChangeLog.md index 9dcce3a..f807ebf 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -6,6 +6,7 @@ - Allow backreferences to occur in the environment - Allow user to choose separator used between multiple results (previously a space) - Internal refactor: category expansion is now separate from parsing +- Add `--version` command-line option ## v0.1.1 diff --git a/cli/Main.hs b/cli/Main.hs index bece0e9..a2142b1 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -33,7 +33,7 @@ main = execParser opts >>= \case .| outC where - opts = info (args <**> helper) fullDesc + opts = info (args <**> helper <**> simpleVersioner "v0.1.1") fullDesc args = batchArgs <|> serverArgs serverArgs = flag' Server (long "server" <> help "Run server (for internal use only)") From d89a1485d6e0d4b08a0a8458ef614caea04d1250 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 20 Dec 2023 19:39:51 +1100 Subject: [PATCH 10/26] Allow and suggest building with GHCup cross bindist --- BUILDING.md | 7 +++++-- cabal-wasm.project | 4 ++++ cabal.project | 6 +----- 3 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 cabal-wasm.project diff --git a/BUILDING.md b/BUILDING.md index 733f9b6..20d4a89 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -60,18 +60,21 @@ Finally, use [NSIS](https://nsis.sourceforge.io/Main_Page) with the given `insta ## Online version Building the online version of Brassica is slightly more difficult as it requires the WebAssembly backend of GHC. +It is easiest to get this using GHCup’s [WASM cross bindists](https://www.haskell.org/ghcup/guide/#cross-support). You will also need [Wizer](https://github.com/bytecodealliance/wizer) to pre-initialise the WASM binary, and [npm](https://www.npmjs.com/) for JavaScript package management. -Follow the instructions at [ghc-wasm-meta](https://gitlab.haskell.org/ghc/ghc-wasm-meta) to install this version. Then: 1. In `./gui/brassica-interop-wasm`, run the following commands: ``` - wasm32-wasi-cabal build brassica-interop-wasm + cabal build --project-file=cabal-wasm.project brassica-interop-wasm wizer --allow-wasi --wasm-bulk-memory true "$(wasm32-wasi-cabal list-bin -v0 brassica-interop-wasm)" -o "./dist/brassica-interop-wasm.wasm" ``` This will create a file `./gui/brassica-interop-wasm/dist/brassica-interop-wasm.wasm` containing the WASM binary. + + (Note: it can also be convenient to set `CABAL_DIR` so that WASM packages are installed to a different location.) + 2. In `./gui/brassica-web`, run the following commands to copy the required files into `./gui/brassica-web/static`: ``` npm install diff --git a/cabal-wasm.project b/cabal-wasm.project new file mode 100644 index 0000000..864e79d --- /dev/null +++ b/cabal-wasm.project @@ -0,0 +1,4 @@ +packages: ./brassica.cabal + ./gui/brassica-interop-wasm/brassica-interop-wasm.cabal +with-compiler: wasm32-wasi-ghc +with-hc-pkg: wasm32-wasi-ghc-pkg diff --git a/cabal.project b/cabal.project index 549f7c4..9ffce3e 100644 --- a/cabal.project +++ b/cabal.project @@ -1,5 +1 @@ -if(os(wasi)) - packages: ./brassica.cabal - ./gui/brassica-interop-wasm/brassica-interop-wasm.cabal -else - packages: ./brassica.cabal +packages: ./brassica.cabal From 74a1829b727f8ee46855deddcd40ea6e36e50765 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Mon, 1 Jan 2024 12:12:43 +1100 Subject: [PATCH 11/26] Add instance IsString Grapheme --- src/Brassica/SoundChange/Types.hs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Brassica/SoundChange/Types.hs b/src/Brassica/SoundChange/Types.hs index 090ff73..4ad8565 100644 --- a/src/Brassica/SoundChange/Types.hs +++ b/src/Brassica/SoundChange/Types.hs @@ -57,6 +57,7 @@ module Brassica.SoundChange.Types import Control.DeepSeq (NFData(..)) import Data.Kind (Constraint) +import Data.String (IsString(..)) import GHC.Generics (Generic) import GHC.OldList (dropWhileEnd) import GHC.TypeLits @@ -84,6 +85,9 @@ data Grapheme | GBoundary -- ^ A non-letter element representing a word boundary which sound changes can manipulate deriving (Eq, Ord, Show, Generic, NFData) +instance IsString Grapheme where + fromString = GMulti + -- | A word (or a subsequence of one) can be viewed as a list of -- @Grapheme@s: e.g. Portuguese "filha" becomes -- @["f", "i", "lh", "a"] :: 'PWord'@. From beed0a8ce9ede4e79dcbd33017c555210ac6e596 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Mon, 1 Jan 2024 13:16:22 +1100 Subject: [PATCH 12/26] Get benchmarks working again --- bench/Changes.hs | 28 +++++++++++++++++----------- brassica.cabal | 6 +++--- src/Brassica/SoundChange/Types.hs | 12 ++++++------ 3 files changed, 26 insertions(+), 20 deletions(-) diff --git a/bench/Changes.hs b/bench/Changes.hs index 5c3ee91..638ad11 100644 --- a/bench/Changes.hs +++ b/bench/Changes.hs @@ -1,13 +1,15 @@ +{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TemplateHaskell #-} module Main where -import Criterion.Main (defaultMain, bench, nf, bgroup) +import Criterion.Main (defaultMain, bench, nf, bgroup, Benchmark) import Data.FileEmbed (embedFile) import Data.Text (unpack) import Data.Text.Encoding (decodeUtf8) import Brassica.SoundChange +import Brassica.SoundChange.Frontend.Internal main :: IO () main = defaultMain @@ -33,16 +35,19 @@ main = defaultMain [ bench "parse" $ nf parseSoundChanges manyChanges , bench "parseRun" $ case parseSoundChanges manyChanges of Left _ -> error "invalid changes file" - Right cs -> case withFirstCategoriesDecl tokeniseWords cs manyWords of - Left _ -> error "invalid words file" - Right ws -> nf (fmap $ applyChanges cs) $ getWords ws + Right cs -> nf (parseTokeniseAndApplyRules + cs + manyWords + Raw + (ApplyRules NoHighlight WordsOnlyOutput "/")) + Nothing ] ] where basic = Rule { target = [Grapheme "a"] , replacement = [Grapheme "b"] - , environment = ([], []) + , environment = [([], [])] , exception = Nothing , flags = defFlags , plaintext = "a/b" @@ -50,23 +55,24 @@ main = defaultMain complex = Rule { target = - [ Category [GraphemeEl "t", GraphemeEl "d", GraphemeEl "n"] + [ Category $ FromElements $ Left <$> ["t", "d", "n"] , Optional [Grapheme "y"] - , Category [GraphemeEl "i", GraphemeEl "e"] + , Category $ FromElements $ Left <$> ["i", "e"] ] , replacement = - [ Category [GraphemeEl "c", GraphemeEl "j", GraphemeEl "nh"] + [ Category $ FromElements $ Left <$> ["c", "j", "nh"] , Optional [Geminate] ] - , environment = - ( [Category [BoundaryEl, GraphemeEl "a", GraphemeEl "e", GraphemeEl "i"]] - , [Category [GraphemeEl "a", GraphemeEl "e", GraphemeEl "i", GraphemeEl "o", GraphemeEl "u"]] + , environment = pure + ( [Category $ FromElements $ Left <$> [GBoundary, "a", "e", "i"]] + , [Category $ FromElements $ Left <$> ["a", "e", "i", "o", "u"]] ) , exception = Nothing , flags = defFlags , plaintext = "[t d n] (y) [i e] / [č j ñ] (>) / [# a e i] _ [a e i o u]" } + benchChanges :: Rule Expanded -> PWord -> [Benchmark] benchChanges cs l = -- [ bench "log" $ nf (applyStatementWithLogs (RuleS cs)) l -- given the implementation of logging, the above benchmark doesn't help very much at all diff --git a/brassica.cabal b/brassica.cabal index f9a737d..0916527 100644 --- a/brassica.cabal +++ b/brassica.cabal @@ -66,7 +66,7 @@ executable brassica , conduit ^>=1.3 , conduit-extra ^>=1.3 , deepseq >=1.4 && <1.5 - , optparse-applicative ^>=0.17 + , optparse-applicative ^>=0.17 || ^>=0.18 , text >=1.2 && <2.1 default-language: Haskell2010 @@ -79,7 +79,7 @@ benchmark changes-bench build-depends: base >=4.7 && <5 , brassica - , criterion >=1.5 && <1.6 + , criterion >=1.5 && <1.7 , file-embed >=0.0.15 && <0.0.16 , text >=1.2 && <1.3 @@ -93,7 +93,7 @@ benchmark paradigm-bench build-depends: base >=4.7 && <5 , brassica - , criterion >=1.5 && <1.6 + , criterion >=1.5 && <1.7 default-language: Haskell2010 diff --git a/src/Brassica/SoundChange/Types.hs b/src/Brassica/SoundChange/Types.hs index 4ad8565..d945991 100644 --- a/src/Brassica/SoundChange/Types.hs +++ b/src/Brassica/SoundChange/Types.hs @@ -177,7 +177,7 @@ mapCategoryA f (Multiple c) = Multiple <$> f c -- | The type of a category after expansion. newtype Expanded a = FromElements { elements :: [Either Grapheme [Lexeme Expanded a]] } - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Generic, NFData) instance Semigroup (Expanded a) where (FromElements es) <> (FromElements es') = FromElements (es <> es') @@ -291,13 +291,13 @@ type SoundChanges c decl = [Statement c decl] -- | The individual operations used to construct a category in -- Brassica sound-change syntax. data CategoryModification = Union | Intersect | Subtract - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Generic, NFData) -- | The specification of a category in Brassica sound-change syntax. data CategorySpec a = CategorySpec [(CategoryModification, Either Grapheme [Lexeme CategorySpec a])] | MustInline String -- ^ A single grapheme assumed to have been specified earlier as a category - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Generic, NFData) -- | The specification of a suprasegmental feature in Brassica -- sound-change syntax. @@ -306,15 +306,15 @@ data FeatureSpec = FeatureSpec , featureBaseValues :: CategorySpec 'AnyPart , featureDerived :: [(String, CategorySpec 'AnyPart)] } - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Generic, NFData) -- | A definition of a new category, either directly or via features. data CategoryDefinition = DefineCategory String (CategorySpec 'AnyPart) | DefineFeature FeatureSpec - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Generic, NFData) -- | A directive used in Brassica sound-change syntax: currently only -- @categories … end@ or @new categories … end@ data Directive = Categories Bool [CategoryDefinition] - deriving (Show, Eq, Ord) + deriving (Show, Eq, Ord, Generic, NFData) From 485e5ea5dab607174493d7fd2d16341cd9aeea01 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Mon, 1 Jan 2024 14:14:45 +1100 Subject: [PATCH 13/26] Performance improvement: use Vector for MultiZipper Profiling indicates that Brassica spends a lot of time in 'value', which does indexing. Thus using a data structure with fast indexing seems desirable. By my measurements, this gives a >50% speedup, especially for longer input words. --- brassica.cabal | 1 + src/Brassica/SoundChange/Apply/Internal.hs | 2 +- .../SoundChange/Apply/Internal/MultiZipper.hs | 91 +++++++++---------- 3 files changed, 45 insertions(+), 49 deletions(-) diff --git a/brassica.cabal b/brassica.cabal index 0916527..5461f41 100644 --- a/brassica.cabal +++ b/brassica.cabal @@ -49,6 +49,7 @@ library , parser-combinators >=1.2 && <1.3 , split >=0.2 && <0.3 , transformers >=0.5 && <0.7 + , vector >=0.13 && <0.14 default-language: Haskell2010 diff --git a/src/Brassica/SoundChange/Apply/Internal.hs b/src/Brassica/SoundChange/Apply/Internal.hs index f5efa43..3c9319f 100644 --- a/src/Brassica/SoundChange/Apply/Internal.hs +++ b/src/Brassica/SoundChange/Apply/Internal.hs @@ -360,7 +360,7 @@ applyOnce r@Rule{target, replacement, exception} = if maybe True (`elem` exs) p then return False else do - modifyMay $ modifyBetween (TargetStart, TargetEnd) $ const [] + modifyMay $ delete (TargetStart, TargetEnd) modifyMay $ seek TargetStart modifyM $ mkReplacement out replacement return True diff --git a/src/Brassica/SoundChange/Apply/Internal/MultiZipper.hs b/src/Brassica/SoundChange/Apply/Internal/MultiZipper.hs index 962dd83..a6cfd5a 100644 --- a/src/Brassica/SoundChange/Apply/Internal/MultiZipper.hs +++ b/src/Brassica/SoundChange/Apply/Internal/MultiZipper.hs @@ -1,4 +1,5 @@ {-# LANGUAGE DeriveTraversable #-} +{-# LANGUAGE TupleSections #-} {-| __Warning:__ This module is __internal__, and does __not__ follow the Package Versioning Policy. It may be useful for extending @@ -37,13 +38,16 @@ module Brassica.SoundChange.Apply.Internal.MultiZipper , query , untag , untagWhen - , modifyBetween + , delete , extend , extend' ) where import Control.Applicative (Alternative((<|>))) import Data.Foldable (Foldable(foldl')) +import Data.Vector ((!?), (!)) +import Data.Vector.Mutable (write) +import qualified Data.Vector as V import qualified Data.Map.Strict as M -- | A 'MultiZipper' is a list zipper (list+current index), with the @@ -64,25 +68,25 @@ import qualified Data.Map.Strict as M -- 'MultiZipper' and then move to the next element immediately after -- the processed portion, allowing another function to be run to -- process the next part of the 'MultiZipper'.) -data MultiZipper t a = MultiZipper [a] Int (M.Map t Int) +data MultiZipper t a = MultiZipper (V.Vector a) Int (M.Map t Int) deriving (Show, Functor, Foldable, Traversable) -- | Convert a list to a 'MultiZipper' positioned at the start of that -- list. fromListStart :: [a] -> MultiZipper t a -fromListStart as = MultiZipper as 0 M.empty +fromListStart as = MultiZipper (V.fromList as) 0 M.empty -- | Convert a list to a 'MultiZipper' at a specific position in the -- list. Returns 'Nothing' if the index is invalid. fromListPos :: [a] -> Int -> Maybe (MultiZipper t a) fromListPos as pos = - if invalid pos as + if invalid pos (length as) then Nothing - else Just $ MultiZipper as pos M.empty + else Just $ MultiZipper (V.fromList as) pos M.empty -- | Get the list stored in a 'MultiZipper'. toList :: MultiZipper t a -> [a] -toList (MultiZipper as _ _) = as +toList (MultiZipper as _ _) = V.toList as -- | The current position of the 'MultiZipper'. curPos :: MultiZipper t a -> Int @@ -108,10 +112,7 @@ atBoundary = (||) <$> atStart <*> atEnd -- list’ (recall this actually means that the 'MultiZipper' is -- positioned /after/ the last element of its list). value :: MultiZipper t a -> Maybe a -value (MultiZipper as pos _) = - if atNonvalue pos as - then Nothing - else Just $ as !! pos +value (MultiZipper as pos _) = as !? pos -- | @valueN n mz@ returns the next @n@ elements of @mz@ starting from -- the current position, as well as returning a new 'MultiZipper' @@ -122,9 +123,9 @@ value (MultiZipper as pos _) = valueN :: Int -> MultiZipper t a -> Maybe ([a], MultiZipper t a) valueN i (MultiZipper as pos ts) = let pos' = pos + i in - if invalid pos' as || i < 0 + if invalid pos' (V.length as) || i < 0 then Nothing - else Just (take i $ drop pos as, MultiZipper as pos' ts) + else Just (take i $ drop pos $ V.toList as, MultiZipper as pos' ts) -- | Given a tag, return its position locationOf :: Ord t => t -> MultiZipper t a -> Maybe Int @@ -136,7 +137,7 @@ query (MultiZipper _ pos ts) = M.keys $ M.filter (==pos) ts seekIx :: Int -> MultiZipper t a -> Maybe (MultiZipper t a) seekIx i (MultiZipper as _ ts) = - if invalid i as + if invalid i (V.length as) then Nothing else Just (MultiZipper as i ts) @@ -159,9 +160,7 @@ bwd = move (-1) -- over consume :: MultiZipper t a -> Maybe (a, MultiZipper t a) consume (MultiZipper as pos ts) = - if invalid (pos+1) as - then Nothing - else Just (as!!pos, MultiZipper as (pos+1) ts) + fmap (,MultiZipper as (pos+1) ts) (as!?pos) -- | Move the 'MultiZipper' to be at the specified tag. Returns -- 'Nothing' if that tag is not present. @@ -186,8 +185,11 @@ yank p mz = bwd mz >>= \mz' -> (value mz' >>= p) <|> yank p mz' -- | Insert a new element at point and move forward by one position. insert :: a -> MultiZipper t a -> MultiZipper t a insert a (MultiZipper as pos ts) = - case splitAt pos as of - (as1, as2) -> MultiZipper (as1 ++ [a] ++ as2) (pos+1) $ correctIxsFrom pos (+1) ts + case V.splitAt pos as of + (as1, as2) -> MultiZipper + (as1 V.++ V.cons a as2) + (pos+1) + (correctIxsFrom pos (+1) ts) -- | Insert multiple elements at point and move after them. A simple -- wrapper around 'insert'. @@ -204,11 +206,9 @@ zap p = \mz@(MultiZipper as pos ts) -> case go as (pos-1) of go _ (-1) = Nothing go as pos | pos == length as = go as (pos-1) - | otherwise = case p (as !! pos) of + | otherwise = case p (as ! pos) of Nothing -> go as (pos-1) - Just a' -> case splitAt pos as of - (as1, _:as2) -> Just $ as1 ++ (a':as2) - _ -> error "error in zap: impossible case reached" + Just a' -> Just $ V.modify (\v -> write v pos a') as -- | Set a tag at the current position. tag :: Ord t => t -> MultiZipper t a -> MultiZipper t a @@ -217,7 +217,7 @@ tag t (MultiZipper as pos ts) = MultiZipper as pos $ M.insert t pos ts -- | Set a tag at a given position if possible, otherwise return 'Nothing'. tagAt :: Ord t => t -> Int -> MultiZipper t a -> Maybe (MultiZipper t a) tagAt t i (MultiZipper as pos ts) = - if invalid i as + if invalid i (length as) then Nothing else Just $ MultiZipper as pos $ M.insert t i ts @@ -229,25 +229,23 @@ untagWhen p (MultiZipper as pos ts) = MultiZipper as pos $ snd $ M.partitionWith untag :: MultiZipper t a -> MultiZipper t a untag (MultiZipper as pos _) = MultiZipper as pos M.empty --- | Modify a 'MultiZipper' between the selected tags. Returns --- 'Nothing' if a nonexistent tag is selected, else returns the --- modified 'MultiZipper'. -modifyBetween :: Ord t - => (t, t) - -- ^ Selected tags. Note that the resulting interval - -- will be [inclusive, exclusive). - -> ([a] -> [a]) - -- ^ Function to modify designated interval. - -> MultiZipper t a - -> Maybe (MultiZipper t a) -modifyBetween (t1, t2) f mz@(MultiZipper as pos ts) = do +-- | Delete the portion of a 'MultiZipper' between the selected tags. +-- Returns 'Nothing' if a nonexistent tag is selected, else returns +-- the modified 'MultiZipper'. +delete + :: Ord t + => (t, t) + -- ^ Selected tags. Note that the resulting interval + -- will be [inclusive, exclusive). + -> MultiZipper t a + -> Maybe (MultiZipper t a) +delete (t1, t2) mz@(MultiZipper as pos ts) = do (i1, i2) <- fmap correctOrder $ (,) <$> locationOf t1 mz <*> locationOf t2 mz - let (before_t1, after_t1) = splitAt i1 as - (cut_part, after_t2) = splitAt (i2-i1) after_t1 - replacement = f cut_part - dEnd = length replacement - length cut_part - pos' = pos + dEnd - return $ MultiZipper (before_t1 ++ replacement ++ after_t2) pos' (correctIxsFrom i2 (+dEnd) ts) + let (before_t1, after_t1) = V.splitAt i1 as + (cut_part, after_t2) = V.splitAt (i2-i1) after_t1 + removed = length cut_part + pos' = pos - removed + return $ MultiZipper (before_t1 V.++ after_t2) pos' (correctIxsFrom i2 (subtract removed) ts) where correctOrder (m, n) = if m <= n then (m, n) else (n, m) @@ -261,21 +259,18 @@ modifyBetween (t1, t2) f mz@(MultiZipper as pos ts) = do extend :: (MultiZipper t a -> b) -> MultiZipper t a -> MultiZipper t b extend f (MultiZipper as pos ts) = MultiZipper as' pos ts where - as' = fmap (\i -> f $ MultiZipper as i ts) [0 .. length as - 1] + as' = V.map (\i -> f $ MultiZipper as i ts) $ V.enumFromN 0 (length as) -- | Like 'extend', but includes the end position of the zipper, thus -- increasing the 'MultiZipper' length by one when called. extend' :: (MultiZipper t a -> b) -> MultiZipper t a -> MultiZipper t b extend' f (MultiZipper as pos ts) = MultiZipper as' pos ts where - as' = fmap (\i -> f $ MultiZipper as i ts) [0 .. length as] + as' = V.map (\i -> f $ MultiZipper as i ts) $ V.enumFromN 0 (length as + 1) -- Utility functions for checking and modifying indices in lists: -invalid :: Int -> [a] -> Bool -invalid pos as = (pos < 0) || (pos > length as) - -atNonvalue :: Int -> [a] -> Bool -atNonvalue pos as = (pos < 0) || (pos >= length as) +invalid :: Int -> Int -> Bool +invalid pos len = (pos < 0) || (pos > len) correctIxsFrom :: Int -> (Int -> Int) -> M.Map t Int -> M.Map t Int correctIxsFrom i f = M.map $ \pos -> if pos >= i then f pos else pos From d19ad8f3657807c061a23049f919c95370a8bf98 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Mon, 1 Jan 2024 15:53:36 +1100 Subject: [PATCH 14/26] Remember to update ChangeLog --- ChangeLog.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ChangeLog.md b/ChangeLog.md index f807ebf..4255e99 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -7,6 +7,7 @@ - Allow user to choose separator used between multiple results (previously a space) - Internal refactor: category expansion is now separate from parsing - Add `--version` command-line option +- Store `MultiZipper` data in a `Vector` rather than a linked list (for performance) ## v0.1.1 From 06a35e2991b33b0a4ba74b0089f2d23a9b27ea7c Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Tue, 2 Jan 2024 19:25:50 +1100 Subject: [PATCH 15/26] Bugfix: subtraction should remove all subtracted graphemes --- ChangeLog.md | 1 + src/Brassica/SoundChange/Category.hs | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 4255e99..1747b44 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -8,6 +8,7 @@ - Internal refactor: category expansion is now separate from parsing - Add `--version` command-line option - Store `MultiZipper` data in a `Vector` rather than a linked list (for performance) +- Bugfix: subtraction now removes all subtracted graphemes ## v0.1.1 diff --git a/src/Brassica/SoundChange/Category.hs b/src/Brassica/SoundChange/Category.hs index e1dc97a..a1de69c 100644 --- a/src/Brassica/SoundChange/Category.hs +++ b/src/Brassica/SoundChange/Category.hs @@ -22,7 +22,7 @@ import Control.DeepSeq (NFData) import Control.Monad (foldM, unless) import Control.Monad.State.Strict (StateT, evalStateT, lift, get, put) import Data.Containers.ListUtils (nubOrd) -import Data.List (intersect, (\\), transpose, foldl') +import Data.List (intersect, transpose, foldl') import Data.Maybe (mapMaybe) import GHC.Generics (Generic) @@ -72,7 +72,10 @@ expand cs (CategorySpec spec) = FromElements <$> foldM go [] spec pure $ case modifier of Union -> es ++ new Intersect -> es `intersect` new - Subtract -> es \\ new + Subtract -> es `subtractAll` new + + -- NB. normal (\\) only removes the first matching element + subtractAll xs ys = filter (`notElem` ys) xs expandLexeme :: Categories -> Lexeme CategorySpec a -> Either ExpandError (Lexeme Expanded a) expandLexeme cs (Grapheme (GMulti g)) From 1c1c5b282a9d32588a8b7df30a92ba3be4b2322d Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 3 Jan 2024 19:02:43 +1100 Subject: [PATCH 16/26] Store paradigm builder output in a tree data structure --- ChangeLog.md | 1 + cli/Server.hs | 3 +- src/Brassica/Paradigm/Apply.hs | 50 ++++++++++++++++++++++++---------- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 1747b44..1950a6f 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,6 +9,7 @@ - Add `--version` command-line option - Store `MultiZipper` data in a `Vector` rather than a linked list (for performance) - Bugfix: subtraction now removes all subtracted graphemes +- Store paradigm builder output in a tree data structure ## v0.1.1 diff --git a/cli/Server.hs b/cli/Server.hs index 0150d39..fcbc872 100644 --- a/cli/Server.hs +++ b/cli/Server.hs @@ -18,6 +18,7 @@ import Data.Aeson.Parser (json') import Data.Aeson.TH (deriveJSON, defaultOptions, defaultTaggedObject, constructorTagModifier, sumEncoding, tagFieldName) import Data.ByteString (toStrict) import Data.Conduit.Attoparsec (conduitParser) +import Data.Foldable (toList) import GHC.Generics (Generic) import System.IO (hSetBuffering, stdin, stdout, BufferMode(NoBuffering)) import System.Timeout @@ -125,7 +126,7 @@ parseAndBuildParadigmWrapper :: Request -> Response parseAndBuildParadigmWrapper ReqParadigm{..} = case parseParadigm pText of Left e -> RespError $ "
" ++ errorBundlePretty e ++ "
" - Right p -> RespParadigm $ escape $ unlines $ concatMap (applyParadigm p) $ lines input + Right p -> RespParadigm $ escape $ unlines $ concatMap (toList . applyParadigm p) $ lines input parseAndBuildParadigmWrapper _ = error "parseAndBuildParadigmWrapper: unexpected request!" escape :: String -> String diff --git a/src/Brassica/Paradigm/Apply.hs b/src/Brassica/Paradigm/Apply.hs index e7b3a3d..5b6972c 100644 --- a/src/Brassica/Paradigm/Apply.hs +++ b/src/Brassica/Paradigm/Apply.hs @@ -1,16 +1,29 @@ -{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE DeriveFoldable #-} +{-# LANGUAGE DeriveFunctor #-} +{-# LANGUAGE LambdaCase #-} -module Brassica.Paradigm.Apply (applyParadigm) where +module Brassica.Paradigm.Apply + ( ResultsTree(..) + , applyParadigm + ) where import Brassica.Paradigm.Types -import Data.List (sortOn) +import Data.Functor ((<&>)) +import Data.List (sortOn, foldl') import Data.Maybe (mapMaybe) import Data.Ord (Down(Down)) +data ResultsTree a = Node [ResultsTree a] | Result a + deriving (Show, Functor, Foldable) + +addLevel :: (a -> [a]) -> ResultsTree a -> ResultsTree a +addLevel f (Result r) = Node $ Result <$> f r +addLevel f (Node rs) = Node $ addLevel f <$> rs + -- | Apply the given 'Paradigm' to a root, to produce all possible -- derived forms. -applyParadigm :: Paradigm -> String -> [String] +applyParadigm :: Paradigm -> String -> ResultsTree String applyParadigm p w = let fs = mapMaybe getFeature p ms = mapMaybe getMapping p @@ -21,18 +34,25 @@ applyParadigm p w = getMapping (NewMapping k v) = Just (k,v) getMapping _ = Nothing - -combinations :: [Feature] -> [[Grammeme]] -combinations = go [] + +combinations :: [Feature] -> ResultsTree [Grammeme] +combinations = + (fmap.fmap) snd . foldl' go (Result []) where - go :: [(Maybe FeatureName, Grammeme)] -> [Feature] -> [[Grammeme]] - go acc [] = return $ snd <$> reverse acc - go acc (Feature c n gs : fs) = - if satisfied (flip lookup acc . Just) c - then do - g <- gs - go ((n,g) : acc) fs - else go acc fs + addFeature + :: Feature + -> [(Maybe FeatureName, Grammeme)] + -> [[(Maybe FeatureName, Grammeme)]] + addFeature (Feature c n gs) acc + | satisfied (flip lookup acc . Just) c + = gs <&> \g -> (n, g):acc + | otherwise + = [acc] + + go :: ResultsTree [(Maybe FeatureName, Grammeme)] + -> Feature + -> ResultsTree [(Maybe FeatureName, Grammeme)] + go rt f = addLevel (addFeature f) rt satisfied :: (FeatureName -> Maybe Grammeme) From 510261658c66284975e6799996da1f04cff42b2e Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 3 Jan 2024 19:37:57 +1100 Subject: [PATCH 17/26] Allow more compact output format for paradigm builder --- ChangeLog.md | 2 +- Documentation.md | 117 +++++++++------------------ cli/Server.hs | 9 ++- gui/brassica-gui/brassicaprocess.cpp | 3 +- gui/brassica-gui/brassicaprocess.h | 2 +- gui/brassica-gui/paradigmwindow.cpp | 7 +- gui/brassica-gui/paradigmwindow.h | 2 + src/Brassica/Paradigm.hs | 5 +- src/Brassica/Paradigm/Apply.hs | 20 ++++- 9 files changed, 76 insertions(+), 91 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 1950a6f..28d3f46 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -9,7 +9,7 @@ - Add `--version` command-line option - Store `MultiZipper` data in a `Vector` rather than a linked list (for performance) - Bugfix: subtraction now removes all subtracted graphemes -- Store paradigm builder output in a tree data structure +- Store paradigm builder output in a tree data structure, allowing a more compact output format ## v0.1.1 diff --git a/Documentation.md b/Documentation.md index a67ee47..c893735 100644 --- a/Documentation.md +++ b/Documentation.md @@ -858,87 +858,42 @@ Thus an example of a basic paradigm definition would be as follows: The output will then iterate through all combinations of these affixes. For instance, applying the paradigm above to the root `kood` gives: -
- -(Click to show output) - -
-
-zhaazhkoodwim
-zhaawkoodwim
-zhaaykoodwim
-woozhkoodwim
-woowkoodwim
-wooykoodwim
-yaazhkoodwim
-yaawkoodwim
-yaaykoodwim
-zhaazhkood
-zhaawkood
-zhaaykood
-woozhkood
-woowkood
-wooykood
-yaazhkood
-yaawkood
-yaaykood
-zhaazhkoodsoo
-zhaawkoodsoo
-zhaaykoodsoo
-woozhkoodsoo
-woowkoodsoo
-wooykoodsoo
-yaazhkoodsoo
-yaawkoodsoo
-yaaykoodsoo
-zhaazhkoodaa
-zhaawkoodaa
-zhaaykoodaa
-woozhkoodaa
-woowkoodaa
-wooykoodaa
-yaazhkoodaa
-yaawkoodaa
-yaaykoodaa
-zhaazhkoodwimen
-zhaawkoodwimen
-zhaaykoodwimen
-woozhkoodwimen
-woowkoodwimen
-wooykoodwimen
-yaazhkoodwimen
-yaawkoodwimen
-yaaykoodwimen
-zhaazhkooden
-zhaawkooden
-zhaaykooden
-woozhkooden
-woowkooden
-wooykooden
-yaazhkooden
-yaawkooden
-yaaykooden
-zhaazhkoodsooen
-zhaawkoodsooen
-zhaaykoodsooen
-woozhkoodsooen
-woowkoodsooen
-wooykoodsooen
-yaazhkoodsooen
-yaawkoodsooen
-yaaykoodsooen
-zhaazhkoodaaen
-zhaawkoodaaen
-zhaaykoodaaen
-woozhkoodaaen
-woowkoodaaen
-wooykoodaaen
-yaazhkoodaaen
-yaawkoodaaen
-yaaykoodaaen
-
-
-
+``` +zhaazhkoodwim zhaawkoodwim zhaaykoodwim +woozhkoodwim woowkoodwim wooykoodwim +yaazhkoodwim yaawkoodwim yaaykoodwim + +zhaazhkood zhaawkood zhaaykood +woozhkood woowkood wooykood +yaazhkood yaawkood yaaykood + +zhaazhkoodsoo zhaawkoodsoo zhaaykoodsoo +woozhkoodsoo woowkoodsoo wooykoodsoo +yaazhkoodsoo yaawkoodsoo yaaykoodsoo + +zhaazhkoodaa zhaawkoodaa zhaaykoodaa +woozhkoodaa woowkoodaa wooykoodaa +yaazhkoodaa yaawkoodaa yaaykoodaa + + +zhaazhkoodwimen zhaawkoodwimen zhaaykoodwimen +woozhkoodwimen woowkoodwimen wooykoodwimen +yaazhkoodwimen yaawkoodwimen yaaykoodwimen + +zhaazhkooden zhaawkooden zhaaykooden +woozhkooden woowkooden wooykooden +yaazhkooden yaawkooden yaaykooden + +zhaazhkoodsooen zhaawkoodsooen zhaaykoodsooen +woozhkoodsooen woowkoodsooen wooykoodsooen +yaazhkoodsooen yaawkoodsooen yaaykoodsooen + +zhaazhkoodaaen zhaawkoodaaen zhaaykoodaaen +woozhkoodaaen woowkoodaaen wooykoodaaen +yaazhkoodaaen yaawkoodaaen yaaykoodaaen +``` + +(You can also tick the option ‘Each word on its own line’, if you dislike placing multiple words on a single line.) Note that in the paradigm described above, all affixes on each line are assigned to the same slot. This is common in paradigms, so the paradigm builder has a shortcut syntax for this situation, diff --git a/cli/Server.hs b/cli/Server.hs index fcbc872..b1c2f1d 100644 --- a/cli/Server.hs +++ b/cli/Server.hs @@ -25,7 +25,7 @@ import System.Timeout import Brassica.SoundChange import Brassica.SoundChange.Frontend.Internal -import Brassica.Paradigm (applyParadigm, parseParadigm) +import Brassica.Paradigm (applyParadigm, parseParadigm, formatNested, ResultsTree (..)) data Request = ReqRules @@ -41,6 +41,7 @@ data Request | ReqParadigm { pText :: String , input :: String + , separateLines :: Bool } deriving (Show) @@ -126,7 +127,11 @@ parseAndBuildParadigmWrapper :: Request -> Response parseAndBuildParadigmWrapper ReqParadigm{..} = case parseParadigm pText of Left e -> RespError $ "
" ++ errorBundlePretty e ++ "
" - Right p -> RespParadigm $ escape $ unlines $ concatMap (toList . applyParadigm p) $ lines input + Right p -> RespParadigm $ escape $ + (if separateLines + then unlines . toList + else formatNested id) + $ Node $ applyParadigm p <$> lines input parseAndBuildParadigmWrapper _ = error "parseAndBuildParadigmWrapper: unexpected request!" escape :: String -> String diff --git a/gui/brassica-gui/brassicaprocess.cpp b/gui/brassica-gui/brassicaprocess.cpp index d06674a..45fc6db 100644 --- a/gui/brassica-gui/brassicaprocess.cpp +++ b/gui/brassica-gui/brassicaprocess.cpp @@ -65,12 +65,13 @@ QString BrassicaProcess::parseTokeniseAndApplyRules( } } -QString BrassicaProcess::parseAndBuildParadigm(QString paradigm, QString roots) +QString BrassicaProcess::parseAndBuildParadigm(QString paradigm, QString roots, bool separateLines) { QJsonObject req = QJsonObject(); req.insert("method", "Paradigm"); req.insert("pText", paradigm); req.insert("input", roots); + req.insert("separateLines", separateLines); QJsonObject obj = request(QJsonDocument(req)).object(); QString method = obj.value("method").toString(); diff --git a/gui/brassica-gui/brassicaprocess.h b/gui/brassica-gui/brassicaprocess.h index e69a81d..ed99502 100644 --- a/gui/brassica-gui/brassicaprocess.h +++ b/gui/brassica-gui/brassicaprocess.h @@ -41,7 +41,7 @@ class BrassicaProcess : public QObject HighlightMode hlMode, OutputMode outMode, QJsonValue *&prev, QString sep); - QString parseAndBuildParadigm(QString paradigm, QString roots); + QString parseAndBuildParadigm(QString paradigm, QString roots, bool separateLines); private: QProcess *proc; diff --git a/gui/brassica-gui/paradigmwindow.cpp b/gui/brassica-gui/paradigmwindow.cpp index 6312da3..3683286 100644 --- a/gui/brassica-gui/paradigmwindow.cpp +++ b/gui/brassica-gui/paradigmwindow.cpp @@ -8,6 +8,7 @@ #include #include #include +#include ParadigmWindow::ParadigmWindow(BrassicaProcess *proc, QWidget *parent) : QMainWindow(parent) @@ -47,6 +48,9 @@ ParadigmWindow::ParadigmWindow(BrassicaProcess *proc, QWidget *parent) outputEdit->setReadOnly(true); outputLayout->addWidget(outputEdit); + separateLinesBox = new QCheckBox("Each word on its own line"); + outputLayout->addWidget(separateLinesBox); + QPushButton *buildBtn = new QPushButton("Build"); outputLayout->addWidget(buildBtn); @@ -68,7 +72,8 @@ void ParadigmWindow::rebuildResult() QString paradigm = paradigmEdit->toPlainText(); QString roots = rootsEdit->toPlainText(); - QString output = proc->parseAndBuildParadigm(paradigm, roots); + QString output = proc->parseAndBuildParadigm( + paradigm, roots, separateLinesBox->isChecked()); outputEdit->setHtml(output); } diff --git a/gui/brassica-gui/paradigmwindow.h b/gui/brassica-gui/paradigmwindow.h index d231fb5..f315245 100644 --- a/gui/brassica-gui/paradigmwindow.h +++ b/gui/brassica-gui/paradigmwindow.h @@ -6,6 +6,7 @@ class BrassicaProcess; class QPlainTextEdit; class QTextEdit; +class QCheckBox; class ParadigmWindow : public QMainWindow { @@ -25,6 +26,7 @@ private slots: QPlainTextEdit *paradigmEdit; QPlainTextEdit *rootsEdit; QTextEdit *outputEdit; + QCheckBox *separateLinesBox; BrassicaProcess *proc; }; diff --git a/src/Brassica/Paradigm.hs b/src/Brassica/Paradigm.hs index 4c3affb..16379ab 100644 --- a/src/Brassica/Paradigm.hs +++ b/src/Brassica/Paradigm.hs @@ -1,6 +1,5 @@ module Brassica.Paradigm ( - -- * Types Process(..) , Affix , Grammeme(..) @@ -10,10 +9,10 @@ module Brassica.Paradigm , FeatureName(..) , Statement(..) , Paradigm - -- * Application + , ResultsTree(..) , applyParadigm - -- * Parsing , parseParadigm + , formatNested -- ** Re-export , errorBundlePretty ) where diff --git a/src/Brassica/Paradigm/Apply.hs b/src/Brassica/Paradigm/Apply.hs index 5b6972c..11e85df 100644 --- a/src/Brassica/Paradigm/Apply.hs +++ b/src/Brassica/Paradigm/Apply.hs @@ -5,12 +5,13 @@ module Brassica.Paradigm.Apply ( ResultsTree(..) , applyParadigm + , formatNested ) where import Brassica.Paradigm.Types import Data.Functor ((<&>)) -import Data.List (sortOn, foldl') +import Data.List (sortOn, foldl', intercalate) import Data.Maybe (mapMaybe) import Data.Ord (Down(Down)) @@ -21,6 +22,23 @@ addLevel :: (a -> [a]) -> ResultsTree a -> ResultsTree a addLevel f (Result r) = Node $ Result <$> f r addLevel f (Node rs) = Node $ addLevel f <$> rs +-- | Formats a 'ResultsTree' in a nested way, where the lowest-level +-- elements are separated by one space, the second-lowest are +-- separated by one newline, the third-lowest by two newlines, and so +-- on. +formatNested :: (a -> String) -> ResultsTree a -> String +formatNested f = snd . go + where + go (Result a) = (0, f a) + go (Node rts) = + let (depths, formatted) = unzip $ go <$> rts + depth = maximum depths + separator = + if depth == 0 + then " " + else replicate depth '\n' + in (1+depth, intercalate separator formatted) + -- | Apply the given 'Paradigm' to a root, to produce all possible -- derived forms. applyParadigm :: Paradigm -> String -> ResultsTree String From 07ddab87b41109cb16d76b559d1fa1cbde2130d7 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 3 Jan 2024 20:03:10 +1100 Subject: [PATCH 18/26] Remember to document abstract features This has been in the paradigm builder for a very long time, but somehow never got documented! --- ChangeLog.md | 1 + Documentation.md | 40 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 28d3f46..b5c8644 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -10,6 +10,7 @@ - Store `MultiZipper` data in a `Vector` rather than a linked list (for performance) - Bugfix: subtraction now removes all subtracted graphemes - Store paradigm builder output in a tree data structure, allowing a more compact output format +- Documented abstract features in paradigm builder (previously present but undocumented) ## v0.1.1 diff --git a/Documentation.md b/Documentation.md index c893735..1cc7f02 100644 --- a/Documentation.md +++ b/Documentation.md @@ -843,8 +843,9 @@ Thus each affix is either: - A prefix in slot *n*, specified as `-n.prefix`; - A suffix in slot *n*, specified as `n.suffix`; -- An empty affix, specified as `()`; or -- A combination of other affixes, specified as `(affix1 affix2 ...)`. +- An empty affix, specified as `()`; +- A combination of other affixes, specified as `(affix1 affix2 ...)`; or +- An *abstract feature*, specified simply as `featurename` (more on this below). Thus an example of a basic paradigm definition would be as follows: @@ -933,3 +934,38 @@ A condition takes the form `when ( )`, where `` is either `is` or `not`. The output will contain the following feature only in words where the condition is satisfied; in all other words that feature will be absent. + +Earlier, brief reference was made to *abstract features*. +These are similar to affixes, but are not associated with any one slot. +Instead, one or more abstract features can be *mapped* to an affix, + by writing on a separate line: +``` +feature1 feature2 … > affix +``` +The `affix` will then be included in the result when all of the specified features are present. +This allows for implementing fusional morphology, for instance the Latin first declension: +``` +NOM VOC ACC GEN DAT ABL +SG PL +NOM SG > 1.a +NOM PL > 1.ae +VOC SG > 1.a +VOC PL > 1.ae +ACC SG > 1.am +ACC PL > 1.ās +GEN SG > 1.ae +GEN PL > 1.ārum +DAT SG > 1.ae +DAT PL > 1.īs +ABL SG > 1.ā +ABL PL > 1.īs +``` +When applied to, for instance, `puell`, this produces: +``` +puella puellae +puella puellae +puellam puellās +puellae puellārum +puellae puellīs +puellā puellīs +``` From aa5ab0c93ab9e98eb088a9927a7a8b4e7ca4e24f Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 3 Jan 2024 22:07:53 +1100 Subject: [PATCH 19/26] Update dependencies --- brassica.cabal | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/brassica.cabal b/brassica.cabal index 5461f41..6887933 100644 --- a/brassica.cabal +++ b/brassica.cabal @@ -43,8 +43,8 @@ library build-depends: base >=4.7 && <5 , containers >=0.6 && <0.7 - , deepseq >=1.4 && <1.5 - , megaparsec >=8.0 && <9.3 + , deepseq >=1.4 && <1.6 + , megaparsec >=8.0 && <9.7 , mtl >=2.2 && <2.4 , parser-combinators >=1.2 && <1.3 , split >=0.2 && <0.3 @@ -63,12 +63,12 @@ executable brassica , brassica , aeson ^>=2.2 , attoparsec-aeson ^>=2.2 - , bytestring >=0.10 && <0.12 + , bytestring >=0.10 && <0.13 , conduit ^>=1.3 , conduit-extra ^>=1.3 - , deepseq >=1.4 && <1.5 + , deepseq >=1.4 && <1.6 , optparse-applicative ^>=0.17 || ^>=0.18 - , text >=1.2 && <2.1 + , text >=1.2 && <2.2 default-language: Haskell2010 From bd4698ac9f535a2d2ed76835d7bae5104005ba59 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Wed, 3 Jan 2024 22:07:58 +1100 Subject: [PATCH 20/26] Fix WASM building instructions --- BUILDING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BUILDING.md b/BUILDING.md index 20d4a89..7c4953c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -69,7 +69,7 @@ Then: 1. In `./gui/brassica-interop-wasm`, run the following commands: ``` cabal build --project-file=cabal-wasm.project brassica-interop-wasm - wizer --allow-wasi --wasm-bulk-memory true "$(wasm32-wasi-cabal list-bin -v0 brassica-interop-wasm)" -o "./dist/brassica-interop-wasm.wasm" + wizer --allow-wasi --wasm-bulk-memory true "$(cabal --project-file=cabal-wasm.project list-bin -v0 brassica-interop-wasm)" -o "./dist/brassica-interop-wasm.wasm" ``` This will create a file `./gui/brassica-interop-wasm/dist/brassica-interop-wasm.wasm` containing the WASM binary. From 9454b93ba3b7e519c3646d11e0ce9f3fab468a5a Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Thu, 4 Jan 2024 00:00:08 +1100 Subject: [PATCH 21/26] Rationalise website bundling process --- .gitignore | 4 +- BUILDING.md | 15 +- gui/brassica-web/cpfiles | 5 + gui/brassica-web/package-lock.json | 2535 +++++++++++++++++++++++++-- gui/brassica-web/package.json | 3 +- gui/brassica-web/{ => src}/index.js | 38 +- gui/brassica-web/static/index.html | 154 +- gui/brassica-web/static/style.css | 151 ++ gui/brassica-web/webpack.config.js | 24 +- 9 files changed, 2558 insertions(+), 371 deletions(-) create mode 100755 gui/brassica-web/cpfiles rename gui/brassica-web/{ => src}/index.js (88%) create mode 100644 gui/brassica-web/static/style.css diff --git a/.gitignore b/.gitignore index bbd597d..88dadff 100644 --- a/.gitignore +++ b/.gitignore @@ -12,5 +12,5 @@ gui/build* stub/ CMakeLists.txt.user stack.yaml.lock -gui/brassica-web/static/*.js -gui/brassica-web/static/examples/ +gui/brassica-web/dist/*.js +gui/brassica-web/dist/examples/ diff --git a/BUILDING.md b/BUILDING.md index 7c4953c..b9b7c5c 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -75,12 +75,9 @@ Then: (Note: it can also be convenient to set `CABAL_DIR` so that WASM packages are installed to a different location.) -2. In `./gui/brassica-web`, run the following commands to copy the required files into `./gui/brassica-web/static`: - ``` - npm install - npx webpack - cp ../brassica-interop-wasm/dist/brassica-interop-wasm.wasm static/ - cp -r ../../examples/ static/ - ``` -3. To test Brassica, you can now run a webserver in `./gui/brassica-web/static`, - for instance using `python -m http.server`. +2. In `./gui/brassica-web`, run `mkdir dist; ./cpfiles` to copy the asset files into `dist`. +3. Install JavaScript dependencies using `npm install`. + + Now you can use [webpack](https://webpack.js.org/) to bundle the JavaScript files. + For instance, use `npx webpack serve` to run a development server, + or `npx webpack --mode=production` to prepare a release. diff --git a/gui/brassica-web/cpfiles b/gui/brassica-web/cpfiles new file mode 100755 index 0000000..7a93896 --- /dev/null +++ b/gui/brassica-web/cpfiles @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +cp static/* dist/ +cp ../brassica-interop-wasm/dist/* dist/ +cp -r ../../examples dist/ diff --git a/gui/brassica-web/package-lock.json b/gui/brassica-web/package-lock.json index f00aee5..36bf6ec 100644 --- a/gui/brassica-web/package-lock.json +++ b/gui/brassica-web/package-lock.json @@ -15,7 +15,8 @@ }, "devDependencies": { "webpack": "^5.88.1", - "webpack-cli": "^5.1.4" + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" } }, "node_modules/@bjorn3/browser_wasi_shim": { @@ -90,6 +91,50 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, "node_modules/@types/eslint": { "version": "8.40.2", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", @@ -116,18 +161,138 @@ "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.12", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", "dev": true }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true + }, "node_modules/@types/node": { "version": "20.3.3", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.3.tgz", "integrity": "sha512-wheIYdr4NYML61AjC8MKj/2jrR/kDQri/CIpVoZwldwhnIrD/j9jIU5bJ8yBKuB2VhpFV7Ab6G2XkBjv9r9Zzw==", "dev": true }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", @@ -330,6 +495,19 @@ "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ace-builds": { "version": "1.23.2", "resolved": "https://registry.npmjs.org/ace-builds/-/ace-builds-1.23.2.tgz", @@ -372,6 +550,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -381,6 +598,123 @@ "ajv": "^6.9.1" } }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bonjour-service": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.0.tgz", + "integrity": "sha512-xdzMA6JGckxyJzZByjEWRcfKmDxXaGXZWVftah3FkCqdlePNS9DjHSUN5zkP4oEfz/t0EXXlro88EIhzwMB4zA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.21.9", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", @@ -419,6 +753,29 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001512", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001512.tgz", @@ -439,6 +796,33 @@ } ] }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", @@ -474,118 +858,393 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "mime-db": ">= 1.43.0 < 2" }, "engines": { - "node": ">= 8" + "node": ">= 0.6" } }, - "node_modules/electron-to-chromium": { - "version": "1.4.450", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", - "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", - "dev": true - }, - "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.8.0" } }, - "node_modules/envinfo": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", - "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true }, - "node_modules/es-module-lexer": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", - "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, "engines": { - "node": ">=6" + "node": ">=0.8" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" + "safe-buffer": "5.2.1" }, "engines": { - "node": ">=8.0.0" + "node": ">= 0.6" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", "dev": true, "engines": { - "node": ">=4.0" + "node": ">= 0.6" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.450", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.450.tgz", + "integrity": "sha512-BLG5HxSELlrMx7dJ2s+8SFlsCtJp37Zpk2VAxyC6CZtbc+9AJeZHfYHbrlSgdXp6saQ8StMqOTEDaBKgA7u1sw==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.10.0.tgz", + "integrity": "sha512-ZtUjZO6l5mwTHvc1L9+1q5p/R3wTopcfqMW8r5t8SJSKqeVI/LtajORwRFEKpEFuekjD0VBjwu1HMxL4UalIRw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { "node": ">=0.8.x" } }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -607,6 +1266,48 @@ "node": ">= 4.9.1" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -620,24 +1321,168 @@ "node": ">=8" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "node_modules/follow-redirects": { + "version": "1.15.4", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", + "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", "dev": true }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", "dev": true }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, "node_modules/has": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", @@ -656,7 +1501,200 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, "node_modules/import-local": { @@ -678,6 +1716,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -687,6 +1741,27 @@ "node": ">=10.13.0" } }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-core-module": { "version": "2.12.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", @@ -699,6 +1774,63 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -711,116 +1843,379 @@ "node": ">=0.10.0" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "engines": { "node": ">=0.10.0" } }, - "node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "path-key": "^3.0.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">=8" } }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", "dev": true }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, "engines": { - "node": ">=0.10.0" + "node": ">= 0.8" } }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", "dev": true, "engines": { - "node": ">=6.11.5" + "node": ">= 0.8" } }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" + "wrappy": "1" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, "engines": { - "node": ">= 0.6" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", "dev": true, "dependencies": { - "mime-db": "1.52.0" + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" }, "engines": { - "node": ">= 0.6" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", - "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", - "dev": true - }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -848,6 +2243,19 @@ "node": ">=8" } }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -857,6 +2265,15 @@ "node": ">=6" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -866,6 +2283,15 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -881,12 +2307,30 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "dev": true }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -899,6 +2343,34 @@ "node": ">=8" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", @@ -908,6 +2380,21 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -917,6 +2404,65 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -929,6 +2475,21 @@ "node": ">= 10.13.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, "node_modules/resolve": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", @@ -967,6 +2528,30 @@ "node": ">=8" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -987,33 +2572,187 @@ } ] }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, "node_modules/schema-utils": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" }, "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">= 0.8.0" } }, - "node_modules/serialize-javascript": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", - "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", "dev": true, "dependencies": { - "randombytes": "^2.1.0" + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, "node_modules/shallow-clone": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", @@ -1047,6 +2786,46 @@ "node": ">=8" } }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1066,11 +2845,114 @@ "source-map": "^0.6.0" } }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/spdy-transport/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy-transport/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/spdy/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/spdy/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "node_modules/split.js": { "version": "1.6.5", "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -1159,6 +3041,55 @@ } } }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", @@ -1198,6 +3129,39 @@ "punycode": "^2.1.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/watchpack": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", @@ -1211,6 +3175,15 @@ "node": ">=10.13.0" } }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/webpack": { "version": "5.88.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.1.tgz", @@ -1312,6 +3285,194 @@ "node": ">=14" } }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.1.tgz", + "integrity": "sha512-5hbAst3h3C3L8w6W4P96L5vaV0PxSmJhxZvWKYIdgxOQm8pNZ5dEOmmSLBVpP85ReeyRt6AS1QJNyo/oFFPeVA==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/webpack-merge": { "version": "5.9.0", "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", @@ -1334,6 +3495,29 @@ "node": ">=10.13.0" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1354,6 +3538,33 @@ "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/gui/brassica-web/package.json b/gui/brassica-web/package.json index 85bef6e..c1f0793 100644 --- a/gui/brassica-web/package.json +++ b/gui/brassica-web/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "webpack": "^5.88.1", - "webpack-cli": "^5.1.4" + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" } } diff --git a/gui/brassica-web/index.js b/gui/brassica-web/src/index.js similarity index 88% rename from gui/brassica-web/index.js rename to gui/brassica-web/src/index.js index 73d4cb0..24c811e 100644 --- a/gui/brassica-web/index.js +++ b/gui/brassica-web/src/index.js @@ -1,47 +1,13 @@ import ace from "ace-builds"; -import { WASI } from "@bjorn3/browser_wasi_shim"; import Split from "split.js"; +import {hs, withBytesPtr, decodeStableCStringLen, encoder, decoder} from "./interop.js"; + /*********************** * Haskell interop * ***********************/ -const wasi = new WASI([], [], []); -const wasm = await WebAssembly.instantiateStreaming( - fetch("brassica-interop-wasm.wasm"), - {"wasi_snapshot_preview1": wasi.wasiImport} -); -wasi.inst = wasm.instance; -const hs = wasm.instance.exports; - -// adapted from https://github.com/fourmolu/fourmolu/blob/main/web/worker/index.js -function withBytesPtr(bytes, callback) { - const len = bytes.byteLength; - const ptr = hs.malloc(len); - try { - new Uint8Array(hs.memory.buffer, ptr, len).set(bytes); - callback(ptr, len); - } finally { - hs.free(ptr); - } -}; - -function decodeStableCStringLen(stableCStringLen) { - try { - const cstringptr = hs.getString(stableCStringLen); - const cstringlen = hs.getStringLen(stableCStringLen); - const outputBytes = new Uint8Array(hs.memory.buffer, cstringptr, cstringlen); - var output = decoder.decode(outputBytes); - } finally { - hs.freeStableCStringLen(stableCStringLen); - } - return output; -}; - -const encoder = new TextEncoder(); -const decoder = new TextDecoder(); - const results = hs.initResults(); // NB: not const on Haskell side! function applyChanges(changes, words, sep, reportRules, highlightMode, outputMode) { diff --git a/gui/brassica-web/static/index.html b/gui/brassica-web/static/index.html index b5b94cd..ab378e8 100644 --- a/gui/brassica-web/static/index.html +++ b/gui/brassica-web/static/index.html @@ -3,159 +3,7 @@ Brassica - +

diff --git a/gui/brassica-web/static/style.css b/gui/brassica-web/static/style.css new file mode 100644 index 0000000..34b16f2 --- /dev/null +++ b/gui/brassica-web/static/style.css @@ -0,0 +1,151 @@ +@font-face { + font-family: 'Source Sans'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url(/fonts/SourceSans3-Regular.otf.woff2) format('woff2'); +} +@font-face { + font-family: 'Source Sans'; + font-style: italic; + font-weight: 400; + font-display: swap; + src: url(/fonts/SourceSans3-It.otf.woff2) format('woff2'); +} +@font-face { + font-family: 'Source Sans'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url(/fonts/SourceSans3-Bold.otf.woff2) format('woff2'); +} + +html { + font-family: "Source Sans", sans-serif; +} +body { + margin: 0px; + padding: 10px; + display: flex; + flex-flow: column nowrap; + gap: 10px; + height: 100vh; + box-sizing: border-box; +} +h1 { + padding: 0px; + margin: 0px; +} +details { + border: 1px solid #aaa; + border-radius: 4px; + padding: 0.5em 0.5em 0; + max-width: 90vw; +} +summary { + font-weight: bold; + margin: -0.5em -0.5em 0; + padding: 0.5em; +} +details[open] { + padding: 0.5em; +} +details[open] summary { + border-bottom: 1px solid #aaa; + margin-bottom: 0.5em; +} +details p:first-of-type { + margin-top: 0px; +} +details p:last-of-type { + margin-bottom: 0px; +} +#brassica-form { + flex: 1 1 auto; + display: flex; + flex-flow: row nowrap; + align-items: stretch; + background-color: #f3f3f3; + padding: 1rem; + min-height: 10%; +} +.box-container { + flex-flow: column nowrap; + display: flex; + justify-content: start; + align-items: flex-start; + border: #bdcbe1 1px solid; +} +.header { + background-color: #e1e7f1; + border-bottom: #bdcbe1 1px solid; + width: 100%; + box-sizing: border-box; + padding: 0.2rem; +} +.header label { + font-weight: bold; +} +.header .file-buttons { + margin-left: 1rem; +} +.hidden-file-input { + display: none; +} +#rules, #words, #results { + flex: 1 1 auto; + width: 100%; +} +#words { + box-sizing: border-box; + border: none; + resize: none; +} +#controls { + min-width: 13em; + padding: 0 5px; + display: flex; + flex-flow: column nowrap; + gap: 5px; +} +#controls input[type="submit"] { + font: inherit; + font-weight: bold; +} +#examples-div { + padding-top: 2rem; +} +#results { + overflow: scroll; + background-color: #fff; +} +#words, #results { + font: inherit; + font-size: 90%; +} + +.ace-tm .ace_separator { + font-weight: bold; +} +.ace-tm .ace_category, +.regex_category { + background-color: rgb(245,245,220); +} +.regex_category { + position: absolute; + z-index: 20; +} + +.split { + display: flex; + flex-direction: row; +} +.gutter { + background-color: #f3f3f3; + background-repeat: no-repeat; + background-position: 50%; +} +.gutter.gutter-horizontal { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); + cursor: col-resize; +} diff --git a/gui/brassica-web/webpack.config.js b/gui/brassica-web/webpack.config.js index 4b3eefb..2feb725 100644 --- a/gui/brassica-web/webpack.config.js +++ b/gui/brassica-web/webpack.config.js @@ -5,18 +5,26 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const baseConfig = { - entry: './index.js', + entry: { + index: './src/index.js', + }, + devServer: { + static: './dist', + }, output: { - filename: 'index.js', - path: path.resolve(__dirname, 'static'), + filename: '[name].js', + path: path.resolve(__dirname, 'dist'), }, }; export default function (env, argv) { - let config = { - ...baseConfig, - mode: argv.mode || 'development', - devtool: false, - }; + let config = baseConfig; + if (argv.mode === 'development') { + config.mode = 'development'; + config.devtool = 'inline-source-map'; + } else { + config.mode = 'production'; + config.devtool = 'source-map'; + } return config; } From 0fc412417933c5f0411ee6ded6264d88eacce6d3 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Thu, 4 Jan 2024 01:10:28 +1100 Subject: [PATCH 22/26] Add extra detail to BUILDING --- BUILDING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/BUILDING.md b/BUILDING.md index b9b7c5c..cdf2322 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -76,6 +76,7 @@ Then: (Note: it can also be convenient to set `CABAL_DIR` so that WASM packages are installed to a different location.) 2. In `./gui/brassica-web`, run `mkdir dist; ./cpfiles` to copy the asset files into `dist`. + Note that you’ll need to redo this every time you update a static file (HTML or CSS or WASM)! 3. Install JavaScript dependencies using `npm install`. Now you can use [webpack](https://webpack.js.org/) to bundle the JavaScript files. From a3ea61860eec0fc745c1e08f702077da7d5cc46e Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Thu, 4 Jan 2024 01:12:43 +1100 Subject: [PATCH 23/26] Add refreshed web interface for paradigm builder --- Documentation.md | 2 +- .../brassica-interop-wasm.cabal | 1 + .../src/BrassicaInterop.hs | 35 +++++++++++ gui/brassica-web/src/builder.js | 62 +++++++++++++++++++ gui/brassica-web/src/interop.js | 36 +++++++++++ gui/brassica-web/static/builder.html | 55 ++++++++++++++++ gui/brassica-web/static/index.html | 9 ++- gui/brassica-web/static/style.css | 24 +++---- gui/brassica-web/webpack.config.js | 3 +- 9 files changed, 210 insertions(+), 17 deletions(-) create mode 100644 gui/brassica-web/src/builder.js create mode 100644 gui/brassica-web/src/interop.js create mode 100644 gui/brassica-web/static/builder.html diff --git a/Documentation.md b/Documentation.md index 1cc7f02..70fb1b9 100644 --- a/Documentation.md +++ b/Documentation.md @@ -822,7 +822,7 @@ Further modifications can then be made to the output while keeping the etymologi Brassica includes an inbuilt paradigm builder. It may be accessed using the ‘Tools⇒Paradigm Builder’ menu item in the graphical interface, - or by visiting . + or by visiting . The paradigm builder consists of three textboxes and a button. The leftmost textbox contains a description of the paradigm using the syntax described below. diff --git a/gui/brassica-interop-wasm/brassica-interop-wasm.cabal b/gui/brassica-interop-wasm/brassica-interop-wasm.cabal index dcb8d85..a0522e3 100644 --- a/gui/brassica-interop-wasm/brassica-interop-wasm.cabal +++ b/gui/brassica-interop-wasm/brassica-interop-wasm.cabal @@ -16,6 +16,7 @@ executable brassica-interop-wasm ghc-options: -Wall -no-hs-main -optl-mexec-model=reactor -stubdir stub "-optl-Wl\ \,--export=parseTokeniseAndApplyRules_hs\ + \,--export=parseAndBuildParadigm_hs\ \,--export=initResults\ \,--export=getString\ \,--export=getStringLen\ diff --git a/gui/brassica-interop-wasm/src/BrassicaInterop.hs b/gui/brassica-interop-wasm/src/BrassicaInterop.hs index 553bc47..83028ad 100644 --- a/gui/brassica-interop-wasm/src/BrassicaInterop.hs +++ b/gui/brassica-interop-wasm/src/BrassicaInterop.hs @@ -6,12 +6,14 @@ module BrassicaInterop where import Control.Monad ((<=<)) import Data.IORef +import Data.Foldable (toList) import qualified Foreign import Foreign.C hiding (newCString, peekCString) -- hide these so we don't accidentally use them import Foreign.StablePtr import qualified GHC.Foreign as GHC import GHC.IO.Encoding (utf8) +import Brassica.Paradigm import Brassica.SoundChange import Brassica.SoundChange.Frontend.Internal @@ -104,6 +106,31 @@ escape = concatMap $ \case -- '\t' -> " " -- this doesn't seem to do anything - keeping it here in case I eventually figure out how to do tabs in Qt c -> pure c +parseAndBuildParadigm_hs + :: CString -- ^ paradigm text + -> Int -- ^ length of paradigm text + -> CString -- ^ input words + -> Int -- ^ length of input words + -> CBool -- ^ separate lines? / non-compact? + -> IO (StablePtr CStringLen) +parseAndBuildParadigm_hs + paradigmRaw + paradigmRawLen + inputRaw + inputRawLen + (CBool separateLines) + = do + paradigmText <- GHC.peekCStringLen utf8 (paradigmRaw, paradigmRawLen) + inputText <- GHC.peekCStringLen utf8 (inputRaw, inputRawLen) + + newStableCStringLen $ case parseParadigm paradigmText of + Left e -> "
" ++ errorBundlePretty e ++ "
" + Right p -> escape $ + (if separateLines == 1 + then unlines . toList + else formatNested id) + $ Node $ applyParadigm p <$> lines inputText + foreign export ccall parseTokeniseAndApplyRules_hs :: CString -> Int @@ -118,6 +145,14 @@ foreign export ccall parseTokeniseAndApplyRules_hs -> StablePtr (IORef (Maybe [Component PWord])) -> IO (StablePtr CStringLen) +foreign export ccall parseAndBuildParadigm_hs + :: CString + -> Int + -> CString + -> Int + -> CBool + -> IO (StablePtr CStringLen) + foreign export ccall initResults :: IO (StablePtr (IORef (Maybe [Component PWord]))) foreign export ccall getString :: StablePtr CStringLen -> IO CString foreign export ccall getStringLen :: StablePtr CStringLen -> IO Int diff --git a/gui/brassica-web/src/builder.js b/gui/brassica-web/src/builder.js new file mode 100644 index 0000000..c9fe420 --- /dev/null +++ b/gui/brassica-web/src/builder.js @@ -0,0 +1,62 @@ +import Split from "split.js"; + +import {hs, withBytesPtr, decodeStableCStringLen, encoder, decoder} from "./interop.js"; + +/*********************** + * Haskell interop * + ***********************/ + +function buildParadigm(paradigm, words, separateLines) { + const inputParadigm = encoder.encode(paradigm); + const inputWords = encoder.encode(words); + + const separateLinesC = separateLines ? 1 : 0; + + var output = ""; + withBytesPtr(inputParadigm, (inputParadigmPtr, inputParadigmLen) => { + withBytesPtr(inputWords, (inputWordsPtr, inputWordsLen) => { + try { + const outputStableCStringLen = hs.parseAndBuildParadigm_hs( + inputParadigmPtr, inputParadigmLen, + inputWordsPtr, inputWordsLen, + separateLinesC); + output = decodeStableCStringLen(outputStableCStringLen); + } catch (err) { + console.error(err); + output = err; + } + }); + }); + return output; +} + +/*********************** + * Set up content * + ***********************/ + +Split(["#paradigm-div", "#roots-div", "#output-div"]); + +const form = document.getElementById("brassica-form"); + +form.addEventListener("submit", (event) => { + event.preventDefault(); + + const data = new FormData(form); + const paradigm = data.get("paradigm"); + const roots = data.get("roots"); + const separateLines = data.get("separateLines"); + + const output = buildParadigm(paradigm, roots, separateLines); + console.log(output); + document.getElementById("output").innerHTML = output; +}); + +const blurb = document.getElementById("blurb"); +const blurbHeader = document.getElementById("blurb-header"); +blurb.addEventListener("toggle", (event) => { + if (blurb.open) { + blurbHeader.innerHTML = "Click to close"; + } else { + blurbHeader.innerHTML = "Click to open"; + } +}); diff --git a/gui/brassica-web/src/interop.js b/gui/brassica-web/src/interop.js new file mode 100644 index 0000000..397b668 --- /dev/null +++ b/gui/brassica-web/src/interop.js @@ -0,0 +1,36 @@ +import { WASI } from "@bjorn3/browser_wasi_shim"; + +const wasi = new WASI([], [], []); +const wasm = await WebAssembly.instantiateStreaming( + fetch("brassica-interop-wasm.wasm"), + {"wasi_snapshot_preview1": wasi.wasiImport} +); +wasi.inst = wasm.instance; +export const hs = wasm.instance.exports; + +// adapted from https://github.com/fourmolu/fourmolu/blob/main/web/worker/index.js +export function withBytesPtr(bytes, callback) { + const len = bytes.byteLength; + const ptr = hs.malloc(len); + try { + new Uint8Array(hs.memory.buffer, ptr, len).set(bytes); + callback(ptr, len); + } finally { + hs.free(ptr); + } +}; + +export function decodeStableCStringLen(stableCStringLen) { + try { + const cstringptr = hs.getString(stableCStringLen); + const cstringlen = hs.getStringLen(stableCStringLen); + const outputBytes = new Uint8Array(hs.memory.buffer, cstringptr, cstringlen); + var output = decoder.decode(outputBytes); + } finally { + hs.freeStableCStringLen(stableCStringLen); + } + return output; +}; + +export const encoder = new TextEncoder(); +export const decoder = new TextDecoder(); diff --git a/gui/brassica-web/static/builder.html b/gui/brassica-web/static/builder.html new file mode 100644 index 0000000..b90f8ed --- /dev/null +++ b/gui/brassica-web/static/builder.html @@ -0,0 +1,55 @@ + + + + + Brassica: Paradigm Builder + + + +

+ Brassica paradigm builder: online version +

+
+ Click to close +

+ Brassica is an advanced sound change applier. + This page runs the paradigm builder accompanying Brassica 0.1.0. +

+

+ You are currently on the online version of Brassica. + For supported platforms a standalone desktop version is available from GitHub repository. +

+

+ For help, see the official documentation. +

+
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+ +
+ + +
+
+
+
+ +
+
+
+ + + + diff --git a/gui/brassica-web/static/index.html b/gui/brassica-web/static/index.html index ab378e8..6cd4115 100644 --- a/gui/brassica-web/static/index.html +++ b/gui/brassica-web/static/index.html @@ -30,6 +30,9 @@

+

+ Also: try the accompanying paradigm builder! +

@@ -41,7 +44,7 @@

-
+

@@ -52,7 +55,7 @@

- +
@@ -96,7 +99,7 @@

-
+

diff --git a/gui/brassica-web/static/style.css b/gui/brassica-web/static/style.css index 34b16f2..346542a 100644 --- a/gui/brassica-web/static/style.css +++ b/gui/brassica-web/static/style.css @@ -60,7 +60,7 @@ details p:first-of-type { details p:last-of-type { margin-bottom: 0px; } -#brassica-form { +form { flex: 1 1 auto; display: flex; flex-flow: row nowrap; @@ -92,21 +92,29 @@ details p:last-of-type { .hidden-file-input { display: none; } -#rules, #words, #results { +.input-box { flex: 1 1 auto; width: 100%; } -#words { +.input-box:not(.lib-config) { + font: inherit; + font-size: 90%; +} +div.input-box:not(.lib-config) { + overflow: scroll; + background-color: #fff; +} +textarea.input-box:not(.lib-config) { box-sizing: border-box; border: none; resize: none; } #controls { - min-width: 13em; padding: 0 5px; display: flex; flex-flow: column nowrap; gap: 5px; + min-width: 13em; } #controls input[type="submit"] { font: inherit; @@ -115,14 +123,6 @@ details p:last-of-type { #examples-div { padding-top: 2rem; } -#results { - overflow: scroll; - background-color: #fff; -} -#words, #results { - font: inherit; - font-size: 90%; -} .ace-tm .ace_separator { font-weight: bold; diff --git a/gui/brassica-web/webpack.config.js b/gui/brassica-web/webpack.config.js index 2feb725..76b639d 100644 --- a/gui/brassica-web/webpack.config.js +++ b/gui/brassica-web/webpack.config.js @@ -6,7 +6,8 @@ const __dirname = path.dirname(__filename); const baseConfig = { entry: { - index: './src/index.js', + index: './src/index.js', + builder: './src/builder.js', }, devServer: { static: './dist', From a23c7fdd3bebaf06deecee5e961abf166202ab44 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Thu, 4 Jan 2024 13:27:05 +1100 Subject: [PATCH 24/26] Update copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index dc8da8f..48bdecb 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright Brad Neimann (c) 2020-2023 +Copyright Brad Neimann (c) 2020-2024 All rights reserved. From ef939b9d8192e29cece9d8375c190cc3e406ff86 Mon Sep 17 00:00:00 2001 From: Brad Neimann Date: Thu, 4 Jan 2024 13:33:07 +1100 Subject: [PATCH 25/26] Bump version everywhere --- ChangeLog.md | 2 +- brassica.cabal | 2 +- cli/Main.hs | 2 +- gui/brassica-gui/main.cpp | 2 +- gui/brassica-interop-wasm/brassica-interop-wasm.cabal | 2 +- gui/brassica-web/package-lock.json | 4 ++-- gui/brassica-web/package.json | 2 +- gui/brassica-web/static/builder.html | 2 +- gui/brassica-web/static/index.html | 4 ++-- installer.nsi | 2 +- src/Brassica/Paradigm/Parse.hs | 2 +- src/Brassica/SoundChange/Parse.hs | 2 +- 12 files changed, 14 insertions(+), 14 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index b5c8644..d1508a5 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,6 +1,6 @@ # Brassica changelog -## Unreleased changes +## v0.2.0 - Allow lexeme sequences in categories using `{…}` syntax - Allow backreferences to occur in the environment diff --git a/brassica.cabal b/brassica.cabal index 6887933..6dc4c34 100644 --- a/brassica.cabal +++ b/brassica.cabal @@ -1,6 +1,6 @@ cabal-version: 2.0 name: brassica -version: 0.1.1 +version: 0.2.0 synopsis: Featureful sound change applier description: The Brassica library for the simulation of sound changes in historical linguistics and language construction. diff --git a/cli/Main.hs b/cli/Main.hs index a2142b1..1ff350b 100644 --- a/cli/Main.hs +++ b/cli/Main.hs @@ -33,7 +33,7 @@ main = execParser opts >>= \case .| outC where - opts = info (args <**> helper <**> simpleVersioner "v0.1.1") fullDesc + opts = info (args <**> helper <**> simpleVersioner "v0.2.0") fullDesc args = batchArgs <|> serverArgs serverArgs = flag' Server (long "server" <> help "Run server (for internal use only)") diff --git a/gui/brassica-gui/main.cpp b/gui/brassica-gui/main.cpp index 6cca6c1..064a34d 100644 --- a/gui/brassica-gui/main.cpp +++ b/gui/brassica-gui/main.cpp @@ -10,7 +10,7 @@ int main(int argc, char *argv[]) QCoreApplication::setOrganizationName("bradrn"); QCoreApplication::setOrganizationDomain("bradrn.com"); QCoreApplication::setApplicationName("Brassica"); - QCoreApplication::setApplicationVersion("0.1.1"); + QCoreApplication::setApplicationVersion("0.2.0"); BrassicaProcess proc = BrassicaProcess(); if (!proc.startupCorrect()) { diff --git a/gui/brassica-interop-wasm/brassica-interop-wasm.cabal b/gui/brassica-interop-wasm/brassica-interop-wasm.cabal index a0522e3..db3ad41 100644 --- a/gui/brassica-interop-wasm/brassica-interop-wasm.cabal +++ b/gui/brassica-interop-wasm/brassica-interop-wasm.cabal @@ -1,5 +1,5 @@ name: brassica-interop-wasm -version: 0.1.0 +version: 0.2.0 -- synopsis: -- description: license: BSD3 diff --git a/gui/brassica-web/package-lock.json b/gui/brassica-web/package-lock.json index 36bf6ec..dabfa7c 100644 --- a/gui/brassica-web/package-lock.json +++ b/gui/brassica-web/package-lock.json @@ -1,12 +1,12 @@ { "name": "brassica-web", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "brassica-web", - "version": "0.1.0", + "version": "0.2.0", "license": "BSD-3-Clause", "dependencies": { "@bjorn3/browser_wasi_shim": "^0.2.14", diff --git a/gui/brassica-web/package.json b/gui/brassica-web/package.json index c1f0793..15593e4 100644 --- a/gui/brassica-web/package.json +++ b/gui/brassica-web/package.json @@ -1,6 +1,6 @@ { "name": "brassica-web", - "version": "0.1.0", + "version": "0.2.0", "description": "", "private": true, "type": "module", diff --git a/gui/brassica-web/static/builder.html b/gui/brassica-web/static/builder.html index b90f8ed..4ab121d 100644 --- a/gui/brassica-web/static/builder.html +++ b/gui/brassica-web/static/builder.html @@ -13,7 +13,7 @@

Click to close

Brassica is an advanced sound change applier. - This page runs the paradigm builder accompanying Brassica 0.1.0. + This page runs the paradigm builder accompanying Brassica 0.2.0.

You are currently on the online version of Brassica. diff --git a/gui/brassica-web/static/index.html b/gui/brassica-web/static/index.html index 6cd4115..2de3d00 100644 --- a/gui/brassica-web/static/index.html +++ b/gui/brassica-web/static/index.html @@ -13,14 +13,14 @@

Click to close

Brassica is an advanced sound change applier with features including multigraphs, syntax highlighting, flexible category operations and feature support. - This page runs Brassica 0.1.0. + This page runs Brassica 0.2.0.

You are currently on the online version of Brassica. For supported platforms a standalone desktop version is available from GitHub repository.

- For help, see the official documentation. + For help, see the official documentation.
Or select an example here: