about summary refs log tree commit diff
path: root/corp/russian
diff options
context:
space:
mode:
Diffstat (limited to 'corp/russian')
-rw-r--r--corp/russian/README.md9
-rw-r--r--corp/russian/predlozhnik/.gitignore3
-rw-r--r--corp/russian/predlozhnik/Cargo.lock460
-rw-r--r--corp/russian/predlozhnik/Cargo.toml12
-rw-r--r--corp/russian/predlozhnik/default.nix52
-rw-r--r--corp/russian/predlozhnik/index.css29
-rw-r--r--corp/russian/predlozhnik/index.html24
-rw-r--r--corp/russian/predlozhnik/src/main.rs345
8 files changed, 934 insertions, 0 deletions
diff --git a/corp/russian/README.md b/corp/russian/README.md
new file mode 100644
index 000000000000..23c3d594c8de
--- /dev/null
+++ b/corp/russian/README.md
@@ -0,0 +1,9 @@
+//corp/russian
+==============
+
+This folder contains TVL corp projects related to the Russian
+language, such as the code powering
+[Предложник](https://predlozhnik.ru).
+
+Unless otherwise specified, all rights to these projects are reserved
+by ООО "ТВЛ".
diff --git a/corp/russian/predlozhnik/.gitignore b/corp/russian/predlozhnik/.gitignore
new file mode 100644
index 000000000000..58eaf3e32687
--- /dev/null
+++ b/corp/russian/predlozhnik/.gitignore
@@ -0,0 +1,3 @@
+/target/
+**/*.rs.bk
+dist/
diff --git a/corp/russian/predlozhnik/Cargo.lock b/corp/russian/predlozhnik/Cargo.lock
new file mode 100644
index 000000000000..131aa134fec6
--- /dev/null
+++ b/corp/russian/predlozhnik/Cargo.lock
@@ -0,0 +1,460 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+
+[[package]]
+name = "boolinator"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9"
+
+[[package]]
+name = "bumpalo"
+version = "3.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "console_error_panic_hook"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23947965eee55e3e97a5cd142dd4c10631cc349b48cecca0ed230fd296f568cd"
+dependencies = [
+ "gloo-console",
+ "gloo-dialogs",
+ "gloo-events",
+ "gloo-file",
+ "gloo-render",
+ "gloo-storage",
+ "gloo-timers",
+ "gloo-utils",
+]
+
+[[package]]
+name = "gloo-console"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b7ce3c05debe147233596904981848862b068862e9ec3e34be446077190d3f"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-dialogs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67062364ac72d27f08445a46cab428188e2e224ec9e37efdba48ae8c289002e6"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-events"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68b107f8abed8105e4182de63845afcc7b69c098b7852a813ea7462a320992fc"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-file"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d5564e570a38b43d78bdc063374a0c3098c4f0d64005b12f9bbe87e869b6d7"
+dependencies = [
+ "gloo-events",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-render"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fd9306aef67cfd4449823aadcd14e3958e0800aa2183955a309112a84ec7764"
+dependencies = [
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-storage"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d6ab60bf5dbfd6f0ed1f7843da31b41010515c745735c970e821945ca91e480"
+dependencies = [
+ "gloo-utils",
+ "js-sys",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "gloo-timers"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "gloo-utils"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40913a05c8297adca04392f707b1e73b12ba7b8eab7244a4961580b1fd34063c"
+dependencies = [
+ "js-sys",
+ "serde",
+ "serde_json",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
+
+[[package]]
+name = "indexmap"
+version = "1.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
+dependencies = [
+ "autocfg",
+ "hashbrown",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754"
+
+[[package]]
+name = "js-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47"
+dependencies = [
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "log"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
+[[package]]
+name = "once_cell"
+version = "1.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+
+[[package]]
+name = "predlozhnik"
+version = "0.1.0"
+dependencies = [
+ "lazy_static",
+ "maplit",
+ "wasm-bindgen",
+ "yew",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
+
+[[package]]
+name = "scoped-tls-hkt"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2e9d7eaddb227e8fbaaa71136ae0e1e913ca159b86c7da82f3e8f0044ad3a63"
+
+[[package]]
+name = "serde"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.145"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.101"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e90cde112c4b9690b8cbe810cba9ddd8bc1d7472e2cae317b69e9438c1cba7d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[package]]
+name = "version_check"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268"
+dependencies = [
+ "cfg-if",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142"
+dependencies = [
+ "bumpalo",
+ "log",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f"
+
+[[package]]
+name = "web-sys"
+version = "0.3.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "yew"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a1ccb53e57d3f7d847338cf5758befa811cabe207df07f543c06f502f9998cd"
+dependencies = [
+ "console_error_panic_hook",
+ "gloo",
+ "gloo-utils",
+ "indexmap",
+ "js-sys",
+ "scoped-tls-hkt",
+ "slab",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "yew-macro",
+]
+
+[[package]]
+name = "yew-macro"
+version = "0.19.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fab79082b556d768d6e21811869c761893f0450e1d550a67892b9bce303b7bb"
+dependencies = [
+ "boolinator",
+ "lazy_static",
+ "proc-macro-error",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/corp/russian/predlozhnik/Cargo.toml b/corp/russian/predlozhnik/Cargo.toml
new file mode 100644
index 000000000000..90205bc4fb1b
--- /dev/null
+++ b/corp/russian/predlozhnik/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "predlozhnik"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+maplit = "1.0"
+lazy_static = "1.4"
+yew = "0.19"
+
+# needs to be in sync with nixpkgs
+wasm-bindgen = "= 0.2.83"
diff --git a/corp/russian/predlozhnik/default.nix b/corp/russian/predlozhnik/default.nix
new file mode 100644
index 000000000000..2137be111278
--- /dev/null
+++ b/corp/russian/predlozhnik/default.nix
@@ -0,0 +1,52 @@
+{ lib, pkgs, ... }:
+
+let
+  wasmRust = pkgs.rust-bin.stable.latest.default.override {
+    targets = [ "wasm32-unknown-unknown" ];
+  };
+
+  cargoToml = with builtins; fromTOML (readFile ./Cargo.toml);
+
+  wasmBindgenMatch =
+    cargoToml.dependencies.wasm-bindgen == "= ${pkgs.wasm-bindgen-cli.version}";
+
+  assertWasmBindgen = assert (lib.assertMsg wasmBindgenMatch ''
+    Due to instability in the Rust WASM ecosystem, the trunk build
+    tool enforces that the Cargo-dependency version of `wasm-bindgen`
+    MUST match the version of the CLI supplied in the environment.
+
+    This can get out of sync when nixpkgs is updated. To resolve it,
+    wasm-bindgen must be bumped in the Cargo.toml file and cargo needs
+    to be run to resolve the dependencies.
+
+    Versions of `wasm-bindgen` in Cargo.toml:
+
+      Expected: '= ${pkgs.wasm-bindgen-cli.version}'
+      Actual:   '${cargoToml.dependencies.wasm-bindgen}'
+  ''); pkgs.wasm-bindgen-cli;
+
+  deps = with pkgs; [
+    binaryen
+    sass
+    wasmRust
+    trunk
+    assertWasmBindgen
+  ];
+
+in
+pkgs.rustPlatform.buildRustPackage rec {
+  pname = "predlozhnik";
+  version = "canon";
+  src = lib.cleanSource ./.;
+  cargoLock.lockFile = ./Cargo.lock;
+
+  buildPhase = ''
+    export PATH=${lib.makeBinPath deps}:$PATH
+    mkdir home
+    export HOME=$PWD/.home
+    env
+    trunk build --release -d $out
+  '';
+
+  dontInstall = true;
+}
diff --git a/corp/russian/predlozhnik/index.css b/corp/russian/predlozhnik/index.css
new file mode 100644
index 000000000000..3529574c4f2b
--- /dev/null
+++ b/corp/russian/predlozhnik/index.css
@@ -0,0 +1,29 @@
+body {
+    max-width: 800px;
+    margin: 40px auto;
+}
+
+#header {
+    display: flex;
+    flex-direction: column;
+}
+
+.btn.btn-ghost:disabled {
+    border-color: #9f9f9f;
+    color: #9f9f9f;
+}
+
+#predlogi,#padezhi {
+    display: flex;
+    flex-direction: row;
+    flex-wrap: wrap;
+}
+
+.btn {
+    margin: 3px;
+    flex-grow: 1;
+}
+
+.footer {
+    text-align: right;
+}
diff --git a/corp/russian/predlozhnik/index.html b/corp/russian/predlozhnik/index.html
new file mode 100644
index 000000000000..6af1adc0bfba
--- /dev/null
+++ b/corp/russian/predlozhnik/index.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <link rel="stylesheet"
+          href="https://unpkg.com/terminal.css@0.7.2/dist/terminal.min.css" />
+    <link data-trunk rel="inline" href="index.css">
+    <title>Предложник</title>
+
+    <!-- Yandex.RTB -->
+    <script>window.yaContextCb=window.yaContextCb||[]</script>
+    <script src="https://yandex.ru/ads/system/context.js" async></script>
+  </head>
+  <body>
+    <noscript>
+      <h1>Предложник</h1>
+      <p>
+        ... показывает с какими падежами употребляются предлоги в
+        русском языке. Но, к сожалению, только с помощью Javascript.
+      </p>
+    </noscript>
+  </body>
+</html>
diff --git a/corp/russian/predlozhnik/src/main.rs b/corp/russian/predlozhnik/src/main.rs
new file mode 100644
index 000000000000..e267c849de83
--- /dev/null
+++ b/corp/russian/predlozhnik/src/main.rs
@@ -0,0 +1,345 @@
+use yew::html::Scope;
+use yew::prelude::*;
+
+use lazy_static::lazy_static;
+use maplit::hashmap;
+use std::collections::BTreeSet;
+use std::collections::HashMap;
+
+#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
+enum Падеж {
+    Именительный,
+    Родительный,
+    Дательный,
+    Винительный,
+    Творительный,
+    Предложный,
+}
+
+impl Падеж {
+    const ВСЕ: [Self; 6] = [
+        Self::Именительный,
+        Self::Родительный,
+        Self::Дательный,
+        Self::Винительный,
+        Self::Творительный,
+        Self::Предложный,
+    ];
+
+    fn вопрос(&self) -> &str {
+        use Падеж::*;
+        match self {
+            Именительный => "кто? Что?",
+            Родительный => "кого? Чего?",
+            Дательный => "кому? Чему?",
+            Винительный => "кого? Что?",
+            Творительный => "кем? Чем?",
+            Предложный => "ком? Чём?",
+        }
+    }
+}
+
+lazy_static! {
+    static ref ПО_ПРЕДЛОГУ: HashMap<&'static str, BTreeSet<Падеж>> = {
+        use Падеж::*;
+
+        hashmap! {
+            "без" => BTreeSet::from([Родительный]),
+            "близ" => BTreeSet::from([Родительный]),
+            "в" => BTreeSet::from([Винительный, Предложный]),
+            "вместо" => BTreeSet::from([Родительный]),
+            "вне" => BTreeSet::from([Родительный]),
+            "внутри" => BTreeSet::from([Родительный]),
+            "возле" => BTreeSet::from([Родительный]),
+            "вокруг" => BTreeSet::from([Родительный]),
+            "вроде" => BTreeSet::from([Родительный]),
+            "для" => BTreeSet::from([Родительный]),
+            "до" => BTreeSet::from([Родительный]),
+            "за" => BTreeSet::from([Винительный, Творительный]),
+            "из" => BTreeSet::from([Родительный]),
+            "из-за" => BTreeSet::from([Родительный]),
+            "из-под" => BTreeSet::from([Родительный]),
+            "к" => BTreeSet::from([Дательный]),
+            "кроме" => BTreeSet::from([Родительный]),
+            "между" => BTreeSet::from([Творительный, Родительный]),
+            "на" => BTreeSet::from([Винительный, Предложный]),
+            "над" => BTreeSet::from([Творительный]),
+            "нет" => BTreeSet::from([Родительный]),
+            "о" => BTreeSet::from([Винительный, Предложный]),
+            "около" => BTreeSet::from([Родительный]),
+            "от" => BTreeSet::from([Родительный]),
+            "перед" => BTreeSet::from([Творительный]),
+            "по" => BTreeSet::from([Винительный, Дательный, Предложный]),
+            "под" => BTreeSet::from([Винительный, Творительный]),
+            "после" => BTreeSet::from([Родительный]),
+            "при" => BTreeSet::from([Предложный]),
+            "про" => BTreeSet::from([Винительный]),
+            "ради" => BTreeSet::from([Родительный]),
+            "с" => BTreeSet::from([Родительный, Винительный, Творительный]),
+            "сквозь" => BTreeSet::from([Винительный]),
+            "среди" => BTreeSet::from([Родительный]),
+            "у" => BTreeSet::from([Родительный]),
+            "через" => BTreeSet::from([Винительный]),
+        }
+    };
+    static ref ПО_ПАДЕЖУ: HashMap<Падеж, BTreeSet<&'static str>> = {
+        let mut m = hashmap!();
+
+        for c in Падеж::ВСЕ {
+            let mut предлоги: BTreeSet<&'static str> = BTreeSet::new();
+            for (k, v) in &*ПО_ПРЕДЛОГУ {
+                if v.contains(&c) {
+                    предлоги.insert(k);
+                }
+            }
+
+            m.insert(c, предлоги);
+        }
+
+        m
+    };
+    static ref ПАДЕЖИ: BTreeSet<Падеж> = BTreeSet::from(Падеж::ВСЕ);
+    static ref ПРЕДЛОГИ: BTreeSet<&'static str> = {
+        let mut s: BTreeSet<&'static str> = BTreeSet::new();
+
+        for п in ПО_ПРЕДЛОГУ.keys() {
+            s.insert(п);
+        }
+
+        s
+    };
+}
+
+fn исключение(предлог: &str, падеж: Падеж) -> Option<Html> {
+    use Падеж::*;
+
+    match (предлог, падеж) {
+        ("в", Винительный) => Some(html! {"Во что? В кого?"}),
+
+        ("о", Винительный) => Some(html! {
+            <>
+              <p>{"О кого? Обо что?"}</p>
+              <p>{"Редко используется. Например:"}</p>
+              <ul>
+                <li>{"Удариться о притолоку."}</li>
+                <li>{"точить о камень."}</li>
+              </ul>
+            </>
+        }),
+
+        ("между", Родительный) => Some(html! {
+            <>
+              <p>{"Между чего?"}</p>
+              <p>{"Редко используется. Только в идиомах и старой литературе:"}</p>
+              <ul>
+                <li>{"Читаю между строк."}</li>
+              </ul>
+            </>
+        }),
+
+        _ => None,
+    }
+}
+
+enum Сообщение {
+    ВыбралПадеж(Option<Падеж>),
+    ВыбралПредлог(Option<&'static str>),
+}
+
+#[derive(Default)]
+struct Модель {
+    падеж: Option<Падеж>,
+    предлог: Option<&'static str>,
+}
+
+struct Вывод {
+    доступные_падежи: BTreeSet<Падеж>,
+    доступные_предлоги: BTreeSet<&'static str>,
+    объяснение: Option<Html>,
+}
+
+fn объясни(падеж: Падеж, предлог: &str) -> Html {
+    let иск = match исключение(предлог, падеж) {
+        Some(exp) => html! { exp },
+        None => html! { format!("{} {}", предлог, падеж.вопрос()) },
+    };
+
+    html! {
+        <div id="obyasnenie">
+          <hr/>
+          <h2>{"Пример:"}</h2>
+          {иск}
+        </div>
+    }
+}
+
+fn ограничить(м: &Модель) -> Вывод {
+    match (м.падеж, &м.предлог) {
+        (Some(пж), Some(пл)) => Вывод {
+            доступные_падежи: (*ПО_ПРЕДЛОГУ)[пл].clone(),
+            доступные_предлоги: (*ПО_ПАДЕЖУ)[&пж].clone(),
+            объяснение: Some(объясни(пж, пл)),
+        },
+
+        (Some(пж), None) => Вывод {
+            доступные_падежи: BTreeSet::from([пж]),
+            доступные_предлоги: (*ПО_ПАДЕЖУ)[&пж].clone(),
+            объяснение: None,
+        },
+
+        (None, Some(пл)) => Вывод {
+            доступные_падежи: (*ПО_ПРЕДЛОГУ)[пл].clone(),
+            доступные_предлоги: BTreeSet::from([*пл]),
+            объяснение: None,
+        },
+
+        (None, None) => Вывод {
+            доступные_падежи: ПАДЕЖИ.clone(),
+            доступные_предлоги: ПРЕДЛОГИ.clone(),
+            объяснение: None,
+        },
+    }
+}
+
+fn класс_кнопки(выбран: bool, доступен: bool) -> String {
+    let класс = "btn ".to_string();
+    класс
+        + match (выбран, доступен) {
+            (true, _) => "btn-primary",
+            (false, true) => "btn-ghost btn-primary",
+            (false, false) => "btn-ghost btn-default",
+        }
+}
+
+fn покажи_предлог(
+    link: &Scope<Модель>,
+    м: &Модель,
+    вв: &Вывод,
+    п: &'static str,
+) -> Html {
+    let выбран = м.предлог == Some(п);
+    let доступен = вв.доступные_предлоги.contains(п);
+    let класс = класс_кнопки(выбран, доступен);
+
+    html! {
+        <button class={класс}
+         onclick={link.callback(move |_| if выбран {
+             Сообщение::ВыбралПредлог(None)
+         } else {
+             Сообщение::ВыбралПредлог(Some(п))
+         })}
+         disabled={!доступен}>
+        {п}
+        </button>
+    }
+}
+
+fn покажи_падеж(
+    link: &Scope<Модель>, м: &Модель, вв: &Вывод, п: Падеж
+) -> Html {
+    let выбран = м.падеж == Some(п);
+    let доступен = вв.доступные_падежи.contains(&п);
+    let класс = класс_кнопки(выбран, доступен);
+
+    html! {
+        <button class={класс}
+         onclick={link.callback(move |_| if выбран {
+             Сообщение::ВыбралПадеж(None)
+         } else {
+             Сообщение::ВыбралПадеж(Some(п))
+         })}
+         disabled={!доступен}>
+        {format!("{:?}", п)}
+        </button>
+    }
+}
+
+impl Component for Модель {
+    type Message = Сообщение;
+    type Properties = ();
+
+    fn create(_ctx: &Context<Self>) -> Self {
+        Default::default()
+    }
+
+    fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
+        match msg {
+            Сообщение::ВыбралПадеж(пж) => self.падеж = пж,
+            Сообщение::ВыбралПредлог(пл) => self.предлог = пл,
+        }
+
+        true
+    }
+
+    fn view(&self, ctx: &Context<Self>) -> Html {
+        let вв = ограничить(self);
+        let link = ctx.link();
+
+        let кнопки_предлогов = ПРЕДЛОГИ
+            .iter()
+            .map(|п| покажи_предлог(link, self, &вв, п))
+            .collect::<Html>();
+
+        let кнопки_падежов = ПАДЕЖИ
+            .iter()
+            .map(|п| покажи_падеж(link, self, &вв, *п))
+            .collect::<Html>();
+
+        let объяснение = вв.объяснение.map(|exp| exp).unwrap_or_else(|| html! {});
+
+        let footer = html! {
+            <footer>
+              <hr/>
+              <p class="footer">
+                <a href="https://code.tvl.fyi/tree/users/tazjin/predlozhnik">{"код"}</a>
+                {" | "}
+                {"сделано "}<a href="https://tvl.su">{"ООО \"ТВЛ\""}</a>
+              </p>
+            </footer>
+        };
+
+        let код_рекламы = r#"
+window.yaContextCb.push(()=>{
+  Ya.Context.AdvManager.render({
+    renderTo: 'yandex_rtb_R-A-1773485-1',
+    blockId: 'R-A-1773485-1'
+  })
+})
+"#;
+
+        let реклама = html! {
+            <div id="ad">
+              <div id="yandex_rtb_R-A-1773485-1"></div>
+              <script>{код_рекламы}</script>
+            </div>
+        };
+
+        html! {
+            <>
+                <div id="header">
+                  <h1>{"Предложник"}</h1>
+                  <p>{"... показывает с какими падежами употребляются предлоги в русском языке."}</p>
+                </div>
+
+                <h2>{"Выбирай предлог:"}</h2>
+                <div id="predlogi">
+                  {кнопки_предлогов}
+                </div>
+                <hr/>
+
+                <h2>{"Выбирай падеж:"}</h2>
+                <div id="padezhi">
+                  {кнопки_падежов}
+                </div>
+
+                {объяснение}
+                {footer}
+                {реклама}
+            </>
+        }
+    }
+}
+
+fn main() {
+    yew::start_app::<Модель>();
+}