Compare commits

...
16 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
35 changed files with 3620 additions and 856 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]
+6 -5
View File
@@ -96,7 +96,7 @@ API_URI=/_/v1/
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, path=None, 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, path=self.path.database, sort_keys=True, indent=4, separators=(",", ": ") schema, path=self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
@@ -165,12 +165,10 @@ API_URI=/_/v1/
else: else:
search_uri = uri search_uri = uri
# self.log.debug(f"Searching for page in {table = } with {search_uri = }; its parent is {parent_uri=}") self.log.debug(f"Looking for page with uri {search_uri}")
# self.log.debug("\n".join([f"{p.doc_id}: {p.uri}" for p in table.all()]))
page = table.get(where("uri") == search_uri, recurse=False) page = table.get(where("uri") == search_uri, recurse=False)
if not page: if not page:
# load the parent to check for write permissions # load the parent to check for write permissions
# self.log.debug(f"Page at {search_uri} does not exist, looking for parent at {parent_uri=}")
parent_table = table if parent_uri and "/" in parent_uri else self.db.Page parent_table = table if parent_uri and "/" in parent_uri else self.db.Page
parent = None parent = None
try: try:
@@ -185,7 +183,10 @@ API_URI=/_/v1/
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.") raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
obj = getattr(schema, table.name) obj = getattr(schema, table.name)
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent) 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}") # self.log.debug(f"Returning {page.doc_id}: {page.uri}")
return page return page
+229 -27
View File
@@ -1,41 +1,240 @@
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()
@@ -43,26 +242,29 @@ def bootstrap():
app.db.save(schema.User(name="__system__")) app.db.save(schema.User(name="__system__"))
# create the top-level pages # create the top-level pages
root = app.db.save(schema.Page(name=app.config.VIEW_URI, body=b"This is the home page", uri="")) root = app.db.save(schema.Page(name=app.config.VIEW_URI, body="This is the home page", uri=""))
users = root.add_member(schema.Page(name="User", body=b"# Users\nusers go here.")) root.add_member(schema.Page(name="Wiki", body=TEMPLATE))
groups = root.add_member(schema.Page(name="Group", body=b"# Groups\ngroups go here."))
npcs = root.add_member(schema.Page(name="NPC", body=b"# NPCS!")) users = root.add_member(schema.Page(name="User", body="# Users\nusers go here."))
wiki = root.add_member(schema.Page(name="Wiki", body=TEMPLATE.encode())) 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", body=b"# 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, body=b"# fnord") 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], body=b"# administrators")) 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(
-83
View File
@@ -1,83 +0,0 @@
import logging
from dataclasses import dataclass, field
from functools import cached_property
from flask import g
from grung.types import BackReference, Collection, Pointer, Record, Timestamp
from ttfrog import schema
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
logger = logging.getLogger(__name__)
@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", "acl"]
def prepare(self):
for key, value in self.data.items():
if key in self.read_only:
continue
if self.record[key] != value:
self.record[key] = value
self.record.author = None if self.record == g.user else g.user
return self.record
@dataclass
class Page(Form):
"""
A form for creating and updating Page records.
"""
record: schema.Page
@dataclass
class Wiki(Form):
"""
A form for creating and updating Wiki records.
"""
record: schema.Page
@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
+162 -17
View File
@@ -1,24 +1,30 @@
from __future__ import annotations from __future__ import annotations
import re
from datetime import datetime from datetime import datetime
from enum import StrEnum from enum import StrEnum
from functools import cached_property
from textwrap import dedent from textwrap import dedent
from typing import List from typing import List
from grung.types import ( from flask import g
from grung.objects import (
BackReference, BackReference,
Collection, Collection,
DateTime, DateTime,
Dict, Dict,
Field,
Password, Password,
Pointer, Pointer,
Record, Record,
String,
TextFilePointer, TextFilePointer,
Timestamp, Timestamp,
) )
from grung.validators import LengthValidator, PatternValidator
from tinydb import where from tinydb import where
from ttfrog.exceptions import MalformedRequestError
def app_context(): def app_context():
import ttfrog.app import ttfrog.app
@@ -27,6 +33,9 @@ def app_context():
return ttfrog.app return ttfrog.app
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
class Permissions(StrEnum): class Permissions(StrEnum):
READ = "r" READ = "r"
WRITE = "w" WRITE = "w"
@@ -57,17 +66,62 @@ class Page(Record):
# 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
Collection("members", Page), # The pages that exist below this page's URI String("uri", unique=True, validators=[
Pointer("author", value_type=User), # The last user to touch the page. PatternValidator(re.compile(
DateTime("created"), # When the page was created r"""
Timestamp("last_modified"), # The last time the page was modified. ^ # match from beginning of line
(?:
/ # first char can be a /
| # 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"),
TextFilePointer("body", extension='.md', default=Page.default), # The main content blob of the page
] ]
# fmt: on # fmt: on
@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): def parent(self):
if self.uri == "": if self.uri == "":
return None return None
@@ -75,7 +129,7 @@ class Page(Record):
app = app_context() app = app_context()
parent_uri = "" parent_uri = ""
if "/" in self.uri: if self.uri and "/" in self.uri:
parent_uri = self.uri.rsplit("/", 1)[0] parent_uri = self.uri.rsplit("/", 1)[0]
for table_name in app.db.tables(): for table_name in app.db.tables():
page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False) page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False)
@@ -95,7 +149,7 @@ class Page(Record):
super().before_insert(app.db) super().before_insert(app.db)
if not self.author: if not self.author:
self.author = app.db.User.get(where('name') == '__system__') 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:
@@ -161,7 +215,7 @@ class Entity(Page):
def fields(cls): def fields(cls):
inherited = [field for field in super().fields() if field.name not in ("members", "uid")] inherited = [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:
@@ -187,10 +241,16 @@ 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)
@@ -218,7 +278,7 @@ class Group(Entity):
@classmethod @classmethod
def fields(cls): def fields(cls):
return super().fields() + [Collection("members", Entity)] return super().fields() + [Collection("members", member_type=Entity)]
class NPC(Page): class NPC(Page):
@@ -243,3 +303,88 @@ class NPC(Page):
""" """
) )
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),
]
+13 -11
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,22 +13,24 @@
<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" %} {% include "breadcrumbs.html" %}
{% endblock %} {% endblock %}
{% if user.can_write(page) %}
{% include "toolbar.html" %}
{% endif %}
</div>
<div class='main-aligned'> <div class='main-aligned'>
<div class='content'> <main id='main'>
<main>
{% for message in g.messages %} {% for message in g.messages %}
<div class="alert"> <dialog class="alert">
{{ message }} {{ message }}
</div> </dialog>
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
</div>
</div> </div>
<footer> <footer>
{% block footer %} {% block footer %}
+1 -1
View File
@@ -1,4 +1,4 @@
<nav> <nav id='pagenav'>
<ul class="container content-aligned"> <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 %}
+13 -12
View File
@@ -1,26 +1,27 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block styles %} {% block styles %}
{% if user.can_write(page) %} {% if user.can_write(page) %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='editor/editor.css' ) }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat-editor.css' ) }}">
{% else %} {% endif %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='viewer/viewer.css' ) }}">
{% endif %}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div id='{% if user.can_write(page) %}editor{% else %}viewer{% endif %}' class='read-only'></div> <article id='froghat'>{{ page.body }}</article>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
<!-- 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) %} {% if user.can_write(page) %}
<script src="{{ url_for('static', filename='editor/toastui-editor-all.min.js' ) }}"></script> <script src="{{ url_for('static', filename='froghat-editor.js' ) }}"></script>
<script src="{{ url_for('static', filename='editor/editor.js' ) }}"></script>
<script>initialize("{{ app.config.VIEW_URI }}");</script>
{% else %}
<script src="{{ url_for('static', filename='viewer/toastui-editor-viewer.min.js' ) }}"></script>
<script src="{{ url_for('static', filename='viewer/viewer.js' ) }}"></script>
{% endif %} {% endif %}
<script>
const wiki = new Froghat{% if user.can_write(page) %}Editor{% endif %}({plugins: [MacroPlugin]});
wiki.run();
</script>
{% endblock %} {% endblock %}
@@ -1,106 +0,0 @@
@import 'toastui-editor.min.css';
#editor {
display: inline;
}
.toastui-editor-defaultUI-toolbar {
padding: 0px !important;
margin: 0px !important;
border: 0px !important;
background: transparent !important;
}
.toastui-editor-toolbar {
box-shadow: 0px 3px 5px #CCC;
margin-bottom: 20px !important;
}
.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;
}
#editor .toastui-editor-md-container > div > div,
#editor .toastui-editor-ww-container > div > div {
border: 1px dotted #ccc;
}
#editor .toastui-editor-defaultUI-toolbar {
margin-bottom: 20px;
}
#editor .toastui-editor-toolbar {
position: sticky !important;
top: 0 !important;
z-index: 99 !important;
width: 100% !important;
background: #FFF !important;
}
/* applied to #editor */
.read-only {
}
#editor.read-only .toastui-editor-toolbar {
box-shadow: none;
}
#editor.read-only .toastui-editor-ww-container > div > div {
background: #FFF;
}
div.toastui-editor-md-splitter,
div.toastui-editor-md-preview {
display: none !Important;
}
div.toastui-editor.md-mode {
width: 100% !important;
}
#editor button.toastui-editor-toolbar-icons {
filter:unset;
pointer-events: all;
opacity: 1.0;
}
#editor.read-only button.toastui-editor-toolbar-icons {
filter:saturate(0) !Important;
pointer-events: none;
opacity: 0.3;
}
#editor button.actions {
background-image: none;
margin: 0;
}
#editor.read-only button.actions {
display: none;
}
#editor.read-only #toggleButton {
display: block !important;
}
.toastui-editor-defaultUI-toolbar > div:nth-child(5) {
flex: auto;
justify-content: flex-end;
}
@@ -1,295 +0,0 @@
var editor = document.querySelector("#editor");
var toolBar = null;
var contents = null;
var pageContent = null;
var saveButton = null;
var editorUI = null;
var VIEW_URI = null;;
APIv1 = {
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);
})();
},
};
isReadOnly = function() {
if (editor) {
return editor.className == 'read-only';
}
return undefined;
}
setReadOnly = function() {
pageContent.innerHTML = editorUI.getHTML();
editorUI.changeMode('wysiwig');
editorUI.blur();
editor.classList.add('read-only');
pageContent.contentEditable = false;
}
setEditable = function() {
editor.classList.remove('read-only');
pageContent.contentEditable = true;
editorUI.moveCursorToStart();
editorUI.focus();
}
makeMarkdownButton = function() {
button = document.createElement('button');
button.classList.add("toastui-editor-toolbar-icons");
button.classList.add("last");
button.ariaLabel = "Toggle Markdown";
button.style.backgroundImage = 'none';
button.innerHTML = 'MD';
button.style.margin = '0';
button.addEventListener('click', () => {
if (editorUI.isMarkdownMode()) {
editorUI.changeMode("wysiwig");
} else {
editorUI.changeMode("markdown");
}
});
return button;
};
makeSaveButton = function() {
const button = document.createElement('button');
button.className = 'actions';
button.innerHTML = 'save';
button.id = 'saveButton';
button.style.border = "1px solid black";
button.addEventListener('click', () => {
APIv1.put({
'body': editorUI.getMarkdown()
}, (res) => {
if (res['code'] == 200) {
button.style.border = "1px solid green";
} else {
button.style.border = "1px solid red";
}
});
});
saveButton = button;
return button;
};
toggleButton = function() {
const button = document.createElement('button');
button.className = 'actions';
button.id = 'toggleButton';
button.innerHTML = 'edit';
button.addEventListener('click', () => {
if (isReadOnly()) {
setEditable();
} else {
setReadOnly();
}
});
return button;
}
handleContentChange = function() {
}
autoComplete = function(search_string, matches, callback) {
id = `_ac_${search_string}`;
el = document.getElementById(id);
var addEl = false;
if (!el) {
el = document.createElement("ul");
el.id = id;
addEl = true;
}
el.className = 'autocomplete';
el.innerHTML = "";
el.addEventListener("keyup", function(e) {
// do navigation / selection
});
matches.forEach(match => {
var text = match.uri;
for (pos = 0; pos < match.uri.length - search_string.length; pos++) {
var substr = match.name.substring(pos, search_string.length);
if (substr.toLowerCase() == search_string.toLowerCase()) {
text = match.name.substring(0, pos) + "<strong>" + substr + "</strong>" + match.name.substr(pos + substr.length);
break;
}
}
var option = document.createElement("li");
option.innerHTML = text;
option.addEventListener("click", function(e) {
// do selection
});
el.appendChild(option);
});
if (addEl) {
var selection = window.getSelection();
if (selection.rangeCount > 0) {
var range = selection.getRangeAt(0);
range.insertNode(el);
}
}
}
initialize = function(base_uri) {
const macro_rule = /@(\S{3,})/;
VIEW_URI = base_uri;
const ed = new toastui.Editor({
el: editor,
initialEditType: 'wysiwyg',
initialValue: "",
hideModeSwitch : true,
previewStyle: 'vertical',
usageStatistics: false,
autofocus: false,
toolbarItems: [
['heading', 'bold', 'italic' ],
['ul', 'ol', 'indent', 'outdent'],
['table', 'image', 'link'],
[
{ el: makeMarkdownButton(), tooltip: 'Toggle MD' },
],
[
{ el: makeSaveButton(), command: 'save', tooltip: 'Save Changes' },
{ el: toggleButton(), tooltip: 'Toggle Edit Mode' }
],
],
widgetRules: [ ],
/*
{
rule: macro_rule, toDOM(text) {
const matched = text.match(macro_rule);
const search_string = matched[1];
var replacement = "";
});
console.log(replacement);
return replacement;
},
},
],
*/
events: {
'loadUI': function(e) {
editorUI = e;
pageContent = document.querySelector(".toastui-editor-ww-container > div > div");
toolBar = document.querySelector('.toastui-editor-toolbar');
e.setMarkdown(document.getElementById("data_form__body").value);
setReadOnly();
},
'changeMode': function() {
if (editor && Array(editor.classList).includes('read-only')) {
setReadOnly();
}
},
'change': handleContentChange,
}
});
var searchPos = null;
ed.on('keyup', (editorType, ev) => {
const [start, end] = editorUI.getSelection();
console.log(start, end);
if (ev.key === '@') {
searchPos = start;
console.log(`Setting search position to ${searchPos}`);
return;
}
if (searchPos === null) {
return;
}
var range = window.getSelection().getRangeAt(0);
range.selectNodeContents(editor);
range.setStart(editor, 0);
range.setEnd(editor, end);
var search_string = range.toString();
console.log(search_string);
/*
APIv1.search("", search_string, (res) => {
if (res.code == 404) {
return;
}
const matches = res.response;
if (matches.length == 1) {
replacement = document.createElement('span');
replacement.innerHTML = `<a class="tooltip-preview" data-uri="${matches[0].uri}" href="${VIEW_URI}${matches[0].uri}">${matches[0].name}</a>`;
return;
}
autoComplete(search_string, matches, (selection) => {
console.log(`Selected ${selection}`);
document.remove(options.id);
});
};
*/
});
return ed;
};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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;})();
-195
View File
@@ -1,195 +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);
background: var(--body-background);
height: inherit;
width: 100%;
margin: 0px;
box-sizing: border-box;
overflow-wrap: break-word;
overflow-y: auto;
scrollbar-gutter: stable;
}
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 {
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 {
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 {
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 {
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 {
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 {
height: 100%;
align-content: center;
margin: auto;
}
ul {
list-style: none;
padding-left: 0;
margin:0;
}
nav {
display: block;
margin: 0px;
padding: 0px;
height: var(--nav-height);
background: var(--blue);
}
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 {
color: var(--nav-color);
display: inline-block;
}
nav > ul > li:hover > a {
color: var(--nav-hover-color);
}
.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);
}
.content {
position: relative;
display: table;
width: calc(100% - (2 * var(--content-padding)));
padding: var(--content-padding);
background: var(--content-background);
border: var(--content-border);
border-radius: var(--content-border-radius);
}
main {
display: table-row;
height: var(--main-height);
}
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;
}
+6 -5
View File
@@ -18,23 +18,24 @@
--breadcrumbs-height: 50px; --breadcrumbs-height: 50px;
--footer-height: 50px; --footer-height: 50px;
--footer-spacing: var(--breadcrumbs-height); --footer-spacing: var(--breadcrumbs-height);
--toolbar-height: 0px;
--toolbar-spacing: 0px;
--masthead-spacing: 0px;
--max-width: 1024px; --max-width: 1024px;
--min-width: calc(710px + (2 * var(--content-padding))); --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));
--main-height: calc(100vh - var(--nav-height) - 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-radius: calc(0.5 * var(--content-padding));
--content-border: 1px solid transparent;
/* colors */ /* colors */
--blue: #0ca0d6; --blue: #0ca0d6;
--white: #FFF; --white: #FFF;
--body-background: #EEE; --body-background: #EEE;
--nav-color: #000055; --nav-color: #000055;
--nav-hover-color: #FFF; --nav-hover-color: #FFF;
--content-background: #FFF; --content-background: #FFF;
--footer-background-color: #DDD; --footer-background-color: #DDD;
} }
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,9 +0,0 @@
@import 'toastui-editor-viewer.min.css';
#viewer {
display: inline;
}
.toastui-editor-contents {
font-size: var(--default-font-size);
}
@@ -1,6 +0,0 @@
var viewer = new toastui.Editor({
viewer: true,
el: document.querySelector("#viewer"),
usageStatistics: false,
});
viewer.setMarkdown(document.getElementById("data_form__body").value);
+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>
+38 -18
View File
@@ -1,10 +1,12 @@
import json import json
import re import re
from urllib.parse import unquote
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for 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 from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
@@ -21,6 +23,8 @@ def get_page(
except RecordNotFoundError as e: except RecordNotFoundError as e:
if not create_okay: if not create_okay:
return None, e return None, e
except ValidationError as e:
return None, e
return page, None return page, None
@@ -63,8 +67,12 @@ def breadcrumbs():
def before_request(): def before_request():
g.messages = [] g.messages = []
if not request.path.startswith("/static"): if not request.path.startswith("/static"):
user_id = session.get("user_id", 1) user_id = session.get("user_id")
g.user = app.db.User.get(doc_id=user_id, recurse=False) 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_id"] = user_id
session["user"] = dict(g.user.serialize()) session["user"] = dict(g.user.serialize())
@@ -109,11 +117,19 @@ 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):
clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
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) page, error = get_page(request.path, table=table, create_okay=True)
if error: if error:
app.log.error(error)
g.messages.append(str(error)) g.messages.append(str(error))
return rendered(page) return rendered(page)
@@ -121,24 +137,24 @@ def view(table, path):
@app.web.route(f"{app.config.API_URI}/put/<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.API_URI}/put/<path:path>", methods=["POST"], defaults={"table": "Page"}) @app.web.route(f"{app.config.API_URI}/put/<path:path>", methods=["POST"], defaults={"table": "Page"})
def put(table, path): def put(table, path):
app.log.debug(f"Checking for page at {table}/{path} in {table} space") clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
clean_path = re.sub(r"[^a-zA-Z0-9]", "", unquote(path))
if clean_table != table or clean_path != clean_path:
return redirect(url_for("put", table=clean_table, path=clean_path), 302)
page, error = get_page("/".join([table, path]), table=table, create_okay=True) page, error = get_page("/".join([table, path]), table=table, create_okay=True)
app.log.debug(f"Found {page.doc_id}")
if error: if error:
return api_response(error=error) return api_response(error=error)
params = json.loads(request.data.decode())["body"] page.update(**json.loads(request.data.decode())["body"])
save_data = getattr(forms, table)(page, params).prepare() updated = app.db.save(page)
app.log.debug("Saving form data...")
app.log.debug(f"{save_data=}")
doc = app.db.save(save_data)
app.log.debug(f"Saved {dict(doc)}")
if not page.doc_id: if not page.doc_id:
print(f"Adding {doc.doc_id} to {page.parent.members}") parent = page.parent()
page.parent.members = list(set(page.parent.members + [doc])) if parent:
app.db.save(page.parent) parent.update(members=list(set(parent.members + [updated])))
app.log.debug(f"Saved: {dict(doc)=}") app.db.save(parent)
return api_response(response=dict(doc)) app.log.debug(f"Saved page at uri {updated.uri}")
return api_response(response=dict(updated))
@app.web.route(f"{app.config.API_URI}/search/<string:space>", methods=["POST"]) @app.web.route(f"{app.config.API_URI}/search/<string:space>", methods=["POST"])
@@ -163,6 +179,10 @@ def search(space):
@app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"]) @app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
def get(table, doc_id): def get(table, doc_id):
clean_table = re.sub(r"[^a-zA-Z0-9]", "", unquote(table))
if clean_table != table:
return redirect(url_for("get", table=clean_table, doc_id=doc_id), 302)
app.log.debug(f"API: getting {table}({doc_id})") app.log.debug(f"API: getting {table}({doc_id})")
page, error = get_page(g.user, table=table, doc_id=doc_id) page, error = get_page(g.user, table=table, doc_id=doc_id)
return api_response(response=dict(page), error=error) return api_response(response=dict(page), error=error)
+3 -4
View File
@@ -1,3 +1,4 @@
from pathlib import Path
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
import pytest import pytest
@@ -12,7 +13,7 @@ from ttfrog import schema
@pytest.fixture @pytest.fixture
def app(): def app():
with TemporaryDirectory() as path: with TemporaryDirectory() as path:
fixture_db = GrungDB.with_schema(schema, path=path, storage=MemoryStorage) 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
@@ -21,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)
@@ -32,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?"
@@ -42,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