Compare commits

..
24 Commits
Author SHA1 Message Date
gsb 3869a3abdd Check for node/npm before building ribbit
pre_build hook now warns and skips the ribbit build if node or npm
are not installed, instead of failing the entire slam build.

Also, copy all of dist/ instead of individual files
2026-04-29 00:32:15 +00:00
gsb 256c8d7090 Replace marked/turndown/purify with ribbit editor
This commit moves all html/markdown editor logic to the ribbit library.

Submodule tracks https://git.evilchi.li/evilchili/ribbit.git.
The dist files in static/ are committed copies; the submodule
provides source access for development.

We use pre_format and pre_build slam hooks to ensure the submodules are
initialized and updated correctly, and to perform the dist file copy.
2026-04-29 00:26:00 +00:00
evilchili ce8042759e checkpoint 2026-04-28 16:18:22 -07:00
evilchili d28b31ed14 fix editor layout 2026-01-30 14:59:00 -08:00
evilchili 2d07a5c5f6 fix read-only viewer 2026-01-17 15:53:29 -08:00
evilchili 3d66587678 improve docs on default widget 2026-01-17 15:49:05 -08:00
evilchili 39a1b07c07 fix wysiwyg compatibility in widgets 2026-01-17 15:06:13 -08:00
evilchili 705cf9e8ba bugfix wysiwyg 2026-01-17 11:54:12 -08:00
evilchili 1bcaff892b fix markdown<=>html 2026-01-17 11:19:38 -08:00
evilchili ff78d55b8e add functioning TOC links 2025-12-25 12:09:32 -08:00
evilchili 995e75d3e2 simplifying theme layout 2025-12-25 10:58:59 -08:00
evilchili 27cf36c390 rename js obj to froghat, add widget plugin 2025-12-25 10:42:59 -08:00
evilchili f21bdbdb0c checkpoint: working markdown<=>html converter 2025-12-25 10:29:46 -08:00
evilchili c540073b66 reimplemented editor 2025-11-27 01:57:25 -08:00
evilchili ce2c65cce1 Switch to String fields by default 2025-11-02 00:15:56 -07:00
evilchili fff34948d2 remove forms, add validators 2025-11-01 23:48:56 -07:00
evilchili 68cfd0e7f6 formatting 2025-10-30 20:52:15 -07:00
evilchili f6efbeb54a Add system user, prevent circular refs in author 2025-10-30 20:51:24 -07:00
evilchili 5fffbf59f5 Put root page on uri "", make parent() a method 2025-10-29 19:06:57 -07:00
evilchili 6afab2a15c refactor page loader 2025-10-22 19:20:47 -07:00
evilchili 8fd28cf8b1 body is now a TextFilePointer 2025-10-18 17:26:21 -07:00
evilchili 51795c8f7c ui wip 2025-10-18 11:23:03 -07:00
evilchili fb391f9f4a only load the viewer when the user cannot write the page 2025-10-17 20:14:25 -07:00
evilchili 83527b85f4 working UI 2025-10-17 19:33:29 -07:00
29 changed files with 3952 additions and 618 deletions
+1
View File
@@ -16,6 +16,7 @@ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ lib/
!lib/ribbit
lib64/ lib64/
parts/ parts/
sdist/ sdist/
+3
View File
@@ -0,0 +1,3 @@
[submodule "lib/ribbit"]
path = lib/ribbit
url = https://git.evilchi.li/evilchili/ribbit.git
+28
View File
@@ -0,0 +1,28 @@
#!/bin/sh
set -e
RIBBIT_DIR="lib/ribbit"
STATIC_DIR="src/ttfrog/themes/default/static"
missing=""
command -v node >/dev/null 2>&1 || missing="$missing node"
command -v npm >/dev/null 2>&1 || missing="$missing npm"
if [ -n "$missing" ]; then
echo ""
echo "WARNING: ribbit build skipped — missing dependencies:$missing"
echo "Install Node.js to enable ribbit builds: https://nodejs.org"
echo ""
exit 0
fi
echo "Building ribbit..."
cd "$RIBBIT_DIR"
npm install --silent
npm run build --silent
cd - > /dev/null
echo "Copying ribbit dist files..."
cp -r "$RIBBIT_DIR/dist/." "$STATIC_DIR/"
echo "ribbit build complete."
+5
View File
@@ -0,0 +1,5 @@
#!/bin/sh
# Initialize and update git submodules to their latest remote commits.
if [ -f .gitmodules ]; then
git -c protocol.file.allow=always submodule update --init --force --remote --recursive
fi
Submodule
+1
Submodule lib/ribbit added at 5983ce50fd
Generated
+772
View File
@@ -0,0 +1,772 @@
# This file is automatically @generated by Poetry 2.3.4 and should not be changed by hand.
[[package]]
name = "annotated-doc"
version = "0.0.4"
description = "Document parameters, class attributes, return types, and variables inline, with Annotated."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"},
{file = "annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4"},
]
[[package]]
name = "blinker"
version = "1.9.0"
description = "Fast, simple object-to-object and broadcast signaling"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc"},
{file = "blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf"},
]
[[package]]
name = "cachelib"
version = "0.13.0"
description = "A collection of cache libraries in the same API interface."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "cachelib-0.13.0-py3-none-any.whl", hash = "sha256:8c8019e53b6302967d4e8329a504acf75e7bc46130291d30188a6e4e58162516"},
{file = "cachelib-0.13.0.tar.gz", hash = "sha256:209d8996e3c57595bee274ff97116d1d73c4980b2fd9a34c7846cd07fd2e1a48"},
]
[[package]]
name = "click"
version = "8.3.3"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"},
{file = "click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
groups = ["main", "dev"]
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""}
[[package]]
name = "coverage"
version = "7.13.5"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5"},
{file = "coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf"},
{file = "coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8"},
{file = "coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4"},
{file = "coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d"},
{file = "coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930"},
{file = "coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0"},
{file = "coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0"},
{file = "coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58"},
{file = "coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e"},
{file = "coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d"},
{file = "coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587"},
{file = "coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642"},
{file = "coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b"},
{file = "coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686"},
{file = "coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743"},
{file = "coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd"},
{file = "coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8"},
{file = "coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf"},
{file = "coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9"},
{file = "coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028"},
{file = "coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01"},
{file = "coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f"},
{file = "coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376"},
{file = "coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256"},
{file = "coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf"},
{file = "coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c"},
{file = "coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf"},
{file = "coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810"},
{file = "coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de"},
{file = "coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1"},
{file = "coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26"},
{file = "coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b"},
{file = "coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a"},
{file = "coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6"},
{file = "coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17"},
{file = "coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85"},
{file = "coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b"},
{file = "coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d"},
{file = "coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd"},
{file = "coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479"},
{file = "coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2"},
{file = "coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a"},
{file = "coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819"},
{file = "coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911"},
{file = "coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f"},
{file = "coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a"},
{file = "coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247"},
{file = "coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6"},
{file = "coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0"},
{file = "coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0"},
{file = "coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc"},
{file = "coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633"},
{file = "coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b"},
{file = "coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90"},
{file = "coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea"},
{file = "coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a"},
{file = "coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215"},
{file = "coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43"},
{file = "coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45"},
{file = "coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61"},
{file = "coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179"},
]
[package.extras]
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "flask"
version = "3.1.3"
description = "A simple framework for building complex web applications."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "flask-3.1.3-py3-none-any.whl", hash = "sha256:f4bcbefc124291925f1a26446da31a5178f9483862233b23c0c96a20701f670c"},
{file = "flask-3.1.3.tar.gz", hash = "sha256:0ef0e52b8a9cd932855379197dd8f94047b359ca0a78695144304cb45f87c9eb"},
]
[package.dependencies]
blinker = ">=1.9.0"
click = ">=8.1.3"
itsdangerous = ">=2.2.0"
jinja2 = ">=3.1.2"
markupsafe = ">=2.1.1"
werkzeug = ">=3.1.0"
[package.extras]
async = ["asgiref (>=3.2)"]
dotenv = ["python-dotenv"]
[[package]]
name = "flask-session"
version = "0.8.0"
description = "Server-side session support for Flask"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "flask_session-0.8.0-py3-none-any.whl", hash = "sha256:5dae6e9ddab334f8dc4dea4305af37851f4e7dc0f484caf3351184001195e3b7"},
{file = "flask_session-0.8.0.tar.gz", hash = "sha256:20e045eb01103694e70be4a49f3a80dbb1b57296a22dc6f44bbf3f83ef0742ff"},
]
[package.dependencies]
cachelib = "*"
flask = ">=2.2"
msgspec = ">=0.18.6"
[package.extras]
all = ["Flask-Session[cachelib,memcached,mongodb,redis,sqlalchemy]"]
cachelib = ["cachelib (>=0.10.2)"]
memcached = ["pymemcache"]
mongodb = ["pymongo (>=4.6.2)"]
redis = ["redis (>=5.0.3)"]
sqlalchemy = ["flask-sqlalchemy (>=3.0.5)"]
[[package]]
name = "grung-db"
version = "1.0.0.2"
description = "grung-db: A very small database toolkit."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = []
develop = false
[package.dependencies]
nanoid = "^2.0.0"
tinydb = "^4.8.2"
[package.source]
type = "git"
url = "https://git.evilchi.li/evilchili/grung-db.git"
reference = "HEAD"
resolved_reference = "8355d5a3c28efcdb583623caf54b70e79fab2af5"
[[package]]
name = "iniconfig"
version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
]
[[package]]
name = "jinja2"
version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
]
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "markdown-it-py"
version = "4.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
{file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
]
[package.dependencies]
mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins (>=0.5.0)"]
profiling = ["gprof2dot"]
rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
[[package]]
name = "markupsafe"
version = "3.0.3"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
{file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
{file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
{file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
{file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
{file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
{file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
{file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
{file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
{file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
{file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
{file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
{file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
{file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
{file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
{file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
{file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
{file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
{file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
{file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
{file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
{file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
{file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
{file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
{file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
{file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
{file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
{file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
{file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
{file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
{file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
{file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
{file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
{file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
{file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
{file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
{file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
{file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
{file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
{file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
{file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
]
[[package]]
name = "mdurl"
version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
]
[[package]]
name = "msgspec"
version = "0.21.1"
description = "A fast serialization and validation library, with builtin support for JSON, MessagePack, YAML, and TOML."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "msgspec-0.21.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72d9cd03241b8b2edb2e12dcc66c500fa480d8cbd71a8bac105809d468882064"},
{file = "msgspec-0.21.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ed2ab278200e743a1d2610a4e0c8fc74f6cecb8548544cdec43f927bd9265238"},
{file = "msgspec-0.21.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd677e3001fdfed9186de72eab434da2976303cd5eb9550921d3d0c3e3e168ce"},
{file = "msgspec-0.21.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f667b90b37fad734a91671abd68e0d7f4d066862771b87e91c53996dcb7a9027"},
{file = "msgspec-0.21.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:49880fd20fdbcfe1b793f07dd83f12572bab679c9800352c8b2240289aa46a06"},
{file = "msgspec-0.21.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae0162e22849a5e91eaad907766525107523b0daea3df267a9fcb5ba4e0936ae"},
{file = "msgspec-0.21.1-cp310-cp310-win_amd64.whl", hash = "sha256:f041a2279f31e3a53319005e4d60ba77c085cfcbe394cdc7ce803c2d01fe9449"},
{file = "msgspec-0.21.1-cp310-cp310-win_arm64.whl", hash = "sha256:1bf17cbd7b28a5dffc7e764c654eed8ccde5e0f1de7970628608304640d4ce4e"},
{file = "msgspec-0.21.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b504b6e7f7a22a24b27232b73034421692147865162daaec9f3bf62439007c87"},
{file = "msgspec-0.21.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4692b7c1609155708c4418f88e92f63c13fdf08aa095c84bae82bad75b53389b"},
{file = "msgspec-0.21.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3124010b3815451494c85ff345e693cb9fe5889cfcbbef39ed8622e0e72319c"},
{file = "msgspec-0.21.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6badc03b9725352219cca017bfe71c61f2fbd0fb5982b410ac17c97c213deb30"},
{file = "msgspec-0.21.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5d2d4116ebe3035a78d9ec76e99a9d64e5fa6d44fe61a9c5de7fd1acf54bcc69"},
{file = "msgspec-0.21.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0d1009f6715f5bff3b54d4ff5c7428ad96197e0534e1645b8e9b955890c84664"},
{file = "msgspec-0.21.1-cp311-cp311-win_amd64.whl", hash = "sha256:c6faffe5bb644ec884052679af4dfd776d4b5ca90e4a7ec7e7e319e4e6b93a6e"},
{file = "msgspec-0.21.1-cp311-cp311-win_arm64.whl", hash = "sha256:ee9e3f11fa94603f7d673bf795cfa31b549c4a2c723bc39b45beb1e7f5a3fb99"},
{file = "msgspec-0.21.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d4248cf0b6129b7d230eacd493c17cc2d4f3989f3bb7f633a928a85b7dcfa251"},
{file = "msgspec-0.21.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5102c7e9b3acff82178449b85006d96310e690291bb1ea0142f1b24bcb8aabcb"},
{file = "msgspec-0.21.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:846758412e9518252b2ac9bffd6f0e54d9ff614f5f9488df7749f81ff5c80920"},
{file = "msgspec-0.21.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:21995e74b5c598c2e004110ad66ec7f1b8c20bf2bcf3b2de8fd9a3094422d3ff"},
{file = "msgspec-0.21.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6129f0cca52992e898fd5344187f7c8127b63d810b2fd73e36fca73b4c6475ee"},
{file = "msgspec-0.21.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ef3ec2296248d1f8b9231acb051b6d471dfde8f21819e86c9adaaa9f42918521"},
{file = "msgspec-0.21.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4ab834a054c6f0cbeef6df9e7e1b33d5f1bc7b86dea1d2fd7cad003873e783d"},
{file = "msgspec-0.21.1-cp312-cp312-win_arm64.whl", hash = "sha256:628aaa35c74950a8c59da330d7e98917e1c7188f983745782027748ee4ca573e"},
{file = "msgspec-0.21.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:764173717a01743f007e9f74520ed281f24672c604514f7d76c1c3a10e8edb66"},
{file = "msgspec-0.21.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:344c7cd0eaed1fb81d7959f99100ef71ec9b536881a376f11b9a6c4803365697"},
{file = "msgspec-0.21.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48943e278b3854c2f89f955ddc6f9f430d3f0784b16e47d10604ee0463cd21f5"},
{file = "msgspec-0.21.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9aa659ebb0101b1cbc31461212b87e341d961f0ab0772aaf068a99e001ec4aa"},
{file = "msgspec-0.21.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7b27d1a8ead2b6f5b0c4f2d07b8be1ccfcc041c8a0e704781edebe3ae13c484"},
{file = "msgspec-0.21.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:38fe93e86b61328fe544cb7fd871fad5a27c8734bfda90f65e5dbe288ae50f61"},
{file = "msgspec-0.21.1-cp313-cp313-win_amd64.whl", hash = "sha256:8bc666331c35fcce05a7cd2d6221adbe0f6058f8e750711413d22793c080ac6a"},
{file = "msgspec-0.21.1-cp313-cp313-win_arm64.whl", hash = "sha256:42bb1241e0750c1a4346f2aa84db26c5ffd99a4eb3a954927d9f149ff2f42898"},
{file = "msgspec-0.21.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fab48eb45fdbfbdb2c0edfec00ffc53b6b6085beefc6b50b61e01659f9f8757f"},
{file = "msgspec-0.21.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3cb779ea0c35bc807ff941d415875c1f69ca0be91a2e907ab99a171811d86a9a"},
{file = "msgspec-0.21.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:68604db36b3b4dd9bf160e436e12798a4738848144cea1aca1cb984011eb160f"},
{file = "msgspec-0.21.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d6b9dc50948eaf65df54d2fd0ff66e6d8c32f116037209ee861810eb9b676cb"},
{file = "msgspec-0.21.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:52c5e21930942302394429c5a582ce7e6b62c7f983b3760834c2ce107e0dd6df"},
{file = "msgspec-0.21.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:abbb39d65681fa24ed394e01af3d59d869068324f900c61d06062b7fb9980f2f"},
{file = "msgspec-0.21.1-cp314-cp314-win_amd64.whl", hash = "sha256:5666b1b560b97b6ec2eb3fca8a502298ebac56e13bbca1f88523538ce83d01ea"},
{file = "msgspec-0.21.1-cp314-cp314-win_arm64.whl", hash = "sha256:d8b8578e4c83b14ceea4cef0d0b747e31d9330fe4b03b2b2ad4063866a178f93"},
{file = "msgspec-0.21.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:15f523d51c00ebad412213bfe9f06f0a50ec2b93e0c19e824a2d267cabb48ea2"},
{file = "msgspec-0.21.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:4e47390360583ba3d5c6cb44cf0a9f61b0a06a899d3c2c00627cedebb2e2884b"},
{file = "msgspec-0.21.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f60800e6299b798142dc40b0644da77ceac5ea0568be58228417eae14135c847"},
{file = "msgspec-0.21.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5f8e9dfcd98419cf7568808470c4317a3fb30bef0e3715b568730a2b272a20d7"},
{file = "msgspec-0.21.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:92d89dfad13bd1ea640dc3e37e724ed380da1030b272bdf5ecafb983c3ad7c75"},
{file = "msgspec-0.21.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0d03867786e5d7ba25d666df4b11320c27170f4aeafcb8e3a8b0a50a4fb742ca"},
{file = "msgspec-0.21.1-cp314-cp314t-win_amd64.whl", hash = "sha256:740fbf1c9d59992ca3537d6fbe9ebbf9eaf726a65fbf31448e0ecbc710697a63"},
{file = "msgspec-0.21.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0d2cc73df6058d811a126ac3a8ad63a4dfa210c82f9cf5a004802eaf4712de90"},
{file = "msgspec-0.21.1.tar.gz", hash = "sha256:2313508e394b0d208f8f56892ca9b2799e2561329de9763b19619595a6c0f72c"},
]
[package.extras]
toml = ["tomli ; python_version < \"3.11\"", "tomli_w"]
yaml = ["pyyaml"]
[[package]]
name = "nanoid"
version = "2.0.0"
description = "A tiny, secure, URL-friendly, unique string ID generator for Python"
optional = false
python-versions = "*"
groups = ["main"]
files = [
{file = "nanoid-2.0.0-py3-none-any.whl", hash = "sha256:90aefa650e328cffb0893bbd4c236cfd44c48bc1f2d0b525ecc53c3187b653bb"},
{file = "nanoid-2.0.0.tar.gz", hash = "sha256:5a80cad5e9c6e9ae3a41fa2fb34ae189f7cb420b2a5d8f82bd9d23466e4efa68"},
]
[[package]]
name = "packaging"
version = "26.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e"},
{file = "packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661"},
]
[[package]]
name = "pluggy"
version = "1.6.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
]
[package.extras]
dev = ["pre-commit", "tox"]
testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "pygments"
version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pytest"
version = "9.0.3"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9"},
{file = "pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c"},
]
[package.dependencies]
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
iniconfig = ">=1.0.1"
packaging = ">=22"
pluggy = ">=1.5,<2"
pygments = ">=2.7.2"
[package.extras]
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "7.1.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678"},
{file = "pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2"},
]
[package.dependencies]
coverage = {version = ">=7.10.6", extras = ["toml"]}
pluggy = ">=1.2"
pytest = ">=7"
[package.extras]
testing = ["process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "python-dotenv"
version = "1.2.2"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"},
{file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "pyyaml"
version = "6.0.3"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"},
{file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"},
{file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"},
{file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"},
{file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"},
{file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"},
{file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"},
{file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"},
{file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"},
{file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"},
{file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"},
{file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"},
{file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"},
{file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"},
{file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"},
{file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"},
{file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"},
{file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"},
{file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"},
{file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"},
{file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"},
{file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"},
{file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"},
{file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"},
{file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"},
{file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"},
{file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"},
{file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"},
{file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"},
{file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"},
{file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"},
{file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"},
{file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"},
{file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"},
{file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"},
{file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"},
{file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"},
{file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"},
{file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"},
{file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"},
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
]
[[package]]
name = "rich"
version = "15.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.9.0"
groups = ["main"]
files = [
{file = "rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"},
{file = "rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36"},
]
[package.dependencies]
markdown-it-py = ">=2.2.0"
pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "shellingham"
version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
]
[[package]]
name = "tinydb"
version = "4.8.2"
description = "TinyDB is a tiny, document oriented database optimized for your happiness :)"
optional = false
python-versions = "<4.0,>=3.8"
groups = ["main"]
files = [
{file = "tinydb-4.8.2-py3-none-any.whl", hash = "sha256:f97030ee5cbc91eeadd1d7af07ab0e48ceb04aa63d4a983adbaca4cba16e86c3"},
{file = "tinydb-4.8.2.tar.gz", hash = "sha256:f7dfc39b8d7fda7a1ca62a8dbb449ffd340a117c1206b68c50b1a481fb95181d"},
]
[[package]]
name = "typer"
version = "0.25.0"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "typer-0.25.0-py3-none-any.whl", hash = "sha256:ac01b48823d3db9a83c9e164338057eadbb1c9957a2a6b4eeb486669c560b5dc"},
{file = "typer-0.25.0.tar.gz", hash = "sha256:123eaf9f19bb40fd268310e12a542c0c6b4fab9c98d9d23342a01ff95e3ce930"},
]
[package.dependencies]
annotated-doc = ">=0.0.2"
click = ">=8.2.1"
rich = ">=13.8.0"
shellingham = ">=1.3.0"
[[package]]
name = "werkzeug"
version = "3.1.8"
description = "The comprehensive WSGI web application library."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "werkzeug-3.1.8-py3-none-any.whl", hash = "sha256:63a77fb8892bf28ebc3178683445222aa500e48ebad5ec77b0ad80f8726b1f50"},
{file = "werkzeug-3.1.8.tar.gz", hash = "sha256:9bad61a4268dac112f1c5cd4630a56ede601b6ed420300677a869083d70a4c44"},
]
[package.dependencies]
markupsafe = ">=2.1.1"
[package.extras]
watchdog = ["watchdog (>=2.3)"]
[metadata]
lock-version = "2.1"
python-versions = "^3.11"
content-hash = "da7cc095f36ea5b79ca6deb213520dbc0c3bfd4598a105f4659bfe4ba4d50239"
+2 -2
View File
@@ -17,8 +17,8 @@ flask = "*"
tinydb = "^4.8.2" tinydb = "^4.8.2"
pyyaml = "^6.0.2" pyyaml = "^6.0.2"
nanoid = "^2.0.0" nanoid = "^2.0.0"
# grung-db = {git = "https://git.evilchi.li/evilchili/grung-db.git"} # grung-db = {git = "file:///home/greg/dev/grung-db/"}
grung-db = {git = "file:///home/greg/dev/grung-db/"} grung-db = {git = "https://git.evilchi.li/evilchili/grung-db.git"}
flask-session = "^0.8.0" flask-session = "^0.8.0"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
+101 -7
View File
@@ -11,7 +11,12 @@ from tinydb import where
from tinydb.storages import MemoryStorage from tinydb.storages import MemoryStorage
from ttfrog import schema from ttfrog import schema
from ttfrog.exceptions import ApplicationNotInitializedError from ttfrog.exceptions import (
ApplicationNotInitializedError,
MalformedRequestError,
RecordNotFoundError,
UnauthorizedError,
)
class ApplicationContext: class ApplicationContext:
@@ -46,6 +51,7 @@ ADMIN_EMAIL=admin@telisar
THEME=default THEME=default
VIEW_URI=/ VIEW_URI=/
API_URI=/_/v1/
""" """
@@ -90,15 +96,15 @@ VIEW_URI=/
if db: if db:
self.db = db self.db = db
elif self.config.IN_MEMORY_DB: elif self.config.IN_MEMORY_DB:
self.db = GrungDB.with_schema(schema, storage=MemoryStorage) self.db = GrungDB.with_schema(schema, path=self.path.database, storage=MemoryStorage)
else: else:
self.db = GrungDB.with_schema( self.db = GrungDB.with_schema(
schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ") schema, path=self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
) )
self.theme = Path(__file__).parent / "themes" / self.config.THEME self.theme = Path(__file__).parent / "themes" / self.config.THEME
self.web = Flask(self.config.NAME, template_folder=self.theme, static_folder=self.theme / 'static') self.web = Flask(self.config.NAME, template_folder=self.theme, static_folder=self.theme / "static")
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0 self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
self.web.config["DEBUG"] = True self.web.config["DEBUG"] = True
@@ -108,6 +114,8 @@ VIEW_URI=/
Session(self.web) Session(self.web)
self.log = self.web.logger
self._initialized = True self._initialized = True
def check_state(self) -> None: def check_state(self) -> None:
@@ -119,16 +127,16 @@ VIEW_URI=/
Returns the User record matching the given username and password Returns the User record matching the given username and password
""" """
if not (username and password): if not (username and password):
self.web.logger.debug("Need both username and password to login") self.log.debug("Need both username and password to login")
return None return None
user = self.db.User.get(where("name") == username) user = self.db.User.get(where("name") == username)
if not user: if not user:
self.web.logger.debug(f"No user matching {username}") self.log.debug(f"No user matching {username}")
return None return None
if not user.check_credentials(username, password): if not user.check_credentials(username, password):
self.web.logger.debug(f"Invalid credentials for {username}") self.log.debug(f"Invalid credentials for {username}")
return None return None
return user return user
@@ -136,5 +144,91 @@ VIEW_URI=/
def authorize(self, user, record, requested): def authorize(self, user, record, requested):
return user.has_permission(record, requested) return user.has_permission(record, requested)
def _get_or_create_page_by_uri(self, user, table, uri):
"""
Get a page by URI. If it doesn't exist, create a new one if and only if the user has permission
to write on its parent.
"""
uri = uri.replace(" ", "").strip("/")
if uri.startswith(self.config.VIEW_URI):
uri = uri.replace(self.config.VIEW_URI, "", 1)
parent_uri = None
search_uri = uri
page_name = uri
if "/" in uri:
(parent_uri, page_name) = uri.rsplit("/", 1)
if parent_uri == "Page":
parent_uri = None
search_uri = page_name
else:
search_uri = uri
self.log.debug(f"Looking for page with uri {search_uri}")
page = table.get(where("uri") == search_uri, recurse=False)
if not page:
# load the parent to check for write permissions
parent_table = table if parent_uri and "/" in parent_uri else self.db.Page
parent = None
try:
# self.log.debug(f"Loading parent with {parent_uri}")
parent = self.get_page(user, parent_table.name, uri=parent_uri)
except Exception as e:
self.log.debug(f"Error loading parent: {e}")
if not parent:
raise MalformedRequestError(f"Page with uri '{search_uri}' does not exist and neither does its parent.")
if not self.authorize(user, parent, schema.Permissions.WRITE):
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
obj = getattr(schema, table.name)
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent, uri=search_uri)
# validate the page name before we try to create anything
page._metadata.fields["name"].validate(page, db=self.db)
# self.log.debug(f"Returning {page.doc_id}: {page.uri}")
return page
def get_page(self, user, table_name, doc_id=None, uri=None):
"""
Get a page by doc_id or by URI, if and only if the user is allowed to read it. A new Record
instance will be returned if the requested page does not exist but the user has permission
to create it.
"""
if not user.doc_id:
self.log.error(f"Invalid user: {user}")
raise MalformedRequestError("User does not exist.")
try:
table = self.db.table(table_name)
except RuntimeError:
table = self.db.Page
self.log.error(f"Invalid table_name: {table_name}, will use Page")
# raise MalformedRequestError(f"{table_name} table does not exist.")
if doc_id:
page = table.get(doc_id=doc_id)
if not page:
raise RecordNotFoundError(f"No record with {doc_id=} was found.")
page = self._get_or_create_page_by_uri(user, table, uri)
if not self.authorize(user, page, schema.Permissions.READ):
self.log.error(f"No permission for {user.name} on {page}")
raise UnauthorizedError(f"User {user.doc_id} does not have permission to read {table_name} {page.doc_id}.")
# resolve the pointers to subpages so we can render things like nav elements.
if hasattr(page, "members"):
subpages = []
for pointer in page.members:
table, pkey, pval = pointer.split("::")
subpages += self.db.table(table).search(where(pkey) == pval, recurse=False)
page.members = subpages
return page
sys.modules[__name__] = ApplicationContext() sys.modules[__name__] = ApplicationContext()
+234 -28
View File
@@ -1,64 +1,270 @@
from ttfrog import app, schema from ttfrog import app, schema
TEMPLATE = """ TEMPLATE = """
# Heading 1 # Wiki Syntax
{{toc box depth="3"}}
## Heading 2 ## Basic Formatting
### Heading 3
#### Heading 4 The wiki uses [GitHub-flavored Markdown](https://github.github.com/gfm/).
##### Heading 5 {{style equal-widths striped
###### Heading 6 | Type | To Get |
|------|--------|
| `*emphasis*` | *emphasis* |
| `**bold**` | **bold** |
| `[link label](/link/address)` | [link label](/link/address) |
| `` `inline` `` | `inline` |
| `***` | <hr> |
| `# Heading 1` | {{{ <h1>Heading 1</h1> }}} |
| `## Heading 2` | {{{ <h2>Heading 2</h2> }}} |
| `### Heading 3` | {{{ <h3>Heading 3</h3> }}} |
| `#### Heading 4` | {{{ <h4>Heading 4</h4> }}} |
| `##### Heading 5` | {{{ <h5>Heading 5</h5> }}} |
| `###### Heading 6` | {{{ <h6>Heading 6</h6> }}} |
| {{{
*** <pre>
* unordered
* list
* items
</pre>
Normal text. }}} | {{{
**Bold text.**
*Italic Text.*
[A link](/).
1. a <ul><li>unordered</li><li>list</li><li>items</li></ul>
2. numbered
3. list.
> a block quote }}} |
| {{{
| A | Table | Section | <pre>
| --- | ----- | ------- | 1. ordered
| foo | bar | baz | 1. list
1. items
</pre>
}}} | {{{ <ol><li>ordered</li><li>list</li><li>items</li></ol> }}} |
| {{{
<pre>
> blockquote
</pre>
}}} | <blockquote>blockquote</blockquote> |
| {{{
<pre>
```
preformatted
code
block
```
</pre>
}}} | {{{ <pre>preformatted code<br>block</pre> }}} |
| {{{
<pre>
\\{\\{\\{
multi-line content,
whitespace preserved,
\\*\\*markup\\*\\* and \\<i\\>HTML\\</i\\> included.
\\}\\}\\}
</pre>
}}} | {{{ <p>multi-line content,<br>whitespace preserved,<br><b>markup</b> and <i>HTML</i> included.</p> }}} |
}}
## Macros
Macros extend authoring options with computed values evaluated at display time.
There are two types: *inline*, which can be used in the middle of a line of
markdown, and *block*, which enclose a series of lines.
### Inline Macros
The basic format of an inline macro is:
`{{NAME [KEYWORD ..] [PARAM="VALUE" ..]}}`
* `NAME` is the name of the macro. It is always lower case.
* `KEYWORD` is a bare word
* `PARAM` is a parameter name
* `VALUE` is the value of the associated parameter
No macro requires keywords or parameters, but some support them; they are documented below.
### Block Macros
The format of a block macro uses the same format for name, keywords and parameters. The closing
braces must appear on their own line, and both the opening and closing lines must be
surrounded by blank lines. This ensures the block macro is not interpreted as markdown.
```
Before the macro..
{{NAME [KEYWORD ..] [PARAM="VALUE" ..]
..inside the macro..
}}
..after the macro.
```
### Supported Macros
This section documents all supported macros.
WIP (move to its own page)
{{style striped equal-widths
| Type | To Get | Description |
|------|--------|-------------|
|{{{ <pre>{{user}}</pre> }}} | {{user}} | A hyperlink to the current user's wiki page. |
|{{{ <pre>{{style [STYLE ..]<br>pretty markup goes here!<br><br>}}</pre> }}}| pretty markup goes here! | Styled markup. See Block Styling and Positioning, below. |
|{{{ <pre>{{toc [STYLE ..] depth="N"}}</pre>}}} | {{toc depth="1"}} | A table of contents. The default depth is 3, and styles may be assigned as with the `style` macro. |
|{{{ <pre>{{widget NAME}}</pre> }}} | *varies* | Insert specified widget; see [Wiki Syntax: Widgets](/Widgets). |
### Block Styling and Positioning
Several styles are supported by the editor, which may be freely combined. These include:
* **box**: block element with background and border
* **full-width**: Expand block to fill all horizontal space
* **right**: right-aligned floating block (maximum 30% of full width)
* **left**: left-aligned floating block (maximum 30% of full width)
* **center**: centered block
* **inline**: contained within the current text line
#### Examples
{{style striped equal-widths
| Type | To Get |
|------|--------|
| {{{ <pre>{{style box<br><br>😶 box, full-width<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box'>😶 box, full-width</div>normal text }}} |
| {{{ <pre>{{style right<br><br>😶 right-aligned, inline<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='right'>😶 right-aligned, inline</div>normal text }}} |
| {{{ <pre>{{style box right<br><br>😶 box right-aligned, inline<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box right'>😶 box right-aligned, inline</div>normal text }}} |
| {{{ <pre>{{style box center<br><br>😶 box center-aligned, block<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box center'>😶 box center-aligned, block</div>normal text }}} |
| {{{ <pre>{{style box left<br><br>😶 box left-aligned, inline<br><br>}}<br><br>normal text</pre> }}} | {{{ <div class='box left'>😶 box left-aligned, inline</div>normal text }}} |
}}
### Tables
Basic tables follow the Github-Flavoured Markdown. By default, table cells may not contain newlines, but you can add multi-line content to your tables using {{{ &#123;&#123;&#123; and &#125;&#125;&#125; }}}:
{{style equal-widths striped
| Type | To Get |
|------|--------|
| {{{
<pre>
\\| head 1 \\| head 2 \\|
\\| ------ \\| ------ \\|
\\| cell 1 \\| cell 2 \\|
</pre>
}}} | {{{ <table><tr><th>head 1</th><th>head 2</th></tr><tr><td>cell 1</td><td>cell 2</td></tr></table> }}} |
| {{{
<pre>
\\| head 1 \\| head 2 \\|
\\| ------ \\| ------ \\|
\\| \\&#123;\\&#123;\\&#123;
multi-line
content
&#125;&#125;&#125; \\| cell 3 \\|
</pre>
}}} | {{{ <table><tr><th>head 1</th><th>head 2</th></tr><tr><td><p>multi-line<br>content</p></td><td>cell 3</td></tr></table> }}} |
}}
#### Table Styling
{{style box right outer
**Striped, Equal-Widths Example:**
{{style striped equal-widths inner1
| A | B|
|---|--|
| This table has | and |
| equal | alternating |
| widths! | backgrounds! |
}}
**Layout Example:**
{{style layout inner2
| A | B|
|---|--|
| row 1, col 1 | row 1, col 2|
| row 2, col 1 | row 2, col 2|
}}
}}
By wrapping a table in a `style` macro, you can use the following block-level styles on the tables themselves:
* `striped`: Apply zebra-striping to table rows
* `equal-widths`: Enforce equal widths of all table columns
* `layout`: Hide the table headers, borders, and background colors. This is useful in cases
where you would like to control layout of elements using tables.
""" """
def bootstrap(): def bootstrap():
""" """
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group. Bootstrap the database entries by poplating the first Page, the Admin user and the Admins group.
""" """
app.check_state() app.check_state()
# create the top-level pages # the system user does not get added to the list of Users.
root = app.db.save(schema.Page(name=app.config.VIEW_URI, title="Home", body="This is the home page")) app.db.save(schema.User(name="__system__"))
users = root.add_member(schema.Page(name="User", title="Users", body="users go here.")) # create the top-level pages
groups = root.add_member(schema.Page(name="Group", title="Groups", body="groups go here.")) root = app.db.save(schema.Page(name=app.config.VIEW_URI, body="This is the home page", uri=""))
npcs = root.add_member(schema.Page(name="NPC", title="NPCS!", body="NPCS!"))
wiki = root.add_member(schema.Page(name="Wiki", title="Wiki", body=TEMPLATE)) root.add_member(schema.Page(name="Wiki", body=TEMPLATE))
users = root.add_member(schema.Page(name="User", body="# Users\nusers go here."))
groups = root.add_member(schema.Page(name="Group", body="# Groups\ngroups go here."))
npcs = root.add_member(schema.Page(name="NPC", body="# NPCS!"))
widgets = root.add_member(schema.Page(name="Widget", body="Widgets go here."))
widgets.add_member(schema.Widget(name="hello", body=schema.Widget.default))
# create the NPCs # create the NPCs
npcs.add_member(schema.NPC(name="Sabetha", body="")) npcs.add_member(schema.NPC(name="Sabetha", body=""))
npcs.add_member(schema.NPC(name="John", body="")) npcs.add_member(schema.NPC(name="John", body=""))
# create the users # create the users
guest = users.add_member(schema.User(name="guest")) guest = users.add_member(schema.User(name="guest", body="# guest"))
admin = users.add_member( admin = users.add_member(
schema.User(name=app.config.ADMIN_USERNAME, password="fnord", email=app.config.ADMIN_EMAIL) schema.User(name=app.config.ADMIN_USERNAME, password="fnord", email=app.config.ADMIN_EMAIL, body="# fnord")
) )
# create the admin user and admins group # create the admin user and admins group
admins = groups.add_member(schema.Group(name="administrators", members=[admin])) admins = groups.add_member(schema.Group(name="administrators", members=[admin], body="# administrators"))
# admins get full access # admins get full access
root.set_permissions( root.set_permissions(
+18
View File
@@ -3,3 +3,21 @@ class ApplicationNotInitializedError(Exception):
Thrown when attempting to access methods on the Thrown when attempting to access methods on the
ApplicationContext before it has been initialized. ApplicationContext before it has been initialized.
""" """
class MalformedRequestError(Exception):
"""
Thrown when a request cannnot be completed due to bad arguments.
"""
class RecordNotFoundError(Exception):
"""
Thrown when the specified record could not be loaded by doc_id.
"""
class UnauthorizedError(Exception):
"""
Thrown when a user does not have permissino to do the requested action.
"""
-75
View File
@@ -1,75 +0,0 @@
from dataclasses import dataclass, field
from functools import cached_property
from flask import g
from grung.types import BackReference, Collection, Pointer, Record
from ttfrog import schema
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference]
@dataclass
class Form:
"""
The base Form controller for the web UI.
"""
record: Record
data: field(default_factory=dict)
@cached_property
def read_only(self) -> set:
return [
name for (name, attr) in self.record._metadata.fields.items() if type(attr) in READ_ONLY_FIELD_TYPES
] + ["uid"]
def prepare(self):
for key, value in self.data.items():
# filter out fields that cannot be set by the user
if key in self.read_only:
continue
self.record[key] = value
self.record.author = g.user
return self.record
@dataclass
class Page(Form):
"""
A form for creating and updating Page records.
"""
record: schema.Page
@cached_property
def read_only(self) -> set:
return set(list(super().read_only) + ["stub"])
@dataclass
class NPC(Page):
"""
A form for creating and updating Page records.
"""
record: schema.NPC
@dataclass
class User(Page):
"""
A form for creating and updating Page records.
"""
record: schema.NPC
@dataclass
class Group(Page):
"""
A form for creating and updating Page records.
"""
record: schema.NPC
+261 -59
View File
@@ -1,12 +1,40 @@
from __future__ import annotations from __future__ import annotations
import re
from datetime import datetime from datetime import datetime
from typing import List
from enum import StrEnum from enum import StrEnum
from functools import cached_property
from textwrap import dedent
from typing import List
from grung.types import BackReference, Collection, DateTime, Dict, Field, Password, Pointer, Record, Timestamp from flask import g
from grung.objects import (
BackReference,
Collection,
DateTime,
Dict,
Password,
Pointer,
Record,
String,
TextFilePointer,
Timestamp,
)
from grung.validators import LengthValidator, PatternValidator
from tinydb import where from tinydb import where
from ttfrog.exceptions import MalformedRequestError
def app_context():
import ttfrog.app
ttfrog.app.check_state()
return ttfrog.app
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
class Permissions(StrEnum): class Permissions(StrEnum):
READ = "r" READ = "r"
@@ -19,62 +47,122 @@ class Page(Record):
A page in the wiki. Just about everything in the databse is either a Page or a subclass of a Page. A page in the wiki. Just about everything in the databse is either a Page or a subclass of a Page.
""" """
default = dedent(
"""
# {name}
*Overview of this page*
## Section 1
*Organize your text into logically separted sections.*
"""
)
@classmethod @classmethod
def fields(cls): def fields(cls):
# fmt: off # fmt: off
return [ return [
*super().fields(), *super().fields(),
Field("uri", unique=True), # The URI for the page, relative to the app's VIEW_URI
Field("name"), # The portion of the URI after the last / # The URI for the page, relative to the app's VIEW_URI
Field("title"), # The page title String("uri", unique=True, validators=[
Field("body"), # The main content blob of the page PatternValidator(re.compile(
Collection("members", Page), # The pages that exist below this page's URI r"""
BackReference("parent", value_type=Page), # The page that exists above this page's URI ^ # match from beginning of line
Pointer("author", value_type=User), # The last user to touch the page. (?:
DateTime("created"), # When the page was created / # first char can be a /
Timestamp("last_modified"), # The last time the page was modified. | # or
(?:
[0-9a-z] # first char is alphanum
(?:/?[0-9a-z]+)* # following by more alphnum, maybe with a leading /
)+ # at least once, possibly many times,
)? # or URI could be entirely blank
$ # until the end of the string
""",
re.IGNORECASE | re.VERBOSE
)),
]),
# The portion of the URI after the last /
String("name", validators=[
PatternValidator(re.compile(r'^(?:/|(?:[0-9a-z]+))$', re.IGNORECASE)),
LengthValidator(min=1, max=64),
]),
# The pages that exist below this page's URI
Collection("members", member_type=Page),
# The last user to touch the page.
Pointer("author", value_type=User),
# When the page was created
DateTime("created"),
# The last time the page was modified.
Timestamp("last_modified"),
# The main content blob of the page
TextFilePointer("body", extension='.md', default=Page.default),
# The access control list for this Page.
Dict("acl"), Dict("acl"),
] ]
# fmt: on # fmt: on
def before_insert(self, db): @cached_property
def read_only(self) -> set:
return [name for (name, attr) in self._metadata.fields.items() if type(attr) in READ_ONLY_FIELD_TYPES] + [
"uid",
"acl",
]
def update(self, **kwargs):
super().update(**dict((key, value) for key, value in kwargs.items() if key not in self.read_only))
def parent(self):
if self.uri == "":
return None
app = app_context()
parent_uri = ""
if self.uri and "/" in self.uri:
parent_uri = self.uri.rsplit("/", 1)[0]
for table_name in app.db.tables():
page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False)
if page:
return page
def before_insert(self, *args, **kwargs):
""" """
Make the following adjustments before saving this record: Make the following adjustments before saving this record:
* Derive the name from the title, or the title from the name
* Derive the URI from the hierarchy of the parent. * Derive the URI from the hierarchy of the parent.
""" """
super().before_insert(db) if not self.name:
raise Exception("Must provide a name")
app = app_context()
super().before_insert(app.db)
if not self.author:
self.author = app.db.User.get(where("name") == "__system__")
now = datetime.utcnow() now = datetime.utcnow()
if not self.doc_id and self.created < now: if not self.doc_id and self.created < now:
self.created = now self.created = now
if not self.name and not self.title:
raise Exception("Must provide either a name or a title!")
if not self.name:
self.name = self.title.title().replace(" ", "")
if not self.title:
self.title = self.name
self.uri = (self.parent.uri + "/" if self.parent and self.parent.uri != "/" else "") + self.name
def after_insert(self, db):
"""
After saving this record, ensure that any page in the members collection is updated with the
correct URI. This ensures that if a page is moved from one collection to another, the URI is updated.
"""
super().after_insert(db)
if not hasattr(self, "members"):
return
for child in self.members:
obj = BackReference.dereference(child, db)
obj.uri = f"{self.uri}/{obj.name}"
child = db.save(obj)
def add_member(self, child: Record): def add_member(self, child: Record):
from ttfrog import app app = app_context()
app.check_state() prefix = (self.uri + "/") if self.uri else ""
new_uri = f"{prefix}{child.name}"
if child.uri != new_uri:
app.log.debug(f"Moving {child._metadata.table}[{child.doc_id}] from {child.uri} to {new_uri}")
child.uri = new_uri
self.members = list(set(self.members + [app.db.save(child)])) self.members = list(set(self.members + [app.db.save(child)]))
app.db.save(self) app.db.save(self)
return self.get_child(child) return self.get_child(child)
@@ -86,9 +174,7 @@ class Page(Record):
return None return None
def set_permissions(self, entity: Entity, permissions: List) -> str: def set_permissions(self, entity: Entity, permissions: List) -> str:
from ttfrog import app app = app_context()
app.check_state()
perms = "".join(permissions) perms = "".join(permissions)
self.acl[entity.reference] = perms self.acl[entity.reference] = perms
@@ -100,9 +186,7 @@ class Page(Record):
Search upward through the page hierarchy looking for one with an ACL that either Search upward through the page hierarchy looking for one with an ACL that either
has a grant for the entity we care about, or at least one group in which the entity is a member. has a grant for the entity we care about, or at least one group in which the entity is a member.
""" """
from ttfrog import app app = app_context()
app.check_state()
def find_acl(obj): def find_acl(obj):
if hasattr(obj, "acl"): if hasattr(obj, "acl"):
@@ -120,23 +204,18 @@ class Page(Record):
return group_grants return group_grants
if hasattr(obj, "parent"): if hasattr(obj, "parent"):
return find_acl(obj.parent) return find_acl(obj.parent())
return {"": ""} return {"": ""}
return find_acl(self) return find_acl(self)
class Entity(Page): class Entity(Page):
@classmethod @classmethod
def fields(cls): def fields(cls):
inherited = [ inherited = [field for field in super().fields() if field.name not in ("members", "uid")]
field
for field in super().fields()
if field.name not in ("members", "uid")
]
return inherited + [ return inherited + [
Field("name", primary_key=True), String("name", primary_key=True),
] ]
def has_permission(self, record: Record, requested: str) -> bool | None: def has_permission(self, record: Record, requested: str) -> bool | None:
@@ -162,27 +241,150 @@ class User(Entity):
@classmethod @classmethod
def fields(cls): def fields(cls):
return super().fields() + [ return super().fields() + [String("email", unique=True), Password("password")]
Field("email", unique=True),
Password("password"), def update(self, **data):
] self.author = None if self == g.user else g.user
return super().update(**data)
def validate(self):
if self.name == "__system__":
raise MalformedRequestError("Invalid name.")
return super().validate()
def check_credentials(self, username: str, password: str) -> bool: def check_credentials(self, username: str, password: str) -> bool:
return username == self.name and self._metadata.fields["password"].compare(password, self.password) return username == self.name and self._metadata.fields["password"].compare(password, self.password)
def after_insert(self, *args, **kwargs):
"""
After saving this record, ensure that any page in the members collection is updated with the
correct URI. This ensures that if a page is moved from one collection to another, the URI is updated.
"""
app = app_context()
super().after_insert(app.db)
if not hasattr(self, "members"):
return
for child in self.members:
obj = BackReference.dereference(child, app.db, recurse=False)
obj.uri = f"{self.uri}/{obj.name}"
child = app.db.save(obj)
class Group(Entity): class Group(Entity):
""" """
A set of users, editable as a wiki page. A set of users, editable as a wiki page.
""" """
@classmethod @classmethod
def fields(cls): def fields(cls):
return super().fields() + [ return super().fields() + [Collection("members", member_type=Entity)]
Collection("members", Entity)
]
class NPC(Page): class NPC(Page):
""" """
An NPC, editable as a wiki page. An NPC, editable as a wiki page.
""" """
default = dedent(
"""
# {name}
*[Ancestry] [Class]*
| AC | HP | STR | DEX | CON | INT | WIS | CHA
|----|----|-----|-----|-----|-----|-----|------
| 10 | 10 | +0 | +0 | +0 | +0 | +0 | +0
**{name} (they/they)** [description]
* Personality: **[keywords]**
* Flaw: **[flaw]**
* Goal: **[goal]**
"""
)
class Widget(Page):
"""
Wiki UX widgets
"""
default = dedent(
"""
This is a sample widget that you can customize to your liking. Widget pages must contain at minimum the
**Template** section. It, along with the optional **CSS** and **Processor** sections, will be parsed by
wiki at display time. All other content on this page is ignored, so you can include usage docs, exmaples,
and so on, just like this text and the annnotations below in *italics*.
The name of your widget is the portion of the URI following `/Widget/`.
# Hello, World Example Widget
*Provide a description of your widget.*
Insert the word "HELLO" and optionally a name.
## Usage
*Display the usage of your widget. Ensure that the example is enclosed in a preformatted (`pre`) block.*
*Be sure to include a description of any parameters and keyword arguments.*
<pre>
{{widget hello [NAME] }}
</pre>
## Example
*Include one or more example uses of the widget. Once you save this page, you can refer to your widget directly.*
Here is what it looks like to say hello to the whole world: {{widget hello world}}!
## Template
*The template is javascript string that will be evaluated when the widget is loaded. Any keyword parameters*
*you specify in the Usage section above will be available as variables, so you can use variable subsetitution*
*in your template definition. The template must be enclose in a code block.*
```
HELLO ${{token.keywords.split(" ").slice(1).join(" ") || ""}}
```
## CSS
*If you want to customize the styling of your widget, include CSS in this section. This CSS must be enclosed*
*in a code block.*
```
display: inline;
background: green;
padding: 3px;
color: white;
border-radius: 5px;
```
## Processor
*If you want full control over how your widget is processed, you can override the default processor*
*here. The processor function below is the default.*
```
function(token, widget) {{
/*
* token The token object created by the wiki parser. token.keywords and
* token.params contain the keyword and paramater arguments from
* the wiki page source where the widget was used.
* widget: The widget instance. widget.css, widget.template, and
* widget.processor contain the definitions parsed from the
* corresponding sections on this page.
*/
var ret = '';
eval("ret = `" + widget.template + "`");
return ret;
}}
```
"""
)
@classmethod
def fields(cls):
inherited = [field for field in super().fields() if field.name not in ("members", "uid")]
return inherited + [
String("name", primary_key=True),
]
+12 -7
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>{% block title %}TTFROG{% endblock %}</title> <title>{% block title %}TTFROG{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='site.css' ) }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat.css' ) }}">
{% block styles %} {% block styles %}
{% endblock %} {% endblock %}
</head> </head>
@@ -13,16 +13,21 @@
<input name='title' id="data_form__title" type='text' value="{{ page.title }}"> <input name='title' id="data_form__title" type='text' value="{{ page.title }}">
<textarea name="body" id="data_form__body">{{ page.body }}</textarea> <textarea name="body" id="data_form__body">{{ page.body }}</textarea>
</form> </form>
<div id='masthead'>
{% block nav %} {% block nav %}
{% include "nav.html" %} {% include "nav.html" %}
{% include "breadcrumbs.html" %}
{% endblock %} {% endblock %}
<div class='table-wrapper'> {% if user.can_write(page) %}
<main> {% include "toolbar.html" %}
{% for message in g.messages %} {% endif %}
<div class="alert">
{{ message }}
</div> </div>
<div class='main-aligned'>
<main id='main'>
{% for message in g.messages %}
<dialog class="alert">
{{ message }}
</dialog>
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
+1 -5
View File
@@ -1,7 +1,3 @@
<div id='breadcrumbs'> <div id='breadcrumbs' class='content-aligned'>
<a href="{{ url_for('index') }}">Home</a>{% for (uri, name) in breadcrumbs %}.<a href="{{ uri }}">{{ name }}</a>{% endfor %} <a href="{{ url_for('index') }}">Home</a>{% for (uri, name) in breadcrumbs %}.<a href="{{ uri }}">{{ name }}</a>{% endfor %}
<div id='actions'>
<a id='action__edit' data-state='view'>edit</a>
<a id='action__save' style='display: none;'>save</a>
</div>
</div> </div>
+2 -20
View File
@@ -1,5 +1,5 @@
<nav> <nav id='pagenav'>
<ul class="container"> <ul class="container content-aligned">
<li><a href='{{ root.uri }}'>Home</a></li> <li><a href='{{ root.uri }}'>Home</a></li>
{% for subpage in root.members %} {% for subpage in root.members %}
{% if user.can_read(subpage) %} {% if user.can_read(subpage) %}
@@ -14,24 +14,6 @@
[<a href="{{ url_for('logout') }}">Logout</a>] [<a href="{{ url_for('logout') }}">Logout</a>]
{% endif %} {% endif %}
</li> </li>
<!--
<li class='dropdown'>
<a href='#'>Pages <i class="fa fa-angle-down"></i></a>
<div class='mega-menu'>
<div class="container">
<div class="item">
<h3>Home Page</h3>
<ul>
<li><a href='#'>Home Page No #1</a></li>
<li><a href='#'>Home Page No #2</a></li>
<li><a href='#'>Home Page No #3</a></li>
<li><a href='#'>Home Page No #4</a></li>
</ul>
</div>
<</div>
</li>
-->
</ul> </ul>
</nav> </nav>
<menu> <menu>
+13 -46
View File
@@ -1,60 +1,27 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block styles %} {% block styles %}
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" /> {% if user.can_write(page) %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat-editor.css' ) }}">
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include "breadcrumbs.html" %} <article id='froghat'>{{ page.body }}</article>
<div id='viewer'></div>
<div id='editor'></div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script> <!-- ribbit editor library -->
<script src="{{ url_for('static', filename='ribbit.js' ) }}"></script>
<!-- ttfrog app code -->
<script src="{{ url_for('static', filename='froghat.js' ) }}"></script>
{% if user.can_write(page) %}
<script src="{{ url_for('static', filename='froghat-editor.js' ) }}"></script>
{% endif %}
<script> <script>
const viewerEl = document.querySelector("#viewer"); const wiki = new Froghat{% if user.can_write(page) %}Editor{% endif %}({plugins: [MacroPlugin]});
const editorEl = document.querySelector("#editor");; wiki.run();
const initialValue = (
"# " + document.getElementById("data_form__title").value +
"\n" +
document.getElementById("data_form__body").value
);
const editor = new toastui.Editor({
el: editorEl,
initialEditType: 'wysiwyg',
initialValue: initialValue,
minHeight: '500px',
previewStyle: 'tab',
usageStatistics: false
});
const edit_btn = document.querySelector("#action__edit");
const save_btn = document.querySelector("#action__save");
toggleEditor = function() {
if (edit_btn.dataset.state == 'view') {
edit_btn.dataset.state = 'edit';
edit_btn.innerText = "discard draft";
viewerEl.style['display'] = "none";
editorEl.style['display'] = "inline";
save_btn.style['display'] = 'inline-block';
} else {
edit_btn.dataset.state = 'view';
edit_btn.innerText = "edit";
editorEl.style['display'] = "none";
viewerEl.style['display'] = "inline";
save_btn.style['display'] = 'none';
}
};
edit_btn.addEventListener('click', toggleEditor)
viewerEl.innerHTML = editor.getHTML();
</script> </script>
{% endblock %} {% endblock %}
@@ -0,0 +1,192 @@
:root {
--toolbar-background: transparent;
--toolbar-border-radius: var(--content-border-radius);
--toolbar-enabled-background: var(--content-background);
--toolbar-height: 32px !important;
--toolbar-spacing: 5px !important;
--toolbar-button-size: 32px;
--toolbar-button-enabled-background: rgba(128, 192, 128);
--toolbar-button-active-background: rgba(192, 255, 192);
--toolbar-button-enabled-border: 1px solid #000;
--toolbar-icon-bold: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/></svg>');
--toolbar-icon-italic: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/></svg>');
/*
--toolbar-icon-underline: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57s-2.687-1.08-2.687-2.57zM12.5 15h-9v-1h9z"/></svg>');
*/
--toolbar-icon-h1: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.648 13V3H6.3v4.234H1.348V3H0v10h1.348V8.421H6.3V13zM14 13V3h-1.333l-2.381 1.766V6.12L12.6 4.443h.066V13z"/></svg>');
--toolbar-icon-h2: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13zm3.174-7.071v-.05c0-.934.66-1.752 1.801-1.752 1.005 0 1.76.639 1.76 1.651 0 .898-.582 1.58-1.12 2.19l-3.69 4.2V13h6.331v-1.149h-4.458v-.079L13.9 8.786c.919-1.048 1.666-1.874 1.666-3.101C15.565 4.149 14.35 3 12.499 3 10.46 3 9.384 4.393 9.384 5.879v.05z"/></svg>');
--toolbar-icon-h3: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M11.07 8.4h1.049c1.174 0 1.99.69 2.004 1.724s-.802 1.786-2.068 1.779c-1.11-.007-1.905-.605-1.99-1.357h-1.21C8.926 11.91 10.116 13 12.028 13c1.99 0 3.439-1.188 3.404-2.87-.028-1.553-1.287-2.221-2.096-2.313v-.07c.724-.127 1.814-.935 1.772-2.293-.035-1.392-1.21-2.468-3.038-2.454-1.927.007-2.94 1.196-2.981 2.426h1.23c.064-.71.732-1.336 1.744-1.336 1.027 0 1.744.64 1.744 1.568.007.95-.738 1.639-1.744 1.639h-.991V8.4ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
--toolbar-icon-h4: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M13.007 3H15v10h-1.29v-2.051H8.854v-1.18C10.1 7.513 11.586 5.256 13.007 3m-2.82 6.777h3.524v-5.62h-.074a95 95 0 0 0-3.45 5.554zM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
--toolbar-icon-h5: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M9 10.516h1.264c.193.976 1.112 1.364 2.01 1.364 1.005 0 2.067-.782 2.067-2.247 0-1.292-.983-2.082-2.089-2.082-1.012 0-1.658.596-1.924 1.077h-1.12L9.646 3h5.535v1.141h-4.415L10.5 7.28h.072c.201-.316.883-.84 1.967-.84 1.709 0 3.13 1.177 3.13 3.158 0 2.025-1.407 3.403-3.475 3.403-1.809 0-3.1-1.048-3.194-2.484ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.512h4.854V13z"/></svg>');
--toolbar-icon-h6: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M15.596 5.178H14.3c-.106-.444-.62-1.072-1.706-1.072-1.332 0-2.325 1.269-2.325 3.947h.07c.268-.67 1.043-1.445 2.445-1.445 1.494 0 3.017 1.064 3.017 3.073C15.8 11.795 14.37 13 12.48 13c-1.036 0-2.093-.36-2.77-1.452C9.276 10.836 9 9.808 9 8.37 9 4.656 10.494 3 12.636 3c1.812 0 2.883 1.113 2.96 2.178m-5.151 4.566c0 1.367.944 2.15 2.043 2.15 1.128 0 2.037-.684 2.037-2.136 0-1.41-1-2.065-2.03-2.065-1.19 0-2.05.853-2.05 2.051M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
--toolbar-icon-unordered_list: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>');
--toolbar-icon-ordered_list: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/><path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635z"/></svg>');
--toolbar-icon-line: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8"/></svg>');
--toolbar-icon-quote: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M2.5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1zm5 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1zm.79-5.373q.168-.117.444-.275L3.524 6q-.183.111-.452.287-.27.176-.51.428a2.4 2.4 0 0 0-.398.562Q2 7.587 2 7.969q0 .54.217.873.217.328.72.328.322 0 .504-.211a.7.7 0 0 0 .188-.463q0-.345-.211-.521-.205-.182-.568-.182h-.282q.036-.305.123-.498a1.4 1.4 0 0 1 .252-.37 2 2 0 0 1 .346-.298zm2.167 0q.17-.117.445-.275L5.692 6q-.183.111-.452.287-.27.176-.51.428a2.4 2.4 0 0 0-.398.562q-.165.31-.164.692 0 .54.217.873.217.328.72.328.322 0 .504-.211a.7.7 0 0 0 .188-.463q0-.345-.211-.521-.205-.182-.568-.182h-.282a1.8 1.8 0 0 1 .118-.492q.087-.194.257-.375a2 2 0 0 1 .346-.3z"/></svg>');
--toolbar-icon-link: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>');
--toolbar-icon-table: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 2h-4v3h4zm0 4h-4v3h4zm0 4h-4v3h3a1 1 0 0 0 1-1zm-5 3v-3H6v3zm-5 0v-3H1v2a1 1 0 0 0 1 1zm-4-4h4V8H1zm0-4h4V4H1zm5-3v3h4V4zm4 4H6v3h4z"/></svg>');
--toolbar-icon-macro: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M2.114 8.063V7.9c1.005-.102 1.497-.615 1.497-1.6V4.503c0-1.094.39-1.538 1.354-1.538h.273V2h-.376C3.25 2 2.49 2.759 2.49 4.352v1.524c0 1.094-.376 1.456-1.49 1.456v1.299c1.114 0 1.49.362 1.49 1.456v1.524c0 1.593.759 2.352 2.372 2.352h.376v-.964h-.273c-.964 0-1.354-.444-1.354-1.538V9.663c0-.984-.492-1.497-1.497-1.6M13.886 7.9v.163c-1.005.103-1.497.616-1.497 1.6v1.798c0 1.094-.39 1.538-1.354 1.538h-.273v.964h.376c1.613 0 2.372-.759 2.372-2.352v-1.524c0-1.094.376-1.456 1.49-1.456V7.332c-1.114 0-1.49-.362-1.49-1.456V4.352C13.51 2.759 12.75 2 11.138 2h-.376v.964h.273c.964 0 1.354.444 1.354 1.538V6.3c0 .984.492 1.497 1.497 1.6"/></svg>');
--toolbar-icon-macro_user: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/><path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/></svg>');
--toolbar-icon-macro_toc: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"/><path d="M5 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 5 8m0-2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m0 5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-1-5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0M4 8a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0m0 2.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/></svg>');
--toolbar-icon-macro_style: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/><path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/></svg>');
--toolbar-icon-markdown: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16"><path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0m6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0"/></svg>');
--toolbar-icon-save: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/><path fill-rule="evenodd" d="M7.646 4.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V14.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708z"/></svg>');
--toolbar-icon-toggle: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="m22.81,9.08c.807-.642,1.25-1.642,1.183-2.676-.067-1.05-.646-2.001-1.547-2.546-.772-.466-1.946-.892-3.574-1.297-.411-1.476-1.767-2.562-3.372-2.562-1.493,0-2.77.94-3.272,2.259C3.12,6.628.56,12.888.025,17.398c-.195,1.646.332,3.311,1.448,4.567,1.149,1.293,2.799,2.035,4.527,2.035h9c.552,0,1-.448,1-1s-.448-1-1-1h-2v-3c0-1.428.193-3.121.398-4.513l5.464,8.587c.369.579,1,.925,1.687.925h1.451c.552,0,1-.448,1-1s-.448-1-1-1h-1.451s-6.342-9.966-6.342-9.966c.116-.058.245-.095.383-.108,3.276-.294,6.349-1.358,8.22-2.846Zm-7.31-7.08c.827,0,1.5.673,1.5,1.5s-.673,1.5-1.5,1.5-1.5-.673-1.5-1.5.673-1.5,1.5-1.5Zm-3.783,10.367c-.143.797-.297,1.747-.426,2.745-1.189-1.297-2.897-2.112-4.791-2.112h-.5c-.552,0-1,.448-1,1s.448,1,1,1h.5c2.481,0,4.5,2.019,4.5,4.5v2.5h-5c-1.157,0-2.263-.497-3.032-1.363-.747-.841-1.087-1.908-.957-3.003.647-5.452,4.145-9.959,10.144-13.101.442,1.427,1.774,2.467,3.344,2.467,1.541,0,2.853-1.001,3.319-2.387,1.191.315,2.061.636,2.594.958.346.209.56.56.585.962.025.386-.133.744-.433.982-1.857,1.477-4.913,2.218-7.154,2.419-1.351.122-2.459,1.123-2.694,2.433Z" /></svg>');
}
#froghat[contenteditable] {
outline: 0px solid transparent;
}
#froghat {
box-sizing: border-box;
height: -webkit-fill-available;
}
#froghat.edit {
font-family: monospace;
white-space: break-spaces;
}
#froghat.wysiwyg {
span {
display: inline;
border-bottom: 1px solid green;
}
}
main.editing {
border: 1px solid green;
border-top: 1px solid transparent;
border-radius: 0px;
}
#toolbar {
background: var(--toolbar-background);
border-radius: var(--toolbar-border-radius);
height: var(--toolbar-height);
border: 1px solid transparent;
margin: 0;
margin-bottom: 0px;
padding: var(--toolbar-spacing);
ul {
display: flex;
margin: 0px;
padding: 0px;
border-radius: 5px;
li {
padding: 0px;
margin: 2px;
text-align: center;
line-height: var(--toolbar-button-size);
width: var(--toolbar-button-size);
height: var(--toolbar-button-size);
button {
opacity: 0.3;
display: block;
background-repeat: no-repeat;
background-attachment: local;
background-position: center;
background-size: 1.5rem 1.5rem;
border-radius: 5px;
border: 1px solid transparent;
width: var(--toolbar-button-size);
height: var(--toolbar-button-size);
}
button.enabled {
opacity: 1.0;
cursor: pointer;
}
button.on {
border: var(--toolbar-button-enabled-border);
background-color: var(--toolbar-button-enabled-background);
}
button:hover {
border: var(--toolbar-button-enabled-border);
background-color: var(--toolbar-button-active-background);
}
.dropdown-menu {
position-area: bottom;
margin: 0;
border: 0;
width: var(--toolbar-button-size);
min-height: var(--toolbar-button-size);
}
#bold { background-image: var(--toolbar-icon-bold); }
#italic { background-image: var(--toolbar-icon-italic); }
#underline { background-image: var(--toolbar-icon-underline); }
#header { background-image: var(--toolbar-icon-h1); anchor-name: "header"; }
#h1 { background-image: var(--toolbar-icon-h1); }
#h2 { background-image: var(--toolbar-icon-h2); }
#h3 { background-image: var(--toolbar-icon-h3); }
#h4 { background-image: var(--toolbar-icon-h4); }
#h5 { background-image: var(--toolbar-icon-h5); }
#h6 { background-image: var(--toolbar-icon-h6); }
#list { background-image: var(--toolbar-icon-unordered_list); anchor-name: "list"; }
#unordered_list { background-image: var(--toolbar-icon-unordered_list); }
#ordered_list { background-image: var(--toolbar-icon-ordered_list); }
#line { background-image: var(--toolbar-icon-line); }
#quote { background-image: var(--toolbar-icon-quote); }
#link { background-image: var(--toolbar-icon-link); }
#table { background-image: var(--toolbar-icon-table); }
#macro { background-image: var(--toolbar-icon-macro); anchor-name: "macro"}
#macro_user { background-image: var(--toolbar-icon-macro_user); }
#macro_toc { background-image: var(--toolbar-icon-macro_toc); }
#macro_style { background-image: var(--toolbar-icon-macro_style); }
#markdown { background-image: var(--toolbar-icon-markdown); }
#save { background-image: var(--toolbar-icon-save); }
#toggle {
background-color: var(--toolbar-button-enabled-background);
background-image: var(--toolbar-icon-toggle);
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
filter: FlipH;
-ms-filter: "FlipH";
opacity: 1.0;
cursor: pointer;
}
#toggle:hover {
border: var(--toolbar-button-enabled-border);
background-color: var(--toolbar-button-active-background);
}
#header-menu { position-anchor: "header"; }
#list-menu { position-anchor: "list"; }
}
li:last-child {
display: block;
margin-left: auto;
}
}
}
#toolbar.enabled {
background: var(--toolbar-enabled-background);
border-radius: 0px;
border: 1px solid green;
border-bottom: 1px solid transparent;
}
@@ -0,0 +1,413 @@
function reEscape(string) {
return string.replaceAll('*', '\\*');
}
class ToolbarButton {
constructor(settings) {
this.id = settings.id;
this.element = settings.element || document.getElementById(this.id);
this.toolbar = settings.toolbar;
this.isMenu = settings.isMenu || false;
this.open = settings.open || '';
this.close = settings.close || '';
this.tag = settings.tag || null;
this.onclick = settings.onclick;
if (!this.isMenu) {
if (this.open) {
var open = reEscape(this.open);
var close = this.close ? reEscape(this.close) : '';
var leading = '(?<leading>^(?:(?!' + open + ').)*)';
var middle = '(?<middle>(?:(?!' + open;
if (this.close && this.close != this.open) {
middle += '|' + close + ').)+)';
} else {
middle += ').)*)';
}
this.pattern = RegExp(
leading +
'(?<open>' + open + ')' +
middle +
(close ? '(?<closed>(?:' + close + ')(\\s.*?|$))' : '') +
'$'
);
console.log(this.id, this.pattern);
}
this.element.addEventListener('click', (e) => {
if (this.element.enabled) {
if (this.onclick) {
this.onclick({clickEvent: e, button: this });
} else if (this.toolbar.editor.isWysiwyg()) {
this.#applyHTMLFormatting();
} else if (this.toolbar.editor.isMarkdown()) {
this.#applyMarkdownFormatting();
}
}
document.querySelectorAll(".dropdown-menu:popover-open").forEach(el => {
el.hidePopover();
});
this.toolbar.editor.element.focus();
});
}
if (settings.enabled) {
this.enable();
}
}
#applyHTMLFormatting() {
if (!this.tag) {
return;
}
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var node = document.createElement(this.tag);
try {
range.surroundContents(node);
range.setStartAfter(node);
} catch(e) {
console.log(e);
}
selection.removeAllRanges();
selection.addRange(range);
this.toolbar.editor.moveCursorAfter(node);
}
#applyMarkdownFormatting() {
if (!this.open) {
return;
}
var selection = window.getSelection();
var range = selection.getRangeAt(0);
var node = document.createTextNode(this.open + range.toString() + this.close);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
this.toolbar.editor.moveCursorAfter(node);
}
click() {
if (this.element.enabled) {
this.element.click();
}
}
enable() {
this.element.enabled = true;
this.element.classList.add('enabled');
}
disable() {
this.element.enabled = false;
this.element.classList.remove('enabled');
}
on() {
this.element.classList.add('on');
}
off() {
this.element.classList.remove('on');
}
toggle() {
this.element.classList.toggle('on');
}
}
class FroghatToolbar {
constructor(settings) {
this.editor = settings.editor;
this.element = settings.toolbar || document.getElementById('toolbar');
this.currentContext = null;
this.buttons = {
'line': new ToolbarButton({ toolbar: this, id: 'line', open: '***', close: '', tag: 'HR'}),
'bold': new ToolbarButton({ toolbar: this, id: 'bold', open: '**', close: '**', tag: 'STRONG'}),
'italic': new ToolbarButton({ toolbar: this, id: 'italic', open: '*', close: '*', tag: 'EM'}),
'header': new ToolbarButton({ toolbar: this, id: 'header', isMenu: true}),
'h1': new ToolbarButton({ toolbar: this, id: 'h1', markdown: '# ', close: "", tag: 'H1' }),
'h2': new ToolbarButton({ toolbar: this, id: 'h2', markdown: '## ', close: "", tag: 'H2' }),
'h3': new ToolbarButton({ toolbar: this, id: 'h3', markdown: '### ', close: "", tag: 'H3' }),
'h4': new ToolbarButton({ toolbar: this, id: 'h4', markdown: '#### ', close: "", tag: 'H4' }),
'h5': new ToolbarButton({ toolbar: this, id: 'h5', markdown: '##### ', close: "", tag: 'H5' }),
'h6': new ToolbarButton({ toolbar: this, id: 'h6', markdown: '###### ', close: "", tag: 'H6' }),
'list': new ToolbarButton({ toolbar: this, id: 'list', isMenu: true}),
'unordered_list': new ToolbarButton({ toolbar: this, id: 'unordered_list', }),
'ordered_list': new ToolbarButton({ toolbar: this, id: 'ordered_list', }),
'link': new ToolbarButton({ toolbar: this, id: 'link', }),
'quote': new ToolbarButton({ toolbar: this, id: 'quote', }),
'table': new ToolbarButton({ toolbar: this, id: 'table', }),
'macro': new ToolbarButton({ toolbar: this, id: 'macro', isMenu: true}),
'macro_user': new ToolbarButton({ toolbar: this, id: 'macro_user', }),
'macro_toc': new ToolbarButton({ toolbar: this, id: 'macro_toc', }),
'macro_style': new ToolbarButton({ toolbar: this, id: 'macro_style', }),
'save': new ToolbarButton({ toolbar: this, id: 'save', onclick: this.#click_save }),
'markdown': new ToolbarButton({ toolbar: this, id: 'markdown', onclick: this.#click_markdown }),
'toggle': new ToolbarButton({ toolbar: this, id: 'toggle', onclick: this.#click_toggle, enabled: true })
}
}
getContext() {
var context = null;
var node = window.getSelection().baseNode;
console.log({node});
Object.values(this.buttons).forEach(button => {
if (button.pattern) {
var closed = false;
var leading = "";
var matched = false;
matched = node.textContent.match(button.pattern);
if (matched && matched.groups) {
closed = matched.groups.closed;
leading = matched.groups.leading;
}
if (closed && matched.groups.middle) {
context = {
button: button,
closed: closed,
leading: leading,
node: node,
element: null
}
}
if (node.parentElement && node.parentElement.nodeName == button.tag) {
if (!context) {
context = {
button: button,
element: null
};
}
context.element = node.parentElement
}
if (context) {
return;
}
}
});
return context;
}
#click_save({clickEvent, button}) {
}
#click_markdown({clickEvent, button}) {
button.toolbar.editor.toggleMarkdown();
}
#click_toggle({clickEvent, button}) {
button.toolbar.editor.toggleView();
}
enable() {
this.element.classList.add("enabled");
Object.values(this.buttons).forEach(button => { button.enable() });
}
disable() {
this.element.classList.remove("enabled");
Object.values(this.buttons).forEach(button => { button.disable() });
this.buttons.toggle.enable();
}
}
class FroghatEditor extends Froghat {
run() {
this.states = {
VIEW: 'view',
EDIT: 'edit',
WYSIWYG: 'wysiwyg'
}
this.toolbar = new FroghatToolbar({editor: this});
this.#bindEvents();
this.plugins().forEach(plugin => { plugin.setEditable() });
this.element.classList.add("loaded");
this.view();
}
#replaceWysiwygNode(context) {
var offset = context.leading.length || 0;
var slice = context.node.textContent.slice(offset);
var html = this.markdownToHTML(slice);
html = html.replace("<p>", "").replace("</p>", "");
var el = document.createElement("span");
el.innerHTML = context.leading + html;
console.log(el.innerHTML);
if (el.innerHTML != context.node.innerHTML) {
context.node.replaceWith(el);
}
return el;
}
#refreshMarkdownCache() {
if (this.cachedMarkdown != this.element.textContent) {
this.changed = true;
this.toolbar.buttons.save.enable();
this.cachedMarkdown = this.element.textContent;
}
}
#handleEditorChanges(evt) {
var context = this.toolbar.getContext();
if (!context) {
if (this.toolbar.currentContext) {
this.toolbar.currentContext.button.off();
this.toolbar.currentContext = null;
}
return;
}
if (this.toolbar.currentContext) {
this.toolbar.currentContext.button.off();
}
context.button.on();
this.toolbar.currentContext = context;
if (this.isWysiwyg()) {
if (context.closed) {
context.node = this.#replaceWysiwygNode(context);
context.button.off();
this.toolbar.currentContext = null;
this.toolbar.editor.moveCursorAfter(context.node);
}
} else if (context.button) {
context.button.on();
}
}
#bindEvents() {
this.element.addEventListener('keydown', (evt) => {
if (! this.isEditing()) {
return;
}
this.#refreshMarkdownCache();
this.#handleEditorChanges(evt);
});
};
toggleMarkdown() {
if (this.getState() === this.states.EDIT) {
this.wysiwyg();
} else {
this.edit();
}
}
toggleView() {
if (this.getState() === this.states.VIEW) {
this.toolbar.enable();
this.wysiwyg();
this.element.focus();
} else {
this.toolbar.disable();
this.view();
}
}
htmlToMarkdown(html) {
return hopdown.toMarkdown(html || this.element.innerHTML);
}
getMarkdown() {
/*
* Return the current markdown.
*/
if (this.getState() === this.states.EDIT) {
var html = this.element.innerHTML;
html = html.replaceAll(/<(?:div|br)>/ig, '');
html = html.replaceAll(/<\/div>/ig, "\n");
this.cachedMarkdown = decodeHtmlEntities(html);
} else if (this.getState() === this.states.WYSIWYG) {
this.cachedMarkdown = this.htmlToMarkdown(this.element.innerHTML);
} if (!this.cachedMarkdown) {
this.cachedMarkdown = this.element.textContent;
}
return this.cachedMarkdown;
}
wysiwyg() {
/*
* Put the editor in WYSIWYG editing mode.
*/
if (this.getState() === this.states.WYSIWYG) {
return;
}
this.element.contentEditable = true;
this.element.innerHTML = this.getHTML();
Array.from(this.element.querySelectorAll('.macro')).forEach(el => {
if (el.dataset.editable == "false") {
el.contentEditable = false;
el.style.opacity = 0.5;
}
});
this.setState(this.states.WYSIWYG);
this.toolbar.buttons.markdown.off();
this.toolbar.buttons.markdown.enable();
document.getElementById("main").classList.add("editing");
}
edit() {
/*
* Put the editor into source editing mode.
*/
if (this.state === this.states.EDIT) {
return;
}
this.element.contentEditable = true;
this.element.innerHTML = encodeHtmlEntities(this.getMarkdown());
this.setState(this.states.EDIT);
this.toolbar.buttons.markdown.on();
document.getElementById("main").classList.add("editing");
}
moveCursorAfter(node) {
var dummyElement = null;
if (!node.nextElementSibling) {
// workaround for https://issues.chromium.org/issues/41239578
var dummyElement = document.createElement('a');
dummyElement.innerHTML="&#x200B;";
dummyElement.className = 'bugfix';
node.parentNode.appendChild(dummyElement);
}
var nextElement = node.nextElementSibling;
nextElement.tabIndex=0;
nextElement.focus();
var range = document.createRange();
range.setStart(nextElement.childNodes[0], 1);
range.setEnd(nextElement.childNodes[0], 1);
var selection = window.getSelection();
console.log(range);
console.log(selection);
selection.removeAllRanges();
selection.addRange(range);
}
isWysiwyg() {
return this.state === this.states.WYSIWYG;
}
isMarkdown() {
return this.state === this.states.EDIT;
}
isEditing() {
return this.isWysiwyg() || this.isMarkdown();
}
}
@@ -0,0 +1,366 @@
@import "theme.css";
html {
padding: 0px;
margin: 0px;
height: 100vh;
box-sizing: border-box;
}
body {
font-family: var(--default-font-family);
font-size: var(--default-font-size);
background: var(--body-background);
height: auto;
width: 100%;
margin: 0px;
box-sizing: border-box;
overflow-wrap: break-word;
overflow-y: auto;
scrollbar-gutter: stable;
}
h1 {
font-size: var(--h1-size);
font-weight: var(--header-weight);
font-family: var(--header-font);
text-decoration: inherit;
font-variant: inherit;
border: none;
line-height: initial;
padding: initial;
}
h2 {
font-size: var(--h2-size);
font-weight: var(--header-weight);
font-family: var(--header-font);
text-decoration: inherit;
font-variant: inherit;
border: none;
margin: initial;
line-height: initial;
padding: initial;
}
h3 {
font-size: var(--h3-size);
font-weight: var(--header-weight);
font-family: var(--header-font);
text-decoration: inherit;
font-variant: inherit;
border: none;
margin: initial;
line-height: initial;
padding: initial;
}
h4 {
font-size: var(--h4-size);
font-weight: var(--header-weight);
font-family: var(--header-font);
text-decoration: inherit;
font-variant: inherit;
border: none;
margin: initial;
line-height: initial;
padding: initial;
}
h5 {
font-size: var(--h5-size);
font-weight: var(--header-weight);
font-family: var(--header-font);
text-decoration: inherit;
font-variant: inherit;
border: none;
margin: initial;
line-height: initial;
padding: initial;
}
h6 {
font-size: var(--h6-size);
font-weight: var(--header-weight);
font-family: var(--header-font);
text-decoration: inherit;
font-variant: inherit;
border: none;
margin: initial;
line-height: initial;
padding: initial;
}
* > h1:first-child {
margin-top: 0px;
}
a {text-decoration: none;}
q, blockquote {
margin-left: 30px;
font-size: 1.3em;
font-style: italic;
color: #555;
}
table {
width: 100%;
}
th {
border-bottom: 1px solid #000;
padding: 3px;
}
th, td {
padding: 2px;
}
nav ul.container {
height: 100%;
align-content: center;
margin: auto;
}
#masthead {
background: var(--body-background);
padding-bottom: var(--masthead-spacing);
display: block;
height: fit-content;
position:sticky;
top: 0px;
z-index: 100;
}
nav {
display: block;
margin: 0px;
padding: 0px;
height: var(--nav-height);
}
nav > ul > li {
display: inline-block;
padding: 0 var(--nav-spacing);
position: relative;
}
nav > ul > li:first-child {
padding-left: 0px;
}
nav > ul > li > a {
display: inline-block;
}
#pagenav > ul > li > a {
color: var(--nav-color);
}
#pagenav > ul > li:hover > a {
color: var(--nav-hover-color);
}
#pagenav {
background: var(--blue);
}
.dropdown {position: static;}
#breadcrumbs {
display: block;
align-content: center;
height: var(--breadcrumbs-height);
width: 100%;
}
#data_form {
display: none;
}
.main-aligned {
display: block;
margin: auto;
max-width: var(--max-width);
min-width: var(--min-width);
}
.content-aligned {
display: block;
margin: auto;
max-width: calc(var(--max-width) - (2 * var(--content-padding)));
min-width: var(--min-width);
padding-left: var(--content-padding);
padding-right: var(--content-padding);
}
main {
position: relative;
display: block;
width: calc(100% - (2 * var(--content-padding)) - 2px);
min-height: var(--main-height);
height: fit-content;
padding: var(--content-padding);
background: var(--content-background);
border-radius: var(--content-border-radius);
}
footer {
display: block;
position: relative;
bottom: 0;
margin-top: var(--footer-spacing);
height: var(--footer-height);
background-color: var(--footer-background-color);
}
menu {
display: none;
}
.left {
display: block;
float: left;
margin:5px;
max-width: 30%;
}
.right {
display: block;
float: right;
margin: 5px;
max-width: 30%;
}
.center {
width: fit-content;
margin: 0 auto;
}
.box {
border-radius: 5px;
border: 1px solid black;
background: #DEDEDE;
padding: 10px;
}
.striped > table {
background: #FAFAFA;
}
.striped > table tr:nth-child(even) {
background-color: #FFFFFF;
}
.equal-widths > table {
table-layout: fixed;
width: 100%;
}
.layout > table {
border: none;
background: transparent;
}
.layout > table > thead {
display: none;
}
table td table {
max-width:95%;
}
pre {
border: 1px dashed black;
border-radius: 5px;
padding: 10px;
margin: 5px;
background: #EEE;
}
code {
display: inline-block;
border: 1px dashed black;
border-radius: 5px;
padding: 5px;
background: #EEE;
margin: 3px;
}
div.macro {
display: inline;
}
[data-macro-name="toc"] {
display: inline;
float: left;
border-radius: 5px;
font-size: 14px;
margin-right: 2em;
margin-bottom: 2em;
border: 1px solid #000;
padding: 0ch 2ch;
}
[data-macro-name="toc"] ul {
box-sizing: border-box;
list-style: none;
padding-left: 0ch;
font-weight: normal;
margin-left: 0px;
}
[data-macro-name="toc"] ul ul {
padding-left: 2ch;
}
[data-macro-name="toc"] > ul:first-child {
padding-left: 0ch;
margin-left: 0px;
}
[data-macro-name="toc"] > ul:first-child > li {
padding-left: 0ch;
margin-left: 0px;
}
[data-macro-name="toc"] > ul:first-child > li {
font-weight: bold;
}
[data-macro-name="toc"] > ul > li {
font-weight: bold;
}
[data-macro-name="toc"] > ul > li {
padding-left: 0ch;
}
[data-macro-name="toc"] a {
display: block;
width: 100%;
}
[data-macro-name="toc"] a:hover {
background: #CCC;
}
[data-macro-name="toc"] .header {
font-size: var(--default-font-size);
width: fit-content;
margin: 2ch auto 0 auto;
}
#froghat {
display: none;
}
#froghat.loaded {
display: block;
}
#froghat.wysiwyg {
display: none,
}
#froghat.view {
}
+412
View File
@@ -0,0 +1,412 @@
FroghatAPIv1 = {
get: function(doc_id, callback) {
(async () => {
const raw = await fetch('/_/v1/get/' + doc_id, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
const res = await raw.json();
if (res['code'] != 200) {
console.error("APIv1 error: ", res)
}
callback(res);
})();
},
put: function(data, callback) {
(async () => {
const raw = await fetch('/_/v1/put/' + window.location.pathname, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'body': data
}),
});
const res = await raw.json();
if (res['code'] != 200) {
console.error("APIv1 error: ", res)
}
callback(res);
})();
},
search: function(space, query, callback) {
(async () => {
const raw = await fetch('/_/v1/search/' + space, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'body': query
}),
});
const res = await raw.json();
if (res['code'] != 200) {
console.error("APIv1 error: ", res)
}
callback(res);
})();
},
};
/*
* Froghat and FroghatPlugin are provided by the ribbit library
* as Ribbit and RibbitPlugin. Alias them for backward compatibility.
*/
class Froghat extends Ribbit {
constructor(settings) {
super({ ...settings, api: settings.api || FroghatAPIv1, editorId: settings.editorId || 'froghat' });
}
view() {
super.view();
const main = document.getElementById("main");
if (main) main.classList.remove("editing");
}
}
class FroghatPlugin extends RibbitPlugin {};
WIDGETS = {};
function loadWidget(name, callback) {
if (Object.keys(WIDGETS).indexOf(name) == -1) {
(async () => {
await FroghatAPIv1.search("Widget", name, (res) => {
if (res.code == 200) {
function block(prefix) {
return RegExp('##\\s*' + prefix + '.*?```\\w*(.+?)```', 'gims');
};
var html = res.response[0].body;
var proc = block("Processor").exec(html)[1].trim();
if (!proc) {
proc = function(token, widget) {
var name = token.keywords.split(" ").slice(1).join(" ");
var ret = '';
eval("ret = `" + widget.template + "`");
return ret;
}
} else {
eval(`proc = ${proc}`);
}
WIDGETS[name] = {
template: block("Template").exec(html)[1],
css: block("CSS").exec(html)[1],
processor: proc
};
} else {
WIDGETS[name] = {
template: "",
css: "",
processor: function() { return `Invalid Widget: "${name}"` },
};
}
if (callback) {
callback(WIDGETS[name]);
}
});
})();
} else {
return WIDGETS[name];
}
}
class MacroPlugin extends FroghatPlugin {
macros = {
// image: {}
widget: {
inline: true,
editable: false,
toHTML: (token, node) => {
var widgetName = token.keywords.split(" ")[0];
var contents = '';
var cached = loadWidget(widgetName, (widget) => {
contents = widget.processor(token, widget);
var targets = wiki.element.querySelectorAll(`[data-macro-name="widget"][data-keywords="${token.keywords}"]`);
targets.forEach(widgetElement => {
widgetElement.style = widget.css;
widgetElement.innerHTML = contents;
});
wiki.cachedHTML = wiki.element.innerHTML;
});
var ret = node + (cached ? cached.processor(token, cached) : "");
return ret;
},
postprocess: (html) => {
const buf = document.createElement('div');
buf.innerHTML = html;
var targets = buf.querySelectorAll(`[data-macro-name="widget"]`);
targets.forEach(widgetElement => {
var widget = WIDGETS[widgetElement.dataset.keywords.split(" ")[0]];
if (widget) {
widgetElement.style = widget.css;
}
});
return buf.innerHTML;
},
},
style: {
inline: false,
editable: true,
toHTML: (token, node) => {
return node.replace(/class="macro"/, `class="macro ${token.keywords}"`);
}
},
multiline: {
inline: false,
editable: true,
fromHTML: (node, markdown) => {
var content = node.innerHTML.replaceAll("<br>", "\0");
content = content.replaceAll("{{{", "&#123;&#123;&#123;");
content = content.replaceAll("}}}", "&#125;&#125;&#125;");
content = encodeHtmlEntities(content);
return `{{{${content}}}}`;
},
},
user: {
inline: true,
toHTML: (token, node) => {
return node + document.querySelector("nav > ul > li.user > a:first-child").outerHTML;
},
},
toc: {
inline: true,
element: 'aside',
postprocess: (html) => {
const subList = (depth) => {
var li = document.createElement("li");
var ul = document.createElement("ul");
li.appendChild(ul);
return li;
};
var tocIndex = 0;
const buf = document.createElement('div');
buf.innerHTML = html;
buf.querySelectorAll('[data-macro-name="toc"]').forEach(tocElement => {
var params = {
depth: tocElement.dataset.paramDepth || 3,
keywords: tocElement.dataset.paramKeywords || "",
};
const headings = buf.querySelectorAll("h2, h3, h4, h5, h6");
var header = document.createElement("h2");
header.className = 'header';
header.textContent = 'Table of Contents';
tocElement.prepend(header);
const toc = document.createElement("ul");
toc.setAttribute('role', 'list');
var lastDepth = null;
var ul = toc;
headings.forEach(heading => {
var depth = parseInt(heading.nodeName[1]) - 1;
if (depth > params.depth) {
return;
}
if (lastDepth === null) {
lastDepth = depth;
}
var index = document.createElement("li");
var ref = camelCase(heading.textContent).join("");
index.innerHTML = `<a href="#${heading.id}">${heading.textContent}</a>`;
var list = null;
if (depth > lastDepth) {
list = subList(depth);
ul.appendChild(list);
ul = list.firstChild;
} else if (depth < lastDepth) {
var list = subList(depth);
toc.appendChild(list);
ul = list.firstChild;
}
ul.appendChild(index);
lastDepth = depth;
});
if (params.keywords) {
params.keywords.split(" ").forEach(className => {
if (className) {
toc.classList.add(className);
}
});
}
tocElement.appendChild(toc);
});
return buf.innerHTML;
},
},
npc: {
toHTML: (settings) => {
var name = camelCase(settings.keywords).join(" ");
var target = name.replaceAll(" ", "");
return `<a href="/NPC/${target}" data-npc-name="${name}">👤 ${name}</a>`;
},
fromHTML: (node) => {
return `{{npc ${node.firstChild.dataset.npcName}}}`;
},
},
spell: {
toHTML: (settings) => {
var name = camelCase(settings.keywords).join(" ");
var target = name.replaceAll(" ", "");
return `<a href="/Spell/${target}" data-spell-name="${name}">✨ ${name}</a>`;
},
fromHTML: (node) => {
return `{{spell ${node.firstChild.dataset.spellName}}}`;
},
},
}
getTokens = (pattern, source) => {
const matched = source.matchAll(pattern);
const tokens = [];
if (!matched) {
return tokens;
}
matched.forEach(match => {
if (!this.macros[match.groups.name]) {
return;
}
const token = {
type: 'macro',
source: match[0],
matched: match,
macro: this.macros[match.groups.name],
keywords: (match.groups.keywords || '').trim(),
inline: this.macros[match.groups.name].inline,
editable: this.macros[match.groups.name].editable || false,
params: {},
rendered: '',
};
if (match.groups.parameters) {
var params = decodeHtmlEntities(match.groups.parameters.trim());
params.matchAll(this.paramPattern).forEach(param => {
if (param.groups) {
var name = param.groups.name;
token.params[name] = decodeHtmlEntities(param.groups.value.trim());
}
});
}
token.rendered = this.renderToken(token);
tokens.push(token);
});
return tokens;
}
renderToken = (token) => {
const tag = token.macro.element || (token.inline ? 'span' : 'div');
var node = `<${tag} class="macro" data-plugin-name="macro" data-macro-name="${token.matched.groups.name}"`;
for (var name in token.params) {
node += ` data-param-${name}="${(token.params[name] || "").trim()}"`;
}
if (token.keywords) {
node += ` data-keywords="${token.keywords}"`;
}
node += ` data-inline="${token.inline}"`;
node += ` data-editable="${token.editable}"`;
node += ">";
if (token.macro.toHTML) {
node = token.macro.toHTML(token, node);
}
if (token.inline) {
node += `</${tag}>`;
}
// preserve the wrapping element, unless it is a paragraph.
if (token.matched.groups.wrap) {
if (token.matched.groups.wrap != '<p>') {
node = token.matched.groups.wrap + node + token.matched.groups.endwrap;
}
}
return node;
}
constructor(settings) {
super(settings);
this.pattern = new RegExp(
'(?<!`)(?<wrap><[^>]+?>)?' + // capture the enclosing HTML tag, if any
'{{' + // start of the macro
'(?<name>\\w+)' + // the macro name
'(?<keywords>(?:\\s(?:\\s*(?:[\\w-](?![\\w-]+=))+))+)?' + // zero or more keywords separated by spaces
'(?<parameters>[^}<]+)?' + // anything else before the closing
'\\s*(?<closed>}})?' + // is the tag closed?
'(?<endwrap>(?:>!\\<)*?<\\/[^>]+?>)?', // capture the enclosing HTML tag, if any
'mg'
);
this.endPattern = /<p>}}\s*<\/p>/mg;
this.paramPattern = /\s*(?<name>[^=]+)="(?<value>[^"]*)"/g;
this.multilinePattern = /(?<!\{{3}[\s\n]*)\{{3}[\s\n]*(?<content>(.(?!\}{3})*)+?)[\s\n]*\}{3}/smg;
const plugin = this;
// TODO: Re-implement macro pre/post-processing as ribbit Tag hooks.
// The marked.use() hooks for multiline macros and the postprocess
// pipeline need to be converted to custom Tags.
}
setEditable() {
// TODO: Re-implement macro HTML→markdown conversion as a ribbit Tag.
// The turndown macro rule needs to be converted to Tag.toMarkdown().
}
}
function camelCase(words) {
var output = [];
words.trim().split(/\s+/g).forEach(word => {
var lcWord = word.toLowerCase();
output.push(lcWord.charAt(0).toUpperCase() + lcWord.slice(1));
});
return output;
}
function b64encode(input) {
return new TextEncoder().encode(input).toBase64();
}
function b64decode(input) {
return new TextDecoder().decode(Uint8Array.fromBase64(input));
}
function decodeHtmlEntities(html) {
var txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
function encodeHtmlEntities(str) {
return str.replace(/[\u00A0-\u9999<>\&]/g, i => '&#'+i.charCodeAt(0)+';')
}
@@ -0,0 +1,58 @@
/*
* ribbit.css — editor styles for the ribbit WYSIWYG markdown editor.
*
* Provides base content formatting and editor state styles.
* Override with your own theme CSS for custom look and feel.
*/
/* ── Content formatting ──────────────────────────────── */
a { text-decoration: none; }
q, blockquote {
margin-left: 30px;
font-size: 1.3em;
font-style: italic;
color: #555;
}
table { width: 100%; }
th { border-bottom: 1px solid #000; padding: 3px; }
th, td { padding: 2px; }
table td table { max-width: 95%; }
pre {
border: 1px dashed black;
border-radius: 5px;
padding: 10px;
margin: 5px;
background: #EEE;
}
code {
display: inline-block;
border: 1px dashed black;
border-radius: 5px;
padding: 5px;
background: #EEE;
margin: 3px;
}
/* ── Editor states ───────────────────────────────────── */
#ribbit {
display: none;
}
#ribbit.loaded {
display: block;
}
#ribbit.edit {
font-family: monospace;
white-space: pre;
}
#ribbit.wysiwyg .md {
opacity: 0.5;
}
+775
View File
@@ -0,0 +1,775 @@
"use strict";
(() => {
// src/tags.ts
function inlineTag(def) {
const escaped = def.delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const matchPattern = new RegExp("^" + escaped + "(.+?)" + escaped);
const globalPattern = new RegExp(escaped + "(.+?)" + escaped, "g");
const upperTag = def.htmlTag.toUpperCase();
const selector = [upperTag, ...(def.aliases || "").split(",").filter(Boolean)].join(",");
const recursive = def.recursive !== false;
return {
name: def.name,
precedence: def.precedence ?? 50,
recursive,
pattern: globalPattern,
delimiter: def.delimiter,
match: (context) => {
const matched = context.text.slice(context.offset).match(matchPattern);
if (!matched) {
return null;
}
return {
content: matched[1],
raw: matched[0],
consumed: matched[0].length
};
},
toHTML: (token, convert) => {
const inner = recursive ? convert.inline(token.content) : escapeHtml(token.content);
return `<${def.htmlTag}>${inner}</${def.htmlTag}>`;
},
selector,
toMarkdown: (element, convert) => {
if (!recursive && element.parentNode?.nodeName === "PRE") {
return convert.children(element);
}
return def.delimiter + convert.children(element) + def.delimiter;
}
};
}
function escapeHtml(source) {
return source.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function camelId(text) {
return text.trim().split(/\s+/).map(
(word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
).join("");
}
function parseListBlock(lines, start, indent, inlineConvert) {
const prefix = new RegExp("^" + " ".repeat(indent) + "([\\*\\-]|\\d+\\.)\\s");
const isOl = /^\d+\./.test(lines[start].trim());
const tag = isOl ? "ol" : "ul";
const items = [];
let i = start;
while (i < lines.length) {
const line = lines[i];
if (/^\s*$/.test(line)) break;
const lineIndent = line.match(/^(\s*)/)[1].length;
if (lineIndent < indent) break;
if (lineIndent > indent) {
const sub = parseListBlock(lines, i, lineIndent, inlineConvert);
items[items.length - 1].sub = sub.html;
i = sub.end;
continue;
}
if (!prefix.test(line)) break;
items.push({
text: line.replace(prefix, ""),
sub: ""
});
i++;
}
const html = "<" + tag + ">" + items.map(
(item) => "<li>" + inlineConvert(item.text) + item.sub + "</li>"
).join("") + "</" + tag + ">";
return { html, end: i };
}
function listToMd(node, depth, convert) {
const isOl = node.nodeName === "OL";
const indent = " ".repeat(depth);
const lines = [];
Array.from(node.children).forEach((listItem, index) => {
const marker = isOl ? index + 1 + ". " : "- ";
let text = "";
let sublist = "";
Array.from(listItem.childNodes).forEach((child) => {
if (child.nodeType === 1 && (child.nodeName === "UL" || child.nodeName === "OL")) {
sublist += listToMd(child, depth + 1, convert);
} else {
text += convert.node(child);
}
});
lines.push(indent + marker + text.trim());
if (sublist) lines.push(sublist);
});
const result = lines.join("\n");
return depth === 0 ? "\n\n" + result + "\n\n" : result;
}
function isBlockStart(lines, index) {
const line = lines[index];
if (/^(`{3,})/.test(line)) return true;
if (/^(\*{3,}|-{3,}|_{3,})\s*$/.test(line)) return true;
if (/^#{1,6}\s/.test(line)) return true;
if (/^>\s?/.test(line)) return true;
if (/^[*\-]\s/.test(line) || /^\d+\.\s/.test(line)) return true;
if (line.indexOf("|") !== -1 && index + 1 < lines.length && /^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(lines[index + 1])) return true;
return false;
}
function parseTableRow(line) {
return line.replace(/^\|/, "").replace(/\|$/, "").split("|").map((cell) => cell.trim());
}
function parseAligns(line) {
return parseTableRow(line).map((cell) => {
if (/^:-+:$/.test(cell)) return "center";
if (/^-+:$/.test(cell)) return "right";
return null;
});
}
var defaultBlockTags = {
"PRE": {
/*
* ```lang
* code here
* ```
*/
name: "fencedCode",
match: (context) => {
const matched = context.lines[context.index].match(/^(`{3,})(.*)/);
if (!matched) return null;
const fence = matched[1], lang = matched[2].trim();
const code = [];
let i = context.index + 1;
while (i < context.lines.length && !context.lines[i].startsWith(fence))
code.push(context.lines[i++]);
return {
content: code.join("\n"),
raw: "",
consumed: i + 1 - context.index,
meta: {
lang
}
};
},
toHTML: (token) => "<pre><code" + (token.meta?.lang ? ` class="language-${escapeHtml(token.meta.lang)}"` : "") + ">" + escapeHtml(token.content) + "</code></pre>",
selector: "PRE",
toMarkdown: (element) => {
const code = element.querySelector("code");
const lang = (code?.getAttribute("class") || "").match(/language-(\S+)/)?.[1] || "";
return "\n\n```" + lang + "\n" + (code?.textContent || element.textContent || "") + "\n```\n\n";
}
},
"HR": {
/*
* ***
* ---
* ___
*/
name: "hr",
match: (context) => {
if (!/^(\*{3,}|-{3,}|_{3,})\s*$/.test(context.lines[context.index])) {
return null;
}
return {
content: "",
raw: "",
consumed: 1
};
},
toHTML: () => "<hr>",
selector: "HR",
toMarkdown: () => "\n\n---\n\n"
},
"H1,H2,H3,H4,H5,H6": {
/*
* # Heading 1
* ## Heading 2
* ### Heading 3
*/
name: "heading",
match: (context) => {
const matched = context.lines[context.index].match(/^(#{1,6})\s+(.*)/);
if (!matched) return null;
return {
content: matched[2].trim(),
raw: "",
consumed: 1,
meta: {
level: String(matched[1].length)
}
};
},
toHTML: (token, convert) => `<h${token.meta.level} id='${camelId(token.content)}'>${convert.inline(token.content)}</h${token.meta.level}>`,
selector: "H1,H2,H3,H4,H5,H6",
toMarkdown: (element, convert) => "\n\n" + "#".repeat(parseInt(element.nodeName[1])) + " " + convert.children(element) + "\n\n"
},
"BLOCKQUOTE": {
/*
* > quoted text
* > more quoted text
*/
name: "blockquote",
match: (context) => {
if (!/^>\s?/.test(context.lines[context.index])) return null;
const lines = [];
let i = context.index;
while (i < context.lines.length && /^>\s?/.test(context.lines[i]))
lines.push(context.lines[i++].replace(/^>\s?/, ""));
return {
content: lines.join("\n"),
raw: "",
consumed: i - context.index
};
},
toHTML: (token, convert) => "<blockquote>" + convert.block(token.content) + "</blockquote>",
selector: "BLOCKQUOTE",
toMarkdown: (element, convert) => "\n\n" + convert.children(element).trim().split("\n").map((line) => "> " + line).join("\n") + "\n\n"
},
"UL,OL": {
/*
* - unordered item
* - unordered item
* - nested item
*
* 1. ordered item
* 2. ordered item
*/
name: "list",
match: (context) => {
const line = context.lines[context.index];
if (!/^[*\-]\s/.test(line) && !/^\d+\.\s/.test(line)) return null;
return {
content: "",
raw: "",
consumed: 0
};
},
toHTML: (token) => token.raw,
selector: "UL,OL",
toMarkdown: (element, convert) => listToMd(element, 0, convert)
},
"TABLE": {
/*
* | head 1 | head 2 |
* |--------|--------|
* | cell 1 | cell 2 |
*/
name: "table",
match: (context) => {
const { lines, index } = context;
if (lines[index].indexOf("|") === -1 || index + 1 >= lines.length) return null;
if (!/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(lines[index + 1])) return null;
const headers = parseTableRow(lines[index]);
const aligns = parseAligns(lines[index + 1]);
const rows = [];
let i = index + 2;
while (i < lines.length && lines[i].indexOf("|") !== -1 && !/^\s*$/.test(lines[i]))
rows.push(parseTableRow(lines[i++]));
return {
content: "",
raw: "",
consumed: i - index,
meta: {
headers: JSON.stringify(headers),
aligns: JSON.stringify(aligns),
rows: JSON.stringify(rows)
}
};
},
toHTML: (token, convert) => {
const headers = JSON.parse(token.meta.headers);
const aligns = JSON.parse(token.meta.aligns);
const rows = JSON.parse(token.meta.rows);
function cell(tag, text, index) {
const align = aligns[index] ? ` style="text-align:${aligns[index]}"` : "";
return `<${tag}${align}>${convert.inline(text)}</${tag}>`;
}
const head = "<thead><tr>" + headers.map((text, i) => cell("th", text, i)).join("") + "</tr></thead>";
const body = rows.map(
(row) => "<tr>" + row.map((text, i) => cell("td", text, i)).join("") + "</tr>"
).join("");
return "<table>" + head + "<tbody>" + body + "</tbody></table>";
},
selector: "TABLE",
toMarkdown: (element, convert) => {
const rows = Array.from(element.querySelectorAll("tr"));
if (!rows.length) return "";
const headers = Array.from(rows[0].querySelectorAll("th,td")).map((cell) => convert.children(cell).trim());
const separator = headers.map(() => "---");
const output = [
"| " + headers.join(" | ") + " |",
"| " + separator.join(" | ") + " |"
];
rows.slice(1).forEach((row) => {
const cells = Array.from(row.querySelectorAll("td,th")).map((cell) => convert.children(cell).trim());
output.push("| " + cells.join(" | ") + " |");
});
return "\n\n" + output.join("\n") + "\n\n";
}
},
"P": {
/*
* Any text that doesn't match another block tag
* becomes a paragraph.
*/
name: "paragraph",
match: (context) => {
const collected = [];
let i = context.index;
while (i < context.lines.length && !/^\s*$/.test(context.lines[i]) && !isBlockStart(context.lines, i))
collected.push(context.lines[i++]);
return collected.length ? {
content: collected.join("\n"),
raw: "",
consumed: i - context.index
} : null;
},
toHTML: (token, convert) => "<p>" + convert.inline(token.content) + "</p>",
selector: "P",
toMarkdown: (element, convert) => "\n\n" + convert.children(element) + "\n\n"
}
};
var defaultInlineTags = {
"CODE": inlineTag({
/*
* `inline code`
*/
name: "code",
delimiter: "`",
htmlTag: "code",
precedence: 10,
recursive: false
}),
"A": {
/*
* [link text](http://example.com)
*/
name: "link",
match: (context) => {
const matched = context.text.slice(context.offset).match(/^\[([^\]]+)\]\(([^)]+)\)/);
if (!matched) {
return null;
}
return {
content: matched[1],
raw: matched[0],
consumed: matched[0].length,
meta: {
href: matched[2]
}
};
},
toHTML: (token, convert) => '<a href="' + escapeHtml(token.meta.href) + '">' + convert.inline(token.content) + "</a>",
selector: "A",
toMarkdown: (element, convert) => "[" + convert.children(element) + "](" + (element.getAttribute("href") || "") + ")"
},
"_boldItalic": {
/*
* ***bold and italic***
*/
...inlineTag({
name: "boldItalic",
delimiter: "***",
htmlTag: "em",
precedence: 30
}),
toHTML: (token, convert) => "<em><strong>" + convert.inline(token.content) + "</strong></em>",
selector: ((element) => false),
toMarkdown: () => ""
},
"STRONG,B": inlineTag({
/*
* **bold text**
*/
name: "bold",
delimiter: "**",
htmlTag: "strong",
aliases: "B",
precedence: 40
}),
"EM,I": inlineTag({
/*
* *italic text*
*/
name: "italic",
delimiter: "*",
htmlTag: "em",
aliases: "I",
precedence: 50
})
};
var defaultTags = {
...defaultBlockTags,
...defaultInlineTags
};
// src/hopdown.ts
var HopDown = class {
constructor(options = {}) {
let tagMap;
if (options.tags) {
tagMap = options.tags;
} else if (options.exclude) {
const excluded = new Set(options.exclude);
tagMap = Object.fromEntries(
Object.entries(defaultTags).filter(([, tag]) => !excluded.has(tag.name))
);
} else {
tagMap = defaultTags;
}
const allTags = Object.values(tagMap);
const defaultBlockNames = new Set(Object.values(defaultBlockTags).map((t) => t.name));
const defaultInlineNames = new Set(Object.values(defaultInlineTags).map((t) => t.name));
this.blockTags = allTags.filter(
(tag) => defaultBlockNames.has(tag.name) || !defaultInlineNames.has(tag.name) && !tag.pattern
);
this.inlineTags = allTags.filter(
(tag) => defaultInlineNames.has(tag.name) || tag.pattern
);
this.tags = /* @__PURE__ */ new Map();
for (const [selector, tag] of Object.entries(tagMap)) {
for (const sel of selector.split(",").map((s) => s.trim()).filter(Boolean)) {
if (sel.startsWith("_")) {
continue;
}
const existing = this.tags.get(sel);
if (existing && existing !== tag) {
throw new Error(
`HTML tag "${sel}" is claimed by both "${existing.name}" and "${tag.name}". Use the exclude option to remove one before adding the other.`
);
}
this.tags.set(sel, tag);
}
}
this.validateInlineTags();
}
/**
* Verify that no two inline tags have colliding delimiters without
* correct precedence ordering. If delimiter A is a prefix of delimiter B,
* B must have lower (earlier) precedence so the longer match wins.
*/
validateInlineTags() {
const withDelimiters = this.inlineTags.filter((tag) => tag.delimiter).map((tag) => ({
name: tag.name,
delimiter: tag.delimiter,
precedence: tag.precedence ?? 50
}));
for (let i = 0; i < withDelimiters.length; i++) {
for (let j = i + 1; j < withDelimiters.length; j++) {
const a = withDelimiters[i];
const b = withDelimiters[j];
const aPrefix = b.delimiter.startsWith(a.delimiter);
const bPrefix = a.delimiter.startsWith(b.delimiter);
if (!aPrefix && !bPrefix) {
continue;
}
const longer = a.delimiter.length > b.delimiter.length ? a : b;
const shorter = a.delimiter.length > b.delimiter.length ? b : a;
if (longer.precedence >= shorter.precedence) {
throw new Error(
`Inline tag "${longer.name}" (delimiter "${longer.delimiter}") must have lower precedence than "${shorter.name}" (delimiter "${shorter.delimiter}") because its delimiter is a prefix match. Got ${longer.name}=${longer.precedence}, ${shorter.name}=${shorter.precedence}.`
);
}
}
}
}
/**
* Convert a markdown string to HTML.
*/
toHTML(md) {
return this.processBlocks(md);
}
/**
* Convert an HTML string back to markdown.
*/
toMarkdown(html) {
const container = document.createElement("div");
container.innerHTML = html;
return this.nodeToMd(container).replace(/\n{3,}/g, "\n\n").trim();
}
processBlocks(md) {
const lines = md.replace(/\r\n/g, "\n").split("\n");
const output = [];
let index = 0;
while (index < lines.length) {
if (/^\s*$/.test(lines[index])) {
index++;
continue;
}
let matched = false;
for (const tag of this.blockTags) {
const context = {
lines,
index,
text: "",
offset: 0
};
const token = tag.match(context);
if (!token) continue;
if (tag.name === "list") {
const result = parseListBlock(lines, index, 0, (source) => this.processInline(source));
output.push(result.html);
index = result.end;
} else {
output.push(tag.toHTML(token, this.makeConverter()));
index += token.consumed;
}
matched = true;
break;
}
if (!matched) {
index++;
}
}
return output.join("\n");
}
processInline(source) {
const sorted = [...this.inlineTags].sort(
(a, b) => (a.precedence ?? 50) - (b.precedence ?? 50)
);
const placeholders = [];
let text = source;
for (const tag of sorted) {
const recursive = tag.recursive ?? true;
if (tag.name === "link") {
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_, linkText, href) => {
let inner = linkText;
const hasPlaceholders = /\x00P\d+\x00/.test(inner);
if (hasPlaceholders) {
inner = inner.replace(/\x00P(\d+)\x00/g, (__, idx) => placeholders[parseInt(idx)]);
} else {
inner = this.processInline(inner);
}
placeholders.push('<a href="' + escapeHtml(href) + '">' + inner + "</a>");
return "\0P" + (placeholders.length - 1) + "\0";
});
} else if (!recursive && tag.pattern) {
const globalPattern = tag.pattern;
globalPattern.lastIndex = 0;
text = text.replace(globalPattern, (_, content) => {
placeholders.push(tag.toHTML(
{ content, raw: "", consumed: 0 },
this.makeConverter()
));
return "\0P" + (placeholders.length - 1) + "\0";
});
}
}
text = escapeHtml(text);
for (const tag of sorted) {
const recursive = tag.recursive ?? true;
if (tag.name === "link" || !recursive) {
continue;
}
const globalPattern = tag.pattern;
if (globalPattern) {
globalPattern.lastIndex = 0;
text = text.replace(globalPattern, (_, content) => {
const restored = content.replace(/\x00P(\d+)\x00/g, (__, idx) => placeholders[parseInt(idx)]);
const htmlTag = tag.name === "boldItalic" ? null : (tag.selector || "").split(",")[0].toLowerCase();
if (tag.name === "boldItalic") {
return "<em><strong>" + restored + "</strong></em>";
}
return `<${htmlTag}>${restored}</${htmlTag}>`;
});
}
}
text = text.replace(/\x00P(\d+)\x00/g, (_, index) => placeholders[parseInt(index)]);
return text;
}
nodeToMd(node) {
if (node.nodeType === 3) {
return node.textContent || "";
}
if (node.nodeType !== 1) {
return "";
}
const element = node;
const tag = this.tags.get(element.nodeName);
if (tag) {
return tag.toMarkdown(element, this.makeConverter());
}
return this.childrenToMd(node);
}
childrenToMd(node) {
return Array.from(node.childNodes).map((child) => this.nodeToMd(child)).join("");
}
makeConverter() {
return {
inline: (source) => this.processInline(source),
block: (md) => this.processBlocks(md),
children: (node) => this.childrenToMd(node),
node: (node) => this.nodeToMd(node)
};
}
};
var hopdown = new HopDown();
var hopdown_default = hopdown;
// src/ribbit.ts
var RibbitPlugin = class {
constructor(settings) {
this.name = settings.name;
this.wiki = settings.wiki;
this.precedence = 50;
}
setEditable() {
}
toMarkdown(html) {
return html;
}
toHTML(md) {
return md;
}
};
var Ribbit = class {
constructor(settings) {
this.api = settings.api || null;
this.element = document.getElementById(settings.editorId || "ribbit");
this.states = {
VIEW: "view"
};
this.cachedHTML = null;
this.cachedMarkdown = null;
this.state = null;
this.changed = false;
this.enabledPlugins = {};
(settings.plugins || []).forEach((plugin) => {
this.enabledPlugins[plugin.name] = new plugin({
name: plugin.name,
wiki: this
});
});
}
run() {
this.element.classList.add("loaded");
this.view();
}
plugins() {
return Object.values(this.enabledPlugins).sort((a, b) => a.precedence - b.precedence);
}
getState() {
return this.state;
}
setState(newState) {
this.state = newState;
Object.values(this.states).forEach((state) => {
if (state === newState) {
this.element.classList.add(state);
} else {
this.element.classList.remove(state);
}
});
}
markdownToHTML(md) {
return hopdown_default.toHTML(md);
}
getHTML() {
if (this.changed || !this.cachedHTML) {
this.cachedHTML = this.markdownToHTML(this.getMarkdown());
}
return this.cachedHTML;
}
getMarkdown() {
if (!this.cachedMarkdown) {
this.cachedMarkdown = this.element.textContent || "";
}
return this.cachedMarkdown;
}
view() {
if (this.getState() === this.states.VIEW) return;
this.element.innerHTML = this.getHTML();
this.setState(this.states.VIEW);
this.element.contentEditable = "false";
}
};
function camelCase(words) {
return words.trim().split(/\s+/g).map((word) => {
const lc = word.toLowerCase();
return lc.charAt(0).toUpperCase() + lc.slice(1);
});
}
function decodeHtmlEntities(html) {
const txt = document.createElement("textarea");
txt.innerHTML = html;
return txt.value;
}
function encodeHtmlEntities(str) {
return str.replace(/[\u00A0-\u9999<>&]/g, (i) => "&#" + i.charCodeAt(0) + ";");
}
// src/ribbit-editor.ts
var RibbitEditor = class extends Ribbit {
run() {
this.states = {
VIEW: "view",
EDIT: "edit",
WYSIWYG: "wysiwyg"
};
this.#bindEvents();
this.plugins().forEach((plugin) => {
plugin.setEditable();
});
this.element.classList.add("loaded");
this.view();
}
#bindEvents() {
this.element.addEventListener("input", () => {
if (this.state !== this.states.VIEW) {
this.changed = true;
}
});
}
htmlToMarkdown(html) {
return hopdown_default.toMarkdown(html || this.element.innerHTML);
}
getMarkdown() {
if (this.getState() === this.states.EDIT) {
let html = this.element.innerHTML;
html = html.replace(/<(?:div|br)>/ig, "");
html = html.replace(/<\/div>/ig, "\n");
this.cachedMarkdown = decodeHtmlEntities(html);
} else if (this.getState() === this.states.WYSIWYG) {
this.cachedMarkdown = this.htmlToMarkdown(this.element.innerHTML);
}
if (!this.cachedMarkdown) {
this.cachedMarkdown = this.element.textContent || "";
}
return this.cachedMarkdown;
}
wysiwyg() {
if (this.getState() === this.states.WYSIWYG) return;
this.changed = false;
this.element.contentEditable = "true";
this.element.innerHTML = this.getHTML();
Array.from(this.element.querySelectorAll(".macro")).forEach((el) => {
const macroEl = el;
if (macroEl.dataset.editable === "false") {
macroEl.contentEditable = "false";
macroEl.style.opacity = "0.5";
}
});
this.setState(this.states.WYSIWYG);
}
edit() {
if (this.state === this.states.EDIT) return;
this.changed = false;
this.element.contentEditable = "true";
this.element.innerHTML = encodeHtmlEntities(this.getMarkdown());
this.setState(this.states.EDIT);
}
insertAtCursor(node) {
const sel = window.getSelection();
const range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(node);
this.element.focus();
sel.removeAllRanges();
sel.addRange(range);
}
};
window.HopDown = HopDown;
window.hopdown = hopdown_default;
window.inlineTag = inlineTag;
window.defaultTags = defaultTags;
window.defaultBlockTags = defaultBlockTags;
window.defaultInlineTags = defaultInlineTags;
window.Ribbit = Ribbit;
window.RibbitEditor = RibbitEditor;
window.RibbitPlugin = RibbitPlugin;
window.camelCase = camelCase;
window.decodeHtmlEntities = decodeHtmlEntities;
window.encodeHtmlEntities = encodeHtmlEntities;
})();
//# sourceMappingURL=ribbit.js.map
+39
View File
@@ -0,0 +1,39 @@
"use strict";(()=>{function u(t){let e=t.delimiter.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),n=new RegExp("^"+e+"(.+?)"+e),o=new RegExp(e+"(.+?)"+e,"g"),r=[t.htmlTag.toUpperCase(),...(t.aliases||"").split(",").filter(Boolean)].join(","),s=t.recursive!==!1;return{name:t.name,precedence:t.precedence??50,recursive:s,pattern:o,delimiter:t.delimiter,match:l=>{let i=l.text.slice(l.offset).match(n);return i?{content:i[1],raw:i[0],consumed:i[0].length}:null},toHTML:(l,i)=>{let c=s?i.inline(l.content):m(l.content);return`<${t.htmlTag}>${c}</${t.htmlTag}>`},selector:r,toMarkdown:(l,i)=>!s&&l.parentNode?.nodeName==="PRE"?i.children(l):t.delimiter+i.children(l)+t.delimiter}}function m(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function S(t){return t.trim().split(/\s+/).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join("")}function x(t,e,n,o){let a=new RegExp("^"+" ".repeat(n)+"([\\*\\-]|\\d+\\.)\\s"),s=/^\d+\./.test(t[e].trim())?"ol":"ul",l=[],i=e;for(;i<t.length;){let d=t[i];if(/^\s*$/.test(d))break;let h=d.match(/^(\s*)/)[1].length;if(h<n)break;if(h>n){let g=x(t,i,h,o);l[l.length-1].sub=g.html,i=g.end;continue}if(!a.test(d))break;l.push({text:d.replace(a,""),sub:""}),i++}return{html:"<"+s+">"+l.map(d=>"<li>"+o(d.text)+d.sub+"</li>").join("")+"</"+s+">",end:i}}function I(t,e,n){let o=t.nodeName==="OL",a=" ".repeat(e),r=[];Array.from(t.children).forEach((l,i)=>{let c=o?i+1+". ":"- ",d="",h="";Array.from(l.childNodes).forEach(g=>{g.nodeType===1&&(g.nodeName==="UL"||g.nodeName==="OL")?h+=I(g,e+1,n):d+=n.node(g)}),r.push(a+c+d.trim()),h&&r.push(h)});let s=r.join(`
`);return e===0?`
`+s+`
`:s}function R(t,e){let n=t[e];return!!(/^(`{3,})/.test(n)||/^(\*{3,}|-{3,}|_{3,})\s*$/.test(n)||/^#{1,6}\s/.test(n)||/^>\s?/.test(n)||/^[*\-]\s/.test(n)||/^\d+\.\s/.test(n)||n.indexOf("|")!==-1&&e+1<t.length&&/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(t[e+1]))}function k(t){return t.replace(/^\|/,"").replace(/\|$/,"").split("|").map(e=>e.trim())}function O(t){return k(t).map(e=>/^:-+:$/.test(e)?"center":/^-+:$/.test(e)?"right":null)}var p={PRE:{name:"fencedCode",match:t=>{let e=t.lines[t.index].match(/^(`{3,})(.*)/);if(!e)return null;let n=e[1],o=e[2].trim(),a=[],r=t.index+1;for(;r<t.lines.length&&!t.lines[r].startsWith(n);)a.push(t.lines[r++]);return{content:a.join(`
`),raw:"",consumed:r+1-t.index,meta:{lang:o}}},toHTML:t=>"<pre><code"+(t.meta?.lang?` class="language-${m(t.meta.lang)}"`:"")+">"+m(t.content)+"</code></pre>",selector:"PRE",toMarkdown:t=>{let e=t.querySelector("code");return"\n\n```"+((e?.getAttribute("class")||"").match(/language-(\S+)/)?.[1]||"")+`
`+(e?.textContent||t.textContent||"")+"\n```\n\n"}},HR:{name:"hr",match:t=>/^(\*{3,}|-{3,}|_{3,})\s*$/.test(t.lines[t.index])?{content:"",raw:"",consumed:1}:null,toHTML:()=>"<hr>",selector:"HR",toMarkdown:()=>`
---
`},"H1,H2,H3,H4,H5,H6":{name:"heading",match:t=>{let e=t.lines[t.index].match(/^(#{1,6})\s+(.*)/);return e?{content:e[2].trim(),raw:"",consumed:1,meta:{level:String(e[1].length)}}:null},toHTML:(t,e)=>`<h${t.meta.level} id='${S(t.content)}'>${e.inline(t.content)}</h${t.meta.level}>`,selector:"H1,H2,H3,H4,H5,H6",toMarkdown:(t,e)=>`
`+"#".repeat(parseInt(t.nodeName[1]))+" "+e.children(t)+`
`},BLOCKQUOTE:{name:"blockquote",match:t=>{if(!/^>\s?/.test(t.lines[t.index]))return null;let e=[],n=t.index;for(;n<t.lines.length&&/^>\s?/.test(t.lines[n]);)e.push(t.lines[n++].replace(/^>\s?/,""));return{content:e.join(`
`),raw:"",consumed:n-t.index}},toHTML:(t,e)=>"<blockquote>"+e.block(t.content)+"</blockquote>",selector:"BLOCKQUOTE",toMarkdown:(t,e)=>`
`+e.children(t).trim().split(`
`).map(n=>"> "+n).join(`
`)+`
`},"UL,OL":{name:"list",match:t=>{let e=t.lines[t.index];return!/^[*\-]\s/.test(e)&&!/^\d+\.\s/.test(e)?null:{content:"",raw:"",consumed:0}},toHTML:t=>t.raw,selector:"UL,OL",toMarkdown:(t,e)=>I(t,0,e)},TABLE:{name:"table",match:t=>{let{lines:e,index:n}=t;if(e[n].indexOf("|")===-1||n+1>=e.length||!/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?\s*$/.test(e[n+1]))return null;let o=k(e[n]),a=O(e[n+1]),r=[],s=n+2;for(;s<e.length&&e[s].indexOf("|")!==-1&&!/^\s*$/.test(e[s]);)r.push(k(e[s++]));return{content:"",raw:"",consumed:s-n,meta:{headers:JSON.stringify(o),aligns:JSON.stringify(a),rows:JSON.stringify(r)}}},toHTML:(t,e)=>{let n=JSON.parse(t.meta.headers),o=JSON.parse(t.meta.aligns),a=JSON.parse(t.meta.rows);function r(i,c,d){let h=o[d]?` style="text-align:${o[d]}"`:"";return`<${i}${h}>${e.inline(c)}</${i}>`}let s="<thead><tr>"+n.map((i,c)=>r("th",i,c)).join("")+"</tr></thead>",l=a.map(i=>"<tr>"+i.map((c,d)=>r("td",c,d)).join("")+"</tr>").join("");return"<table>"+s+"<tbody>"+l+"</tbody></table>"},selector:"TABLE",toMarkdown:(t,e)=>{let n=Array.from(t.querySelectorAll("tr"));if(!n.length)return"";let o=Array.from(n[0].querySelectorAll("th,td")).map(s=>e.children(s).trim()),a=o.map(()=>"---"),r=["| "+o.join(" | ")+" |","| "+a.join(" | ")+" |"];return n.slice(1).forEach(s=>{let l=Array.from(s.querySelectorAll("td,th")).map(i=>e.children(i).trim());r.push("| "+l.join(" | ")+" |")}),`
`+r.join(`
`)+`
`}},P:{name:"paragraph",match:t=>{let e=[],n=t.index;for(;n<t.lines.length&&!/^\s*$/.test(t.lines[n])&&!R(t.lines,n);)e.push(t.lines[n++]);return e.length?{content:e.join(`
`),raw:"",consumed:n-t.index}:null},toHTML:(t,e)=>"<p>"+e.inline(t.content)+"</p>",selector:"P",toMarkdown:(t,e)=>`
`+e.children(t)+`
`}},f={CODE:u({name:"code",delimiter:"`",htmlTag:"code",precedence:10,recursive:!1}),A:{name:"link",match:t=>{let e=t.text.slice(t.offset).match(/^\[([^\]]+)\]\(([^)]+)\)/);return e?{content:e[1],raw:e[0],consumed:e[0].length,meta:{href:e[2]}}:null},toHTML:(t,e)=>'<a href="'+m(t.meta.href)+'">'+e.inline(t.content)+"</a>",selector:"A",toMarkdown:(t,e)=>"["+e.children(t)+"]("+(t.getAttribute("href")||"")+")"},_boldItalic:{...u({name:"boldItalic",delimiter:"***",htmlTag:"em",precedence:30}),toHTML:(t,e)=>"<em><strong>"+e.inline(t.content)+"</strong></em>",selector:(t=>!1),toMarkdown:()=>""},"STRONG,B":u({name:"bold",delimiter:"**",htmlTag:"strong",aliases:"B",precedence:40}),"EM,I":u({name:"italic",delimiter:"*",htmlTag:"em",aliases:"I",precedence:50})},w={...p,...f};var T=class{constructor(e={}){let n;if(e.tags)n=e.tags;else if(e.exclude){let s=new Set(e.exclude);n=Object.fromEntries(Object.entries(w).filter(([,l])=>!s.has(l.name)))}else n=w;let o=Object.values(n),a=new Set(Object.values(p).map(s=>s.name)),r=new Set(Object.values(f).map(s=>s.name));this.blockTags=o.filter(s=>a.has(s.name)||!r.has(s.name)&&!s.pattern),this.inlineTags=o.filter(s=>r.has(s.name)||s.pattern),this.tags=new Map;for(let[s,l]of Object.entries(n))for(let i of s.split(",").map(c=>c.trim()).filter(Boolean)){if(i.startsWith("_"))continue;let c=this.tags.get(i);if(c&&c!==l)throw new Error(`HTML tag "${i}" is claimed by both "${c.name}" and "${l.name}". Use the exclude option to remove one before adding the other.`);this.tags.set(i,l)}this.validateInlineTags()}validateInlineTags(){let e=this.inlineTags.filter(n=>n.delimiter).map(n=>({name:n.name,delimiter:n.delimiter,precedence:n.precedence??50}));for(let n=0;n<e.length;n++)for(let o=n+1;o<e.length;o++){let a=e[n],r=e[o],s=r.delimiter.startsWith(a.delimiter),l=a.delimiter.startsWith(r.delimiter);if(!s&&!l)continue;let i=a.delimiter.length>r.delimiter.length?a:r,c=a.delimiter.length>r.delimiter.length?r:a;if(i.precedence>=c.precedence)throw new Error(`Inline tag "${i.name}" (delimiter "${i.delimiter}") must have lower precedence than "${c.name}" (delimiter "${c.delimiter}") because its delimiter is a prefix match. Got ${i.name}=${i.precedence}, ${c.name}=${c.precedence}.`)}}toHTML(e){return this.processBlocks(e)}toMarkdown(e){let n=document.createElement("div");return n.innerHTML=e,this.nodeToMd(n).replace(/\n{3,}/g,`
`).trim()}processBlocks(e){let n=e.replace(/\r\n/g,`
`).split(`
`),o=[],a=0;for(;a<n.length;){if(/^\s*$/.test(n[a])){a++;continue}let r=!1;for(let s of this.blockTags){let l={lines:n,index:a,text:"",offset:0},i=s.match(l);if(i){if(s.name==="list"){let c=x(n,a,0,d=>this.processInline(d));o.push(c.html),a=c.end}else o.push(s.toHTML(i,this.makeConverter())),a+=i.consumed;r=!0;break}}r||a++}return o.join(`
`)}processInline(e){let n=[...this.inlineTags].sort((r,s)=>(r.precedence??50)-(s.precedence??50)),o=[],a=e;for(let r of n){let s=r.recursive??!0;if(r.name==="link")a=a.replace(/\[([^\]]+)\]\(([^)]+)\)/g,(l,i,c)=>{let d=i;return/\x00P\d+\x00/.test(d)?d=d.replace(/\x00P(\d+)\x00/g,(g,L)=>o[parseInt(L)]):d=this.processInline(d),o.push('<a href="'+m(c)+'">'+d+"</a>"),"\0P"+(o.length-1)+"\0"});else if(!s&&r.pattern){let l=r.pattern;l.lastIndex=0,a=a.replace(l,(i,c)=>(o.push(r.toHTML({content:c,raw:"",consumed:0},this.makeConverter())),"\0P"+(o.length-1)+"\0"))}}a=m(a);for(let r of n){let s=r.recursive??!0;if(r.name==="link"||!s)continue;let l=r.pattern;l&&(l.lastIndex=0,a=a.replace(l,(i,c)=>{let d=c.replace(/\x00P(\d+)\x00/g,(g,L)=>o[parseInt(L)]),h=r.name==="boldItalic"?null:(r.selector||"").split(",")[0].toLowerCase();return r.name==="boldItalic"?"<em><strong>"+d+"</strong></em>":`<${h}>${d}</${h}>`}))}return a=a.replace(/\x00P(\d+)\x00/g,(r,s)=>o[parseInt(s)]),a}nodeToMd(e){if(e.nodeType===3)return e.textContent||"";if(e.nodeType!==1)return"";let n=e,o=this.tags.get(n.nodeName);return o?o.toMarkdown(n,this.makeConverter()):this.childrenToMd(e)}childrenToMd(e){return Array.from(e.childNodes).map(n=>this.nodeToMd(n)).join("")}makeConverter(){return{inline:e=>this.processInline(e),block:e=>this.processBlocks(e),children:e=>this.childrenToMd(e),node:e=>this.nodeToMd(e)}}},C=new T;var b=C;var H=class{constructor(e){this.name=e.name,this.wiki=e.wiki,this.precedence=50}setEditable(){}toMarkdown(e){return e}toHTML(e){return e}},M=class{constructor(e){this.api=e.api||null,this.element=document.getElementById(e.editorId||"ribbit"),this.states={VIEW:"view"},this.cachedHTML=null,this.cachedMarkdown=null,this.state=null,this.changed=!1,this.enabledPlugins={},(e.plugins||[]).forEach(n=>{this.enabledPlugins[n.name]=new n({name:n.name,wiki:this})})}run(){this.element.classList.add("loaded"),this.view()}plugins(){return Object.values(this.enabledPlugins).sort((e,n)=>e.precedence-n.precedence)}getState(){return this.state}setState(e){this.state=e,Object.values(this.states).forEach(n=>{n===e?this.element.classList.add(n):this.element.classList.remove(n)})}markdownToHTML(e){return b.toHTML(e)}getHTML(){return(this.changed||!this.cachedHTML)&&(this.cachedHTML=this.markdownToHTML(this.getMarkdown())),this.cachedHTML}getMarkdown(){return this.cachedMarkdown||(this.cachedMarkdown=this.element.textContent||""),this.cachedMarkdown}view(){this.getState()!==this.states.VIEW&&(this.element.innerHTML=this.getHTML(),this.setState(this.states.VIEW),this.element.contentEditable="false")}};function $(t){return t.trim().split(/\s+/g).map(e=>{let n=e.toLowerCase();return n.charAt(0).toUpperCase()+n.slice(1)})}function y(t){let e=document.createElement("textarea");return e.innerHTML=t,e.value}function E(t){return t.replace(/[\u00A0-\u9999<>&]/g,e=>"&#"+e.charCodeAt(0)+";")}var v=class extends M{run(){this.states={VIEW:"view",EDIT:"edit",WYSIWYG:"wysiwyg"},this.#e(),this.plugins().forEach(e=>{e.setEditable()}),this.element.classList.add("loaded"),this.view()}#e(){this.element.addEventListener("input",()=>{this.state!==this.states.VIEW&&(this.changed=!0)})}htmlToMarkdown(e){return b.toMarkdown(e||this.element.innerHTML)}getMarkdown(){if(this.getState()===this.states.EDIT){let e=this.element.innerHTML;e=e.replace(/<(?:div|br)>/ig,""),e=e.replace(/<\/div>/ig,`
`),this.cachedMarkdown=y(e)}else this.getState()===this.states.WYSIWYG&&(this.cachedMarkdown=this.htmlToMarkdown(this.element.innerHTML));return this.cachedMarkdown||(this.cachedMarkdown=this.element.textContent||""),this.cachedMarkdown}wysiwyg(){this.getState()!==this.states.WYSIWYG&&(this.changed=!1,this.element.contentEditable="true",this.element.innerHTML=this.getHTML(),Array.from(this.element.querySelectorAll(".macro")).forEach(e=>{let n=e;n.dataset.editable==="false"&&(n.contentEditable="false",n.style.opacity="0.5")}),this.setState(this.states.WYSIWYG))}edit(){this.state!==this.states.EDIT&&(this.changed=!1,this.element.contentEditable="true",this.element.innerHTML=E(this.getMarkdown()),this.setState(this.states.EDIT))}insertAtCursor(e){let n=window.getSelection(),o=n.getRangeAt(0);o.deleteContents(),o.insertNode(e),o.setStartAfter(e),this.element.focus(),n.removeAllRanges(),n.addRange(o)}};window.HopDown=T;window.hopdown=b;window.inlineTag=u;window.defaultTags=w;window.defaultBlockTags=p;window.defaultInlineTags=f;window.Ribbit=M;window.RibbitEditor=v;window.RibbitPlugin=H;window.camelCase=$;window.decodeHtmlEntities=y;window.encodeHtmlEntities=E;})();
-261
View File
@@ -1,261 +0,0 @@
@import "theme.css";
html {
padding: 0px;
margin: 0px;
height: 100vh;
box-sizing: border-box;
}
body {
font-family: var(--default-font-family);
font-size: var(--default-font-size);
height: inherit;
width: 100%;
margin: 0px;
box-sizing: border-box;
overflow-wrap: break-word;
overflow-y: scroll;
scrollbar-gutter: stable;
}
h1, .toastui-editor-contents h1 {
font-size: var(--h1-size) !important;
font-weight: var(--header-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h2, .toastui-editor-contents h2 {
font-size: var(--h2-size) !important;
font-weight: var(--header2-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h3, .toastui-editor-contents h3 {
font-size: var(--h3-size) !important;
font-weight: var(--header3-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h4, .toastui-editor-contents h4 {
font-size: var(--h4-size) !important;
font-weight: var(--header4-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h5, .toastui-editor-contents h5 {
font-size: var(--h5-size) !important;
font-weight: var(--header5-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h6, .toastui-editor-contents h6 {
font-size: var(--h6-size) !important;
font-weight: var(--header6-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
a {text-decoration: none;}
nav ul.container {
margin: auto;
}
ul {
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
list-style: none;
}
nav {
display: block;
margin: 0px;
padding: 0px;
height: var(--nav-height);
background: #0ca0d6;
}
nav > ul > li {
display: inline-block;
font-size: 14px;
padding: 0 15px;
position: relative;
}
nav > ul > li > a {
color: #fff;
display: inline-block;
padding: 20px 0;
border-bottom: 3px solid transparent;
transition: all .3s ease;
}
nav > ul > li:hover > a {
color: #444;
border-bottom: 3px solid #444;
}
.mega-menu {
background: #eee;
visibility: hidden;
opacity: 0;
transition: visibility 0s, opacity 0.5s linear;
position: absolute;
left: 0;
width: 100%;
padding-bottom: 20px;
}
.mega-menu h3 {color: #444;}
.mega-menu .container {
display: flex;
}
.mega-menu .item {
flex-grow: 1;
margin: 0 10px;
}
.mega-menu .item img {
width: 100%;
}
.mega-menu a {
border-bottom: 1px solid #ddd;
color: #4ea3d8;
display: block;
padding: 10px 0;
}
.mega-menu a:hover {color: #2d6a91;}
.dropdown {position: static;}
.dropdown:hover .mega-menu {
visibility: visible;
opacity: 1;
}
#breadcrumbs {
height: var(--breadcrumbs-height);
border-bottom: 1px dotted #DEDEDE;
}
#data_form {
display: none;
}
#editor {
display: none;
}
#viewer {
display: inline;
}
#actions {
display: flex;
float: right;
}
#actions > a {
display: flex;
border: 1px solid black;
border-radius: 5px;
padding: 5px;
background: #FFF;
color: blue;
cursor: pointer;
margin-left: 5px;
}
#actions a:hover {
color: white;
background: blue;
}
.table-wrapper {
padding: 0px;
padding-top: var(--wrapper-padding);
display: table;
margin: auto;
width: var(--max-width);
max-width: 960px;
}
main {
display: table-row;
height: var(--main-height);
}
footer {
display: block;
position: relative;
bottom: 100vh - var(--footer-height);
height: var(--footer-height);
background-color: #DEDEDE;
}
.toastui-editor-main-container {
display: contents;
}
.toastui-editor,
.toastui-editor-main,
.toastui-editor-md-container,
.toastui-editor-defaultUI,
.toastui-editor-contents,
.toastui-editor-ww-mode,
.toastui-editor-md-preview,
.toastui-editor-md,
.ProseMirror {
border: 0px !important;
padding: 0px !important;
margin: 0px !important;
font-size: var(--default-font-size) !important;
font-family: var(--default-font-family) !important;
}
menu {
display: none;
}
+24 -4
View File
@@ -12,10 +12,30 @@
--h6-size: 10.72px; --h6-size: 10.72px;
/* Layout */ /* Layout */
--content-padding: 20px;
--nav-height: 50px; --nav-height: 50px;
--footer-height: 150px; --nav-spacing: 15px;
--breadcrumbs-height: 50px; --breadcrumbs-height: 50px;
--wrapper-padding: 20px; --footer-height: 50px;
--main-height: calc(100vh - var(--nav-height) - var(--footer-height) - var(--breadcrumbs-height) + var(--wrapper-padding)); --footer-spacing: var(--breadcrumbs-height);
--max-width: calc(100vw - 20px); --toolbar-height: 0px;
--toolbar-spacing: 0px;
--masthead-spacing: 0px;
--max-width: 1024px;
--min-width: calc(710px + (2 * var(--content-padding)));
--main-height: calc(100vh - var(--nav-height) - var(--masthead-spacing) - var(--toolbar-height) - (3 * var(--toolbar-spacing)) - var(--footer-height) - var(--breadcrumbs-height) - calc(2 * var(--content-padding)) - var(--footer-spacing));
--content-border-radius: calc(0.5 * var(--content-padding));
--content-border: 1px solid transparent;
/* colors */
--blue: #0ca0d6;
--white: #FFF;
--body-background: #EEE;
--nav-color: #000055;
--nav-hover-color: #FFF;
--content-background: #FFF;
--footer-background-color: #DDD;
} }
+43
View File
@@ -0,0 +1,43 @@
<div class='main-aligned'>
<nav id='toolbar'>
<ul>
<li><button id='bold'></button></li>
<li><button id='italic'></button></li>
<li>
<button id='header' popovertarget='header-menu'></button>
<div id='header-menu' class='dropdown-menu' popover>
<button id='h1'></button>
<button id='h2'></button>
<button id='h3'></button>
<button id='h4'></button>
<button id='h5'></button>
<button id='h6'></button>
</div>
</li>
<li><button id='link'></button></li>
<li class='spacer'></li>
<li><button id='line'></button></li>
<li>
<button id='list' popovertarget='list-menu'></button>
<div id='list-menu' class='dropdown-menu' popover>
<button id='unordered_list'></button>
<button id='ordered_list'></button>
</div>
</li>
<li><button id='quote'></button></li>
<li><button id='table'></button></li>
<li>
<button id='macro' popovertarget='macro-menu'></button>
<div id='macro-menu' class='dropdown-menu' popover>
<button id='macro_user'></button>
<button id='macro_toc'></button>
<button id='macro_style'></button>
</div>
</li>
<li class='spacer'></li>
<li><button id='markdown' alt='toggle markdown editor'></button></li>
<li><button id='save' alt='save'></button></li>
<li><button id='toggle' alt='toggle edit mode'></button></li>
</ul>
</nav>
</div>
+123 -92
View File
@@ -1,71 +1,55 @@
from flask import Response, g, redirect, render_template, request, session, url_for import json
import re
from urllib.parse import unquote
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for
from grung.exceptions import ValidationError
from tinydb import where from tinydb import where
from ttfrog import app, forms, schema from ttfrog import app, schema
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
def relative_uri(path: str = ""): def get_page(
""" path: str, table: str = "Page", doc_id: int = None, create_okay: bool = False
The request's URI relative to the VIEW_URI without the leading '/'. ) -> (schema.Record | None, Exception | None):
"""
return (path or request.path).replace(app.config.VIEW_URI, "", 1).strip("/") or "/"
def get_parent(table: str, uri: str):
try:
parent_uri = uri.strip("/").rsplit("/", 1)[0]
except IndexError:
return None
return get_page(parent_uri, table=table if "/" in parent_uri else "Page", create_okay=False)
def get_page(path: str, table: str = "Page", create_okay: bool = False):
""" """
Get one page, including its members, but not recursively. Get one page, including its members, but not recursively.
""" """
uri = relative_uri(path) try:
page = app.get_page(g.user, table, doc_id=doc_id, uri=path)
if table not in app.db.tables(): except (UnauthorizedError, MalformedRequestError) as e:
return None return None, e
except RecordNotFoundError as e:
page = app.db.table(table).get(where("uri") == uri, recurse=False)
if not page:
if not create_okay: if not create_okay:
return None return None, e
parent = get_parent(table, uri) except ValidationError as e:
if not app.authorize(g.user, parent, schema.Permissions.READ): return None, e
return None return page, None
return getattr(schema, table)(name=uri.split("/")[-1], body="This page does not exist", parent=parent)
if not app.authorize(g.user, page, schema.Permissions.READ):
return None
if hasattr(page, "members"): def api_response(response={}, messages=[], error=None):
subpages = [] response_code = 200
for pointer in page.members:
table, pkey, pval = pointer.split("::")
subpages += app.db.table(table).search(where(pkey) == pval, recurse=False)
page.members = subpages
return page if error:
response_code = 500
response = {}
if isinstance(error, UnauthorizedError):
response_code = 403
elif isinstance(error, MalformedRequestError):
response_code = 4000
elif isinstance(error, RecordNotFoundError):
response_code = 404
return jsonify({"messages": messages, "response": response, "code": response_code}), response_code
def rendered(page: schema.Record, template: str = "page.html"): def rendered(page: schema.Record, template: str = "page.html"):
if not page: if not page:
return Response("Page not found", status=404) return Response("Page not found", status=404)
return render_template( root = page if page.uri == "" else get_page("")[0]
template, return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=root, user=g.user, g=g)
page=page,
app=app,
breadcrumbs=breadcrumbs(),
root=g.root,
user=g.user,
g=g
)
def breadcrumbs(): def breadcrumbs():
@@ -73,14 +57,40 @@ def breadcrumbs():
Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request. Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request.
""" """
uri = "" uri = ""
for name in relative_uri().split("/"): names = (request.path.replace(app.config.VIEW_URI, "", 1).strip("/") or "/").split("/")
for name in names:
uri = "/".join([uri, name]) uri = "/".join([uri, name])
yield (uri, name) yield (uri, name)
@app.web.before_request
def before_request():
g.messages = []
if not request.path.startswith("/static"):
user_id = session.get("user_id")
g.user = (
app.db.User.get(doc_id=user_id, recurse=False)
if user_id
else app.db.User.get(where("name") == "guest", recurse=False)
)
session["user_id"] = user_id
session["user"] = dict(g.user.serialize())
@app.web.after_request
def add_header(r):
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0"
r.headers["Pragma"] = "no-cache"
r.headers["Expires"] = "0"
return r
@app.web.route(app.config.VIEW_URI) @app.web.route(app.config.VIEW_URI)
def index(): def index():
return rendered(get_page(app.config.VIEW_URI, create_okay=False)) page, error = get_page("")
if error:
g.messages.append(str(error))
return rendered(page)
@app.web.route("/login", methods=["GET", "POST"]) @app.web.route("/login", methods=["GET", "POST"])
@@ -96,7 +106,7 @@ def login():
session["user"] = dict(g.user.serialize()) session["user"] = dict(g.user.serialize())
return redirect(url_for("index")) return redirect(url_for("index"))
g.messages.append(f"Invalid login for {username}") g.messages.append(f"Invalid login for {username}")
return rendered(schema.Page(name="Login", title="Please enter your login details"), "login.html") return rendered(schema.Page(name="Login"), "login.html")
@app.web.route("/logout") @app.web.route("/logout")
@@ -107,51 +117,72 @@ def logout():
return redirect(url_for("index")) return redirect(url_for("index"))
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["GET"]) @app.web.route(f"{app.config.VIEW_URI}/<string:table>/<string:path>", methods=["GET"])
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"}) @app.web.route(f"{app.config.VIEW_URI}/<string:path>", methods=["GET"], defaults={"table": "Page"})
def view(table, path): def view(table, path):
parent = get_parent(table, relative_uri()) clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
page = get_page(request.path, table=table, create_okay=(parent and parent.doc_id is not None)) clean_path = re.sub(r"[^a-zA-Z0-9]", "", unquote(path))
if clean_table != table or clean_path != clean_path:
app.log.warning(f"Invalid table/path: {table=}, {path=}. Redirecting to {clean_table}/{clean_path}")
return redirect(url_for("view", table=clean_table, path=clean_path), 302)
app.log.debug(f"Looking for {table=}, {path=}")
page, error = get_page(request.path, table=table, create_okay=True)
if error:
app.log.error(error)
g.messages.append(str(error))
return rendered(page) return rendered(page)
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["POST"]) @app.web.route(f"{app.config.API_URI}/put/<path:table>/<path:path>", methods=["POST"])
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["POST"], defaults={"table": "Page"}) @app.web.route(f"{app.config.API_URI}/put/<path:path>", methods=["POST"], defaults={"table": "Page"})
def edit(table, path): def put(table, path):
uri = relative_uri() clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
parent = get_parent(table, uri) clean_path = re.sub(r"[^a-zA-Z0-9]", "", unquote(path))
if not parent: if clean_table != table or clean_path != clean_path:
return Response("You cannot create a page at this location.", status=403) return redirect(url_for("put", table=clean_table, path=clean_path), 302)
# get or create the docoument at this uri page, error = get_page("/".join([table, path]), table=table, create_okay=True)
page = get_page(uri, table=table, create_okay=True) if error:
if not app.authorize(g.user, page, schema.Permissions.WRITE): return api_response(error=error)
return Response("Permission denied.", status=403)
save_data = getattr(forms, table)(page, request.form).prepare()
# editing existing document page.update(**json.loads(request.data.decode())["body"])
if page.doc_id: updated = app.db.save(page)
if page.uid != request.form["uid"]: if not page.doc_id:
return Response("Invalid UID.", status=403) parent = page.parent()
return rendered(app.db.save(save_data)) if parent:
parent.update(members=list(set(parent.members + [updated])))
# saving a new document app.db.save(parent)
return rendered(parent.add_member(save_data)) app.log.debug(f"Saved page at uri {updated.uri}")
return api_response(response=dict(updated))
@app.web.before_request @app.web.route(f"{app.config.API_URI}/search/<string:space>", methods=["POST"])
def before_request(): @app.web.route(f"{app.config.API_URI}/search/", methods=["POST"], defaults={"space": None})
g.messages = [] def search(space):
user_id = session.get("user_id", 1) spaces = app.db.tables()
g.user = app.db.User.get(doc_id=user_id) if space:
session["user_id"] = user_id spaces = [space.lower().capitalize()]
session["user"] = dict(g.user.serialize())
g.root = get_page(app.config.VIEW_URI) query = json.loads(request.data.decode())["body"]
app.log.debug(f"Searching for records matching query {query}")
matches = []
for space in spaces:
for page in app.db.table(space).search(where("name").matches(query, re.IGNORECASE), recurse=False):
if app.authorize(g.user, page, schema.Permissions.READ):
app.log.debug(f"Adding search result {dict(page)}")
matches.append(dict(page))
return api_response(
response=matches, error=None if matches else RecordNotFoundError(f"No records matching '{query}'")
)
@app.web.after_request @app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
def add_header(r): def get(table, doc_id):
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0" clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
r.headers["Pragma"] = "no-cache" if clean_table != table:
r.headers["Expires"] = "0" return redirect(url_for("get", table=clean_table, doc_id=doc_id), 302)
return r
app.log.debug(f"API: getting {table}({doc_id})")
page, error = get_page(g.user, table=table, doc_id=doc_id)
return api_response(response=dict(page), error=error)
+6 -4
View File
@@ -1,3 +1,6 @@
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest import pytest
from grung.db import GrungDB from grung.db import GrungDB
from tinydb import where from tinydb import where
@@ -9,7 +12,8 @@ from ttfrog import schema
@pytest.fixture @pytest.fixture
def app(): def app():
fixture_db = GrungDB.with_schema(schema, storage=MemoryStorage) with TemporaryDirectory() as path:
fixture_db = GrungDB.with_schema(schema, path=Path(path), storage=MemoryStorage)
ttfrog.app.load_config(defaults=None, IN_MEMORY_DB=1) ttfrog.app.load_config(defaults=None, IN_MEMORY_DB=1)
ttfrog.app.initialize(db=fixture_db, force=True) ttfrog.app.initialize(db=fixture_db, force=True)
yield ttfrog.app yield ttfrog.app
@@ -18,8 +22,6 @@ def app():
def test_create(app): def test_create(app):
user = schema.User(name="john", email="john@foo", password="powerfulCat") user = schema.User(name="john", email="john@foo", password="powerfulCat")
assert user.uid
assert user._metadata.fields["uid"].unique
# insert # insert
john_something = app.db.save(user) john_something = app.db.save(user)
@@ -29,7 +31,6 @@ def test_create(app):
assert app.db.User.get(doc_id=last_insert_id) == john_something assert app.db.User.get(doc_id=last_insert_id) == john_something
assert john_something.name == user.name assert john_something.name == user.name
assert john_something.email == user.email assert john_something.email == user.email
assert john_something.uid == user.uid
# update # update
john_something.name = "james?" john_something.name = "james?"
@@ -39,6 +40,7 @@ def test_create(app):
assert before_update != after_update assert before_update != after_update
@pytest.mark.xfail
def test_permissions(app): def test_permissions(app):
john = app.db.save(schema.User(name="john", email="john@foo", password="powerfulCat")) john = app.db.save(schema.User(name="john", email="john@foo", password="powerfulCat"))
players = app.db.save(schema.Group(name="players", members=[john])) players = app.db.save(schema.Group(name="players", members=[john]))
+39
View File
@@ -0,0 +1,39 @@
from pathlib import Path
from tempfile import TemporaryDirectory
import pytest
from grung.db import GrungDB
from tinydb.storages import MemoryStorage
import ttfrog.app
from ttfrog import schema
from ttfrog.bootstrap import bootstrap
@pytest.fixture
def app():
with TemporaryDirectory() as path:
fixture_db = GrungDB.with_schema(schema, path=Path(path), storage=MemoryStorage)
ttfrog.app.load_config(defaults=None, IN_MEMORY_DB=1)
ttfrog.app.initialize(db=fixture_db, force=True)
ttfrog.app.web.config.update({"TESTING": True})
bootstrap()
yield ttfrog.app
ttfrog.app.db.truncate()
@pytest.fixture
def routes(app):
import ttfrog.web
return ttfrog.web
@pytest.fixture()
def client(routes):
return ttfrog.app.web.test_client()
def test_get(client, routes):
response = client.get(ttfrog.app.config.VIEW_URI)
assert response.status_code == 200