Compare commits
37
Commits
0ce3845a13
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3869a3abdd | ||
|
|
256c8d7090 | ||
|
|
ce8042759e | ||
|
|
d28b31ed14 | ||
|
|
2d07a5c5f6 | ||
|
|
3d66587678 | ||
|
|
39a1b07c07 | ||
|
|
705cf9e8ba | ||
|
|
1bcaff892b | ||
|
|
ff78d55b8e | ||
|
|
995e75d3e2 | ||
|
|
27cf36c390 | ||
|
|
f21bdbdb0c | ||
|
|
c540073b66 | ||
|
|
ce2c65cce1 | ||
|
|
fff34948d2 | ||
|
|
68cfd0e7f6 | ||
|
|
f6efbeb54a | ||
|
|
5fffbf59f5 | ||
|
|
6afab2a15c | ||
|
|
8fd28cf8b1 | ||
|
|
51795c8f7c | ||
|
|
fb391f9f4a | ||
|
|
83527b85f4 | ||
|
|
c1272788f0 | ||
|
|
8eb505f09e | ||
|
|
bb3070aa1a | ||
|
|
b395940d2d | ||
|
|
2064890ea5 | ||
|
|
6224d5fea4 | ||
|
|
582fd2d9a1 | ||
|
|
0e8fd9a1b0 | ||
|
|
36006ceeea | ||
|
|
3854a877bf | ||
|
|
c9927656ce | ||
|
|
120449386a | ||
|
|
8ce642ba90 |
@@ -16,6 +16,7 @@ downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
!lib/ribbit
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "lib/ribbit"]
|
||||
path = lib/ribbit
|
||||
url = https://git.evilchi.li/evilchili/ribbit.git
|
||||
Executable
+28
@@ -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."
|
||||
Executable
+5
@@ -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
@@ -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"
|
||||
+3
-2
@@ -17,8 +17,9 @@ flask = "*"
|
||||
tinydb = "^4.8.2"
|
||||
pyyaml = "^6.0.2"
|
||||
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"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "*"
|
||||
|
||||
+133
-21
@@ -5,12 +5,18 @@ from types import SimpleNamespace
|
||||
|
||||
from dotenv import dotenv_values
|
||||
from flask import Flask
|
||||
from flask_session import Session
|
||||
from grung.db import GrungDB
|
||||
from grung.exceptions import UniqueConstraintError
|
||||
from tinydb import where
|
||||
from tinydb.storages import MemoryStorage
|
||||
|
||||
from ttfrog import schema
|
||||
from ttfrog.exceptions import ApplicationNotInitializedError
|
||||
from ttfrog.exceptions import (
|
||||
ApplicationNotInitializedError,
|
||||
MalformedRequestError,
|
||||
RecordNotFoundError,
|
||||
UnauthorizedError,
|
||||
)
|
||||
|
||||
|
||||
class ApplicationContext:
|
||||
@@ -40,10 +46,13 @@ IN_MEMORY_DB=
|
||||
DATA_ROOT=~/.dnd/ttfrog/
|
||||
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_EMAIL=
|
||||
ADMIN_EMAIL=admin@telisar
|
||||
|
||||
THEME=default
|
||||
|
||||
VIEW_URI=/
|
||||
API_URI=/_/v1/
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
@@ -76,6 +85,7 @@ THEME=default
|
||||
config=config_file,
|
||||
data_root=data_root,
|
||||
database=data_root / f"{self.config.NAME}.json",
|
||||
sessions=data_root / "session_cache",
|
||||
)
|
||||
|
||||
def initialize(self, db: GrungDB = None, force: bool = False) -> None:
|
||||
@@ -83,15 +93,28 @@ THEME=default
|
||||
Instantiate both the database and the flask application.
|
||||
"""
|
||||
if force or not self._initialized:
|
||||
if self.config.IN_MEMORY_DB:
|
||||
self.db = GrungDB.with_schema(schema, storage=MemoryStorage)
|
||||
if db:
|
||||
self.db = db
|
||||
elif self.config.IN_MEMORY_DB:
|
||||
self.db = GrungDB.with_schema(schema, path=self.path.database, storage=MemoryStorage)
|
||||
else:
|
||||
self.db = GrungDB.with_schema(schema, self.path.database)
|
||||
self.db = GrungDB.with_schema(
|
||||
schema, path=self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
|
||||
)
|
||||
|
||||
self.theme = Path(__file__).parent / 'themes' / "default"
|
||||
self.theme = Path(__file__).parent / "themes" / self.config.THEME
|
||||
|
||||
self.web = Flask(self.config.NAME, template_folder=self.theme)
|
||||
self.web = Flask(self.config.NAME, template_folder=self.theme, static_folder=self.theme / "static")
|
||||
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
|
||||
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
|
||||
self.web.config["DEBUG"] = True
|
||||
self.web.config["SESSION_TYPE"] = "filesystem"
|
||||
self.web.config["SESSION_REFRESH_EACH_REQUEST"] = True
|
||||
self.web.config["SESSION_FILE_DIR"] = self.path.sessions
|
||||
|
||||
Session(self.web)
|
||||
|
||||
self.log = self.web.logger
|
||||
|
||||
self._initialized = True
|
||||
|
||||
@@ -99,24 +122,113 @@ THEME=default
|
||||
if not self._initialized:
|
||||
raise ApplicationNotInitializedError("This action requires the application to be initialized.")
|
||||
|
||||
def bootstrap(self):
|
||||
def authenticate(self, username: str, password: str) -> schema.User:
|
||||
"""
|
||||
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
|
||||
Returns the User record matching the given username and password
|
||||
"""
|
||||
self.check_state()
|
||||
if not (username and password):
|
||||
self.log.debug("Need both username and password to login")
|
||||
return None
|
||||
|
||||
user = self.db.User.get(where("name") == username)
|
||||
if not user:
|
||||
self.log.debug(f"No user matching {username}")
|
||||
return None
|
||||
|
||||
if not user.check_credentials(username, password):
|
||||
self.log.debug(f"Invalid credentials for {username}")
|
||||
return None
|
||||
|
||||
return user
|
||||
|
||||
def authorize(self, user, record, requested):
|
||||
return user.has_permission(record, requested)
|
||||
|
||||
def _get_or_create_page_by_uri(self, user, table, uri):
|
||||
"""
|
||||
Get a page by URI. If it doesn't exist, create a new one if and only if the user has permission
|
||||
to write on its parent.
|
||||
"""
|
||||
uri = uri.replace(" ", "").strip("/")
|
||||
if uri.startswith(self.config.VIEW_URI):
|
||||
uri = uri.replace(self.config.VIEW_URI, "", 1)
|
||||
|
||||
parent_uri = None
|
||||
search_uri = uri
|
||||
page_name = uri
|
||||
|
||||
if "/" in uri:
|
||||
(parent_uri, page_name) = uri.rsplit("/", 1)
|
||||
if parent_uri == "Page":
|
||||
parent_uri = None
|
||||
search_uri = page_name
|
||||
else:
|
||||
search_uri = uri
|
||||
|
||||
self.log.debug(f"Looking for page with uri {search_uri}")
|
||||
page = table.get(where("uri") == search_uri, recurse=False)
|
||||
if not page:
|
||||
# load the parent to check for write permissions
|
||||
parent_table = table if parent_uri and "/" in parent_uri else self.db.Page
|
||||
parent = None
|
||||
try:
|
||||
self.db.save(schema.Page(parent_id=None, stub="", title="_", body=""))
|
||||
except UniqueConstraintError:
|
||||
pass
|
||||
try:
|
||||
admin = self.db.save(schema.User(name=self.config.ADMIN_USERNAME, email=self.config.ADMIN_EMAIL))
|
||||
except UniqueConstraintError:
|
||||
pass
|
||||
# self.log.debug(f"Loading parent with {parent_uri}")
|
||||
parent = self.get_page(user, parent_table.name, uri=parent_uri)
|
||||
except Exception as e:
|
||||
self.log.debug(f"Error loading parent: {e}")
|
||||
|
||||
if not parent:
|
||||
raise MalformedRequestError(f"Page with uri '{search_uri}' does not exist and neither does its parent.")
|
||||
if not self.authorize(user, parent, schema.Permissions.WRITE):
|
||||
raise UnauthorizedError(f"User {user.doc_id} does not have permission to create under {parent_uri}.")
|
||||
|
||||
obj = getattr(schema, table.name)
|
||||
page = obj(name=page_name, body=obj.default.format(name=page_name), parent=parent, uri=search_uri)
|
||||
|
||||
# validate the page name before we try to create anything
|
||||
page._metadata.fields["name"].validate(page, db=self.db)
|
||||
|
||||
# self.log.debug(f"Returning {page.doc_id}: {page.uri}")
|
||||
return page
|
||||
|
||||
def get_page(self, user, table_name, doc_id=None, uri=None):
|
||||
"""
|
||||
Get a page by doc_id or by URI, if and only if the user is allowed to read it. A new Record
|
||||
instance will be returned if the requested page does not exist but the user has permission
|
||||
to create it.
|
||||
"""
|
||||
|
||||
if not user.doc_id:
|
||||
self.log.error(f"Invalid user: {user}")
|
||||
raise MalformedRequestError("User does not exist.")
|
||||
|
||||
try:
|
||||
self.db.save(schema.Group(name="admins", users=[admin]))
|
||||
except UniqueConstraintError:
|
||||
pass
|
||||
table = self.db.table(table_name)
|
||||
except RuntimeError:
|
||||
table = self.db.Page
|
||||
self.log.error(f"Invalid table_name: {table_name}, will use Page")
|
||||
# raise MalformedRequestError(f"{table_name} table does not exist.")
|
||||
|
||||
if doc_id:
|
||||
page = table.get(doc_id=doc_id)
|
||||
if not page:
|
||||
raise RecordNotFoundError(f"No record with {doc_id=} was found.")
|
||||
|
||||
page = self._get_or_create_page_by_uri(user, table, uri)
|
||||
|
||||
if not self.authorize(user, page, schema.Permissions.READ):
|
||||
self.log.error(f"No permission for {user.name} on {page}")
|
||||
raise UnauthorizedError(f"User {user.doc_id} does not have permission to read {table_name} {page.doc_id}.")
|
||||
|
||||
# resolve the pointers to subpages so we can render things like nav elements.
|
||||
if hasattr(page, "members"):
|
||||
subpages = []
|
||||
for pointer in page.members:
|
||||
table, pkey, pval = pointer.split("::")
|
||||
subpages += self.db.table(table).search(where(pkey) == pval, recurse=False)
|
||||
page.members = subpages
|
||||
|
||||
return page
|
||||
|
||||
|
||||
sys.modules[__name__] = ApplicationContext()
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
from ttfrog import app, schema
|
||||
|
||||
TEMPLATE = """
|
||||
# Wiki Syntax
|
||||
{{toc box depth="3"}}
|
||||
|
||||
## Basic Formatting
|
||||
|
||||
|
||||
The wiki uses [GitHub-flavored Markdown](https://github.github.com/gfm/).
|
||||
|
||||
{{style equal-widths striped
|
||||
|
||||
| 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>
|
||||
|
||||
}}} | {{{
|
||||
|
||||
<ul><li>unordered</li><li>list</li><li>items</li></ul>
|
||||
|
||||
}}} |
|
||||
| {{{
|
||||
|
||||
<pre>
|
||||
1. ordered
|
||||
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 {{{ {{{ and }}} }}}:
|
||||
|
||||
{{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 \\|
|
||||
\\| ------ \\| ------ \\|
|
||||
\\| \\{\\{\\{
|
||||
|
||||
multi-line
|
||||
content
|
||||
|
||||
}}} \\| 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():
|
||||
"""
|
||||
Bootstrap the database entries by poplating the first Page, the Admin user and the Admins group.
|
||||
"""
|
||||
app.check_state()
|
||||
|
||||
# the system user does not get added to the list of Users.
|
||||
app.db.save(schema.User(name="__system__"))
|
||||
|
||||
# create the top-level pages
|
||||
root = app.db.save(schema.Page(name=app.config.VIEW_URI, body="This is the home page", uri=""))
|
||||
|
||||
root.add_member(schema.Page(name="Wiki", body=TEMPLATE))
|
||||
|
||||
users = root.add_member(schema.Page(name="User", body="# Users\nusers go here."))
|
||||
groups = root.add_member(schema.Page(name="Group", body="# Groups\ngroups go here."))
|
||||
npcs = root.add_member(schema.Page(name="NPC", body="# NPCS!"))
|
||||
widgets = root.add_member(schema.Page(name="Widget", body="Widgets go here."))
|
||||
widgets.add_member(schema.Widget(name="hello", body=schema.Widget.default))
|
||||
|
||||
# create the NPCs
|
||||
npcs.add_member(schema.NPC(name="Sabetha", body=""))
|
||||
npcs.add_member(schema.NPC(name="John", body=""))
|
||||
|
||||
# create the users
|
||||
guest = users.add_member(schema.User(name="guest", body="# guest"))
|
||||
|
||||
admin = users.add_member(
|
||||
schema.User(name=app.config.ADMIN_USERNAME, password="fnord", email=app.config.ADMIN_EMAIL, body="# fnord")
|
||||
)
|
||||
|
||||
# create the admin user and admins group
|
||||
admins = groups.add_member(schema.Group(name="administrators", members=[admin], body="# administrators"))
|
||||
|
||||
# admins get full access
|
||||
root.set_permissions(
|
||||
admins, permissions=[schema.Permissions.READ, schema.Permissions.WRITE, schema.Permissions.DELETE]
|
||||
)
|
||||
|
||||
# guests get read access by default, except on Groups and Users
|
||||
groups.set_permissions(guest, permissions=[])
|
||||
users.set_permissions(guest, permissions=[])
|
||||
root.set_permissions(guest, permissions=[schema.Permissions.READ])
|
||||
+4
-1
@@ -9,6 +9,7 @@ from rich import print
|
||||
from rich.logging import RichHandler
|
||||
|
||||
import ttfrog.app
|
||||
from ttfrog.bootstrap import bootstrap
|
||||
|
||||
main_app = typer.Typer()
|
||||
|
||||
@@ -52,7 +53,8 @@ def init(context: typer.Context, drop: bool = typer.Option(False, help="Drop tab
|
||||
ttfrog.app.db.drop_tables()
|
||||
ttfrog.app.db.close()
|
||||
ttfrog.app.initialize(force=True)
|
||||
ttfrog.app.bootstrap()
|
||||
bootstrap()
|
||||
print(ttfrog.app.db.Page.all())
|
||||
print(ttfrog.app.db)
|
||||
|
||||
|
||||
@@ -62,6 +64,7 @@ def run(context: typer.Context):
|
||||
The default CLI entrypoint is ttfrog.cli.run().
|
||||
"""
|
||||
import ttfrog.web
|
||||
|
||||
ttfrog.app.web.run()
|
||||
|
||||
|
||||
|
||||
@@ -3,3 +3,21 @@ class ApplicationNotInitializedError(Exception):
|
||||
Thrown when attempting to access methods on the
|
||||
ApplicationContext before it has been initialized.
|
||||
"""
|
||||
|
||||
|
||||
class MalformedRequestError(Exception):
|
||||
"""
|
||||
Thrown when a request cannnot be completed due to bad arguments.
|
||||
"""
|
||||
|
||||
|
||||
class RecordNotFoundError(Exception):
|
||||
"""
|
||||
Thrown when the specified record could not be loaded by doc_id.
|
||||
"""
|
||||
|
||||
|
||||
class UnauthorizedError(Exception):
|
||||
"""
|
||||
Thrown when a user does not have permissino to do the requested action.
|
||||
"""
|
||||
|
||||
+382
-10
@@ -1,18 +1,390 @@
|
||||
from grung.types import Collection, Field, Record
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
from enum import StrEnum
|
||||
from functools import cached_property
|
||||
from textwrap import dedent
|
||||
from typing import List
|
||||
|
||||
from flask import g
|
||||
from grung.objects import (
|
||||
BackReference,
|
||||
Collection,
|
||||
DateTime,
|
||||
Dict,
|
||||
Password,
|
||||
Pointer,
|
||||
Record,
|
||||
String,
|
||||
TextFilePointer,
|
||||
Timestamp,
|
||||
)
|
||||
from grung.validators import LengthValidator, PatternValidator
|
||||
from tinydb import where
|
||||
|
||||
from ttfrog.exceptions import MalformedRequestError
|
||||
|
||||
|
||||
class User(Record):
|
||||
_fields = [Field("name"), Field("email", unique=True)]
|
||||
def app_context():
|
||||
import ttfrog.app
|
||||
|
||||
ttfrog.app.check_state()
|
||||
return ttfrog.app
|
||||
|
||||
|
||||
class Group(Record):
|
||||
_fields = [Field("name", unique=True), Collection("users", User)]
|
||||
READ_ONLY_FIELD_TYPES = [Collection, Pointer, BackReference, Timestamp]
|
||||
|
||||
|
||||
class Permissions(StrEnum):
|
||||
READ = "r"
|
||||
WRITE = "w"
|
||||
DELETE = "d"
|
||||
|
||||
|
||||
class Page(Record):
|
||||
_fields = [
|
||||
Field("parent_id"),
|
||||
Field("stub"),
|
||||
Field("title"),
|
||||
Field("body"),
|
||||
"""
|
||||
A page in the wiki. Just about everything in the databse is either a Page or a subclass of a Page.
|
||||
"""
|
||||
|
||||
default = dedent(
|
||||
"""
|
||||
# {name}
|
||||
|
||||
*Overview of this page*
|
||||
|
||||
|
||||
## Section 1
|
||||
|
||||
*Organize your text into logically separted sections.*
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
# fmt: off
|
||||
return [
|
||||
*super().fields(),
|
||||
|
||||
# The URI for the page, relative to the app's VIEW_URI
|
||||
String("uri", unique=True, validators=[
|
||||
PatternValidator(re.compile(
|
||||
r"""
|
||||
^ # 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"),
|
||||
]
|
||||
# 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):
|
||||
if self.uri == "":
|
||||
return None
|
||||
|
||||
app = app_context()
|
||||
|
||||
parent_uri = ""
|
||||
if self.uri and "/" in self.uri:
|
||||
parent_uri = self.uri.rsplit("/", 1)[0]
|
||||
for table_name in app.db.tables():
|
||||
page = app.db.table(table_name).get(where("uri") == parent_uri, recurse=False)
|
||||
if page:
|
||||
return page
|
||||
|
||||
def before_insert(self, *args, **kwargs):
|
||||
"""
|
||||
Make the following adjustments before saving this record:
|
||||
* Derive the URI from the hierarchy of the parent.
|
||||
"""
|
||||
if not self.name:
|
||||
raise Exception("Must provide a name")
|
||||
|
||||
app = app_context()
|
||||
|
||||
super().before_insert(app.db)
|
||||
|
||||
if not self.author:
|
||||
self.author = app.db.User.get(where("name") == "__system__")
|
||||
|
||||
now = datetime.utcnow()
|
||||
if not self.doc_id and self.created < now:
|
||||
self.created = now
|
||||
|
||||
def add_member(self, child: Record):
|
||||
app = app_context()
|
||||
|
||||
prefix = (self.uri + "/") if self.uri else ""
|
||||
new_uri = f"{prefix}{child.name}"
|
||||
if child.uri != new_uri:
|
||||
app.log.debug(f"Moving {child._metadata.table}[{child.doc_id}] from {child.uri} to {new_uri}")
|
||||
child.uri = new_uri
|
||||
self.members = list(set(self.members + [app.db.save(child)]))
|
||||
app.db.save(self)
|
||||
return self.get_child(child)
|
||||
|
||||
def get_child(self, obj: Record):
|
||||
for page in self.members:
|
||||
if page[page._metadata.primary_key] == obj[obj._metadata.primary_key]:
|
||||
return page
|
||||
return None
|
||||
|
||||
def set_permissions(self, entity: Entity, permissions: List) -> str:
|
||||
app = app_context()
|
||||
|
||||
perms = "".join(permissions)
|
||||
self.acl[entity.reference] = perms
|
||||
app.db.save(self)
|
||||
return perms
|
||||
|
||||
def get_acl_for_entity(self, entity) -> (str, str | None):
|
||||
"""
|
||||
Search upward through the page hierarchy looking for one with an ACL that either
|
||||
has a grant for the entity we care about, or at least one group in which the entity is a member.
|
||||
"""
|
||||
app = app_context()
|
||||
|
||||
def find_acl(obj):
|
||||
if hasattr(obj, "acl"):
|
||||
if entity.reference in obj.acl:
|
||||
return {entity.reference: obj.acl[entity.reference]}
|
||||
|
||||
group_grants = {}
|
||||
for ref, grant in obj.acl.items():
|
||||
(table_name, pkey, pval) = ref.split("::")
|
||||
if table_name == "Group":
|
||||
group = app.db.Group.get(where(pkey) == pval, recurse=False)
|
||||
if entity.reference in group.members:
|
||||
group_grants[ref] = grant
|
||||
if group_grants:
|
||||
return group_grants
|
||||
|
||||
if hasattr(obj, "parent"):
|
||||
return find_acl(obj.parent())
|
||||
return {"": ""}
|
||||
|
||||
return find_acl(self)
|
||||
|
||||
|
||||
class Entity(Page):
|
||||
@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),
|
||||
]
|
||||
|
||||
def has_permission(self, record: Record, requested: str) -> bool | None:
|
||||
for entity, grants in record.get_acl_for_entity(self).items():
|
||||
if requested in grants:
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_read(self, record: Record):
|
||||
return self.has_permission(record, Permissions.READ)
|
||||
|
||||
def can_write(self, record: Record):
|
||||
return self.has_permission(record, Permissions.WRITE)
|
||||
|
||||
def can_delete(self, record: Record):
|
||||
return self.has_permission(record, Permissions.DELETE)
|
||||
|
||||
|
||||
class User(Entity):
|
||||
"""
|
||||
A website user, editable as a wiki page.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return super().fields() + [String("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:
|
||||
return username == self.name and self._metadata.fields["password"].compare(password, self.password)
|
||||
|
||||
def after_insert(self, *args, **kwargs):
|
||||
"""
|
||||
After saving this record, ensure that any page in the members collection is updated with the
|
||||
correct URI. This ensures that if a page is moved from one collection to another, the URI is updated.
|
||||
"""
|
||||
app = app_context()
|
||||
|
||||
super().after_insert(app.db)
|
||||
if not hasattr(self, "members"):
|
||||
return
|
||||
for child in self.members:
|
||||
obj = BackReference.dereference(child, app.db, recurse=False)
|
||||
obj.uri = f"{self.uri}/{obj.name}"
|
||||
child = app.db.save(obj)
|
||||
|
||||
|
||||
class Group(Entity):
|
||||
"""
|
||||
A set of users, editable as a wiki page.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
return super().fields() + [Collection("members", member_type=Entity)]
|
||||
|
||||
|
||||
class NPC(Page):
|
||||
"""
|
||||
An NPC, editable as a wiki page.
|
||||
"""
|
||||
|
||||
default = dedent(
|
||||
"""
|
||||
# {name}
|
||||
*[Ancestry] [Class]*
|
||||
|
||||
| AC | HP | STR | DEX | CON | INT | WIS | CHA
|
||||
|----|----|-----|-----|-----|-----|-----|------
|
||||
| 10 | 10 | +0 | +0 | +0 | +0 | +0 | +0
|
||||
|
||||
**{name} (they/they)** [description]
|
||||
|
||||
* Personality: **[keywords]**
|
||||
* Flaw: **[flaw]**
|
||||
* Goal: **[goal]**
|
||||
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class Widget(Page):
|
||||
"""
|
||||
Wiki UX widgets
|
||||
"""
|
||||
|
||||
default = dedent(
|
||||
"""
|
||||
This is a sample widget that you can customize to your liking. Widget pages must contain at minimum the
|
||||
**Template** section. It, along with the optional **CSS** and **Processor** sections, will be parsed by
|
||||
wiki at display time. All other content on this page is ignored, so you can include usage docs, exmaples,
|
||||
and so on, just like this text and the annnotations below in *italics*.
|
||||
|
||||
The name of your widget is the portion of the URI following `/Widget/`.
|
||||
|
||||
# Hello, World Example Widget
|
||||
*Provide a description of your widget.*
|
||||
|
||||
Insert the word "HELLO" and optionally a name.
|
||||
|
||||
## Usage
|
||||
*Display the usage of your widget. Ensure that the example is enclosed in a preformatted (`pre`) block.*
|
||||
*Be sure to include a description of any parameters and keyword arguments.*
|
||||
|
||||
<pre>
|
||||
{{widget hello [NAME] }}
|
||||
</pre>
|
||||
|
||||
|
||||
## Example
|
||||
*Include one or more example uses of the widget. Once you save this page, you can refer to your widget directly.*
|
||||
|
||||
Here is what it looks like to say hello to the whole world: {{widget hello world}}!
|
||||
|
||||
|
||||
## Template
|
||||
*The template is javascript string that will be evaluated when the widget is loaded. Any keyword parameters*
|
||||
*you specify in the Usage section above will be available as variables, so you can use variable subsetitution*
|
||||
*in your template definition. The template must be enclose in a code block.*
|
||||
|
||||
```
|
||||
HELLO ${{token.keywords.split(" ").slice(1).join(" ") || ""}}
|
||||
```
|
||||
|
||||
## CSS
|
||||
*If you want to customize the styling of your widget, include CSS in this section. This CSS must be enclosed*
|
||||
*in a code block.*
|
||||
|
||||
```
|
||||
display: inline;
|
||||
background: green;
|
||||
padding: 3px;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
```
|
||||
|
||||
## Processor
|
||||
*If you want full control over how your widget is processed, you can override the default processor*
|
||||
*here. The processor function below is the default.*
|
||||
|
||||
```
|
||||
function(token, widget) {{
|
||||
/*
|
||||
* token The token object created by the wiki parser. token.keywords and
|
||||
* token.params contain the keyword and paramater arguments from
|
||||
* the wiki page source where the widget was used.
|
||||
* widget: The widget instance. widget.css, widget.template, and
|
||||
* widget.processor contain the definitions parsed from the
|
||||
* corresponding sections on this page.
|
||||
*/
|
||||
var ret = '';
|
||||
eval("ret = `" + widget.template + "`");
|
||||
return ret;
|
||||
}}
|
||||
```
|
||||
"""
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def fields(cls):
|
||||
inherited = [field for field in super().fields() if field.name not in ("members", "uid")]
|
||||
return inherited + [
|
||||
String("name", primary_key=True),
|
||||
]
|
||||
|
||||
@@ -1,34 +1,43 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
{% block head %}
|
||||
<meta charset="utf-8">
|
||||
<title>{% block title %}TTFROG{% endblock %}</title>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat.css' ) }}">
|
||||
{% block styles %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css' ) }}">
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="{{ url_for('index') }}">Home</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="alert">
|
||||
{{ message }}
|
||||
<form id='data_form' method='POST'>
|
||||
<input type="hidden" id="data_form__id" name="uid" value="{{ page.uid }}">
|
||||
<input name='title' id="data_form__title" type='text' value="{{ page.title }}">
|
||||
<textarea name="body" id="data_form__body">{{ page.body }}</textarea>
|
||||
</form>
|
||||
<div id='masthead'>
|
||||
{% block nav %}
|
||||
{% include "nav.html" %}
|
||||
{% include "breadcrumbs.html" %}
|
||||
{% endblock %}
|
||||
{% if user.can_write(page) %}
|
||||
{% include "toolbar.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class='main-aligned'>
|
||||
<main id='main'>
|
||||
{% for message in g.messages %}
|
||||
<dialog class="alert">
|
||||
{{ message }}
|
||||
</dialog>
|
||||
{% endfor %}
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
</div>
|
||||
<footer>
|
||||
{% block footer %}
|
||||
fnord
|
||||
{% endblock %}
|
||||
</footer>
|
||||
</footer>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<div id='breadcrumbs' class='content-aligned'>
|
||||
<a href="{{ url_for('index') }}">Home</a>{% for (uri, name) in breadcrumbs %}.<a href="{{ uri }}">{{ name }}</a>{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block menu %}
|
||||
{% for child in page.pages %}
|
||||
<li><a href="{{ app.config['VIEW_URI'] }}/{{ child.uri }}">{{ child.title }}</a></li>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Create: {{ page.title }}</h1>
|
||||
<h2>{{ page.uri }}</h2>
|
||||
{{ page.body }}
|
||||
|
||||
<pre>
|
||||
{{ page }}
|
||||
</pre>
|
||||
|
||||
<pre>
|
||||
{{ app.web.config }}
|
||||
</pre>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<form method='POST'>
|
||||
<h1>Login</h1>
|
||||
Username: <input type="text" name="username">
|
||||
<br>
|
||||
Password: <input type="password" name="password">
|
||||
<br>
|
||||
<input type="submit">
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
<nav id='pagenav'>
|
||||
<ul class="container content-aligned">
|
||||
<li><a href='{{ root.uri }}'>Home</a></li>
|
||||
{% for subpage in root.members %}
|
||||
{% if user.can_read(subpage) %}
|
||||
<li><a href="{{ url_for('view', path=subpage.uri) }}">{{ subpage.name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<li class='user'>
|
||||
{% if user.name == 'guest' %}
|
||||
Welcome, {{ user.name }}. [ <a href="{{ url_for('login') }}">Login</a> ]
|
||||
{% else %}
|
||||
Welcome, <a href="{{ url_for('view', path=user.uri) }}">{{ user.name }}</a>.
|
||||
[<a href="{{ url_for('logout') }}">Logout</a>]
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<menu>
|
||||
<ul class="container">
|
||||
{% for member in page.members %}
|
||||
{% if user.can_read(member) %}
|
||||
<li><a href="{{ app.config.VIEW_URI }}{{ member.uri }}">{{ member.name }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</menu>
|
||||
@@ -1,6 +1,27 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>{{ page.title }}</h1>
|
||||
{{ page.content }}
|
||||
{% block styles %}
|
||||
{% if user.can_write(page) %}
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='froghat-editor.css' ) }}">
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<article id='froghat'>{{ page.body }}</article>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
|
||||
{% 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) %}
|
||||
<script src="{{ url_for('static', filename='froghat-editor.js' ) }}"></script>
|
||||
{% endif %}
|
||||
<script>
|
||||
const wiki = new Froghat{% if user.can_write(page) %}Editor{% endif %}({plugins: [MacroPlugin]});
|
||||
wiki.run();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
|
||||
|
||||
:root {
|
||||
--toolbar-background: transparent;
|
||||
--toolbar-border-radius: var(--content-border-radius);
|
||||
--toolbar-enabled-background: var(--content-background);
|
||||
|
||||
--toolbar-height: 32px !important;
|
||||
--toolbar-spacing: 5px !important;
|
||||
--toolbar-button-size: 32px;
|
||||
--toolbar-button-enabled-background: rgba(128, 192, 128);
|
||||
--toolbar-button-active-background: rgba(192, 255, 192);
|
||||
--toolbar-button-enabled-border: 1px solid #000;
|
||||
|
||||
--toolbar-icon-bold: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M8.21 13c2.106 0 3.412-1.087 3.412-2.823 0-1.306-.984-2.283-2.324-2.386v-.055a2.176 2.176 0 0 0 1.852-2.14c0-1.51-1.162-2.46-3.014-2.46H3.843V13zM5.908 4.674h1.696c.963 0 1.517.451 1.517 1.244 0 .834-.629 1.32-1.73 1.32H5.908V4.673zm0 6.788V8.598h1.73c1.217 0 1.88.492 1.88 1.415 0 .943-.643 1.449-1.832 1.449H5.907z"/></svg>');
|
||||
--toolbar-icon-italic: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.991 11.674 9.53 4.455c.123-.595.246-.71 1.347-.807l.11-.52H7.211l-.11.52c1.06.096 1.128.212 1.005.807L6.57 11.674c-.123.595-.246.71-1.346.806l-.11.52h3.774l.11-.52c-1.06-.095-1.129-.211-1.006-.806z"/></svg>');
|
||||
/*
|
||||
--toolbar-icon-underline: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M5.313 3.136h-1.23V9.54c0 2.105 1.47 3.623 3.917 3.623s3.917-1.518 3.917-3.623V3.136h-1.23v6.323c0 1.49-.978 2.57-2.687 2.57s-2.687-1.08-2.687-2.57zM12.5 15h-9v-1h9z"/></svg>');
|
||||
*/
|
||||
--toolbar-icon-h1: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.648 13V3H6.3v4.234H1.348V3H0v10h1.348V8.421H6.3V13zM14 13V3h-1.333l-2.381 1.766V6.12L12.6 4.443h.066V13z"/></svg>');
|
||||
--toolbar-icon-h2: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13zm3.174-7.071v-.05c0-.934.66-1.752 1.801-1.752 1.005 0 1.76.639 1.76 1.651 0 .898-.582 1.58-1.12 2.19l-3.69 4.2V13h6.331v-1.149h-4.458v-.079L13.9 8.786c.919-1.048 1.666-1.874 1.666-3.101C15.565 4.149 14.35 3 12.499 3 10.46 3 9.384 4.393 9.384 5.879v.05z"/></svg>');
|
||||
--toolbar-icon-h3: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M11.07 8.4h1.049c1.174 0 1.99.69 2.004 1.724s-.802 1.786-2.068 1.779c-1.11-.007-1.905-.605-1.99-1.357h-1.21C8.926 11.91 10.116 13 12.028 13c1.99 0 3.439-1.188 3.404-2.87-.028-1.553-1.287-2.221-2.096-2.313v-.07c.724-.127 1.814-.935 1.772-2.293-.035-1.392-1.21-2.468-3.038-2.454-1.927.007-2.94 1.196-2.981 2.426h1.23c.064-.71.732-1.336 1.744-1.336 1.027 0 1.744.64 1.744 1.568.007.95-.738 1.639-1.744 1.639h-.991V8.4ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
|
||||
--toolbar-icon-h4: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M13.007 3H15v10h-1.29v-2.051H8.854v-1.18C10.1 7.513 11.586 5.256 13.007 3m-2.82 6.777h3.524v-5.62h-.074a95 95 0 0 0-3.45 5.554zM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
|
||||
--toolbar-icon-h5: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M9 10.516h1.264c.193.976 1.112 1.364 2.01 1.364 1.005 0 2.067-.782 2.067-2.247 0-1.292-.983-2.082-2.089-2.082-1.012 0-1.658.596-1.924 1.077h-1.12L9.646 3h5.535v1.141h-4.415L10.5 7.28h.072c.201-.316.883-.84 1.967-.84 1.709 0 3.13 1.177 3.13 3.158 0 2.025-1.407 3.403-3.475 3.403-1.809 0-3.1-1.048-3.194-2.484ZM7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.512h4.854V13z"/></svg>');
|
||||
--toolbar-icon-h6: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M15.596 5.178H14.3c-.106-.444-.62-1.072-1.706-1.072-1.332 0-2.325 1.269-2.325 3.947h.07c.268-.67 1.043-1.445 2.445-1.445 1.494 0 3.017 1.064 3.017 3.073C15.8 11.795 14.37 13 12.48 13c-1.036 0-2.093-.36-2.77-1.452C9.276 10.836 9 9.808 9 8.37 9 4.656 10.494 3 12.636 3c1.812 0 2.883 1.113 2.96 2.178m-5.151 4.566c0 1.367.944 2.15 2.043 2.15 1.128 0 2.037-.684 2.037-2.136 0-1.41-1-2.065-2.03-2.065-1.19 0-2.05.853-2.05 2.051M7.495 13V3.201H6.174v4.15H1.32V3.2H0V13h1.32V8.513h4.854V13z"/></svg>');
|
||||
--toolbar-icon-unordered_list: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m-3 1a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2m0 4a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>');
|
||||
--toolbar-icon-ordered_list: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 11.5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5"/><path d="M1.713 11.865v-.474H2c.217 0 .363-.137.363-.317 0-.185-.158-.31-.361-.31-.223 0-.367.152-.373.31h-.59c.016-.467.373-.787.986-.787.588-.002.954.291.957.703a.595.595 0 0 1-.492.594v.033a.615.615 0 0 1 .569.631c.003.533-.502.8-1.051.8-.656 0-1-.37-1.008-.794h.582c.008.178.186.306.422.309.254 0 .424-.145.422-.35-.002-.195-.155-.348-.414-.348h-.3zm-.004-4.699h-.604v-.035c0-.408.295-.844.958-.844.583 0 .96.326.96.756 0 .389-.257.617-.476.848l-.537.572v.03h1.054V9H1.143v-.395l.957-.99c.138-.142.293-.304.293-.508 0-.18-.147-.32-.342-.32a.33.33 0 0 0-.342.338zM2.564 5h-.635V2.924h-.031l-.598.42v-.567l.629-.443h.635z"/></svg>');
|
||||
--toolbar-icon-line: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 8a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 0 1h-11A.5.5 0 0 1 2 8"/></svg>');
|
||||
--toolbar-icon-quote: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M2.5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1zm5 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm0 3a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1zm-5 3a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1zm.79-5.373q.168-.117.444-.275L3.524 6q-.183.111-.452.287-.27.176-.51.428a2.4 2.4 0 0 0-.398.562Q2 7.587 2 7.969q0 .54.217.873.217.328.72.328.322 0 .504-.211a.7.7 0 0 0 .188-.463q0-.345-.211-.521-.205-.182-.568-.182h-.282q.036-.305.123-.498a1.4 1.4 0 0 1 .252-.37 2 2 0 0 1 .346-.298zm2.167 0q.17-.117.445-.275L5.692 6q-.183.111-.452.287-.27.176-.51.428a2.4 2.4 0 0 0-.398.562q-.165.31-.164.692 0 .54.217.873.217.328.72.328.322 0 .504-.211a.7.7 0 0 0 .188-.463q0-.345-.211-.521-.205-.182-.568-.182h-.282a1.8 1.8 0 0 1 .118-.492q.087-.194.257-.375a2 2 0 0 1 .346-.3z"/></svg>');
|
||||
--toolbar-icon-link: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M4.715 6.542 3.343 7.914a3 3 0 1 0 4.243 4.243l1.828-1.829A3 3 0 0 0 8.586 5.5L8 6.086a1 1 0 0 0-.154.199 2 2 0 0 1 .861 3.337L6.88 11.45a2 2 0 1 1-2.83-2.83l.793-.792a4 4 0 0 1-.128-1.287z"/><path d="M6.586 4.672A3 3 0 0 0 7.414 9.5l.775-.776a2 2 0 0 1-.896-3.346L9.12 3.55a2 2 0 1 1 2.83 2.83l-.793.792c.112.42.155.855.128 1.287l1.372-1.372a3 3 0 1 0-4.243-4.243z"/></svg>');
|
||||
--toolbar-icon-table: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2zm15 2h-4v3h4zm0 4h-4v3h4zm0 4h-4v3h3a1 1 0 0 0 1-1zm-5 3v-3H6v3zm-5 0v-3H1v2a1 1 0 0 0 1 1zm-4-4h4V8H1zm0-4h4V4H1zm5-3v3h4V4zm4 4H6v3h4z"/></svg>');
|
||||
--toolbar-icon-macro: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M2.114 8.063V7.9c1.005-.102 1.497-.615 1.497-1.6V4.503c0-1.094.39-1.538 1.354-1.538h.273V2h-.376C3.25 2 2.49 2.759 2.49 4.352v1.524c0 1.094-.376 1.456-1.49 1.456v1.299c1.114 0 1.49.362 1.49 1.456v1.524c0 1.593.759 2.352 2.372 2.352h.376v-.964h-.273c-.964 0-1.354-.444-1.354-1.538V9.663c0-.984-.492-1.497-1.497-1.6M13.886 7.9v.163c-1.005.103-1.497.616-1.497 1.6v1.798c0 1.094-.39 1.538-1.354 1.538h-.273v.964h.376c1.613 0 2.372-.759 2.372-2.352v-1.524c0-1.094.376-1.456 1.49-1.456V7.332c-1.114 0-1.49-.362-1.49-1.456V4.352C13.51 2.759 12.75 2 11.138 2h-.376v.964h.273c.964 0 1.354.444 1.354 1.538V6.3c0 .984.492 1.497 1.497 1.6"/></svg>');
|
||||
--toolbar-icon-macro_user: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M11 6a3 3 0 1 1-6 0 3 3 0 0 1 6 0"/><path fill-rule="evenodd" d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8m8-7a7 7 0 0 0-5.468 11.37C3.242 11.226 4.805 10 8 10s4.757 1.225 5.468 2.37A7 7 0 0 0 8 1"/></svg>');
|
||||
--toolbar-icon-macro_toc: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M14.5 3a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-13a.5.5 0 0 1-.5-.5v-9a.5.5 0 0 1 .5-.5zm-13-1A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2z"/><path d="M5 8a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7A.5.5 0 0 1 5 8m0-2.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m0 5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5m-1-5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0M4 8a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0m0 2.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0"/></svg>');
|
||||
--toolbar-icon-macro_style: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 16 16"><path d="M8 5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3m4 3a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3M5.5 7a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0m.5 6a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3"/><path d="M16 8c0 3.15-1.866 2.585-3.567 2.07C11.42 9.763 10.465 9.473 10 10c-.603.683-.475 1.819-.351 2.92C9.826 14.495 9.996 16 8 16a8 8 0 1 1 8-8m-8 7c.611 0 .654-.171.655-.176.078-.146.124-.464.07-1.119-.014-.168-.037-.37-.061-.591-.052-.464-.112-1.005-.118-1.462-.01-.707.083-1.61.704-2.314.369-.417.845-.578 1.272-.618.404-.038.812.026 1.16.104.343.077.702.186 1.025.284l.028.008c.346.105.658.199.953.266.653.148.904.083.991.024C14.717 9.38 15 9.161 15 8a7 7 0 1 0-7 7"/></svg>');
|
||||
|
||||
--toolbar-icon-markdown: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16"><path d="M10.478 1.647a.5.5 0 1 0-.956-.294l-4 13a.5.5 0 0 0 .956.294zM4.854 4.146a.5.5 0 0 1 0 .708L1.707 8l3.147 3.146a.5.5 0 0 1-.708.708l-3.5-3.5a.5.5 0 0 1 0-.708l3.5-3.5a.5.5 0 0 1 .708 0m6.292 0a.5.5 0 0 0 0 .708L14.293 8l-3.147 3.146a.5.5 0 0 0 .708.708l3.5-3.5a.5.5 0 0 0 0-.708l-3.5-3.5a.5.5 0 0 0-.708 0"/></svg>');
|
||||
--toolbar-icon-save: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M4.406 1.342A5.53 5.53 0 0 1 8 0c2.69 0 4.923 2 5.166 4.579C14.758 4.804 16 6.137 16 7.773 16 9.569 14.502 11 12.687 11H10a.5.5 0 0 1 0-1h2.688C13.979 10 15 8.988 15 7.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 2.825 10.328 1 8 1a4.53 4.53 0 0 0-2.941 1.1c-.757.652-1.153 1.438-1.153 2.055v.448l-.445.049C2.064 4.805 1 5.952 1 7.318 1 8.785 2.23 10 3.781 10H6a.5.5 0 0 1 0 1H3.781C1.708 11 0 9.366 0 7.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383"/><path fill-rule="evenodd" d="M7.646 4.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 5.707V14.5a.5.5 0 0 1-1 0V5.707L5.354 7.854a.5.5 0 1 1-.708-.708z"/></svg>');
|
||||
|
||||
--toolbar-icon-toggle: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16"><path d="m22.81,9.08c.807-.642,1.25-1.642,1.183-2.676-.067-1.05-.646-2.001-1.547-2.546-.772-.466-1.946-.892-3.574-1.297-.411-1.476-1.767-2.562-3.372-2.562-1.493,0-2.77.94-3.272,2.259C3.12,6.628.56,12.888.025,17.398c-.195,1.646.332,3.311,1.448,4.567,1.149,1.293,2.799,2.035,4.527,2.035h9c.552,0,1-.448,1-1s-.448-1-1-1h-2v-3c0-1.428.193-3.121.398-4.513l5.464,8.587c.369.579,1,.925,1.687.925h1.451c.552,0,1-.448,1-1s-.448-1-1-1h-1.451s-6.342-9.966-6.342-9.966c.116-.058.245-.095.383-.108,3.276-.294,6.349-1.358,8.22-2.846Zm-7.31-7.08c.827,0,1.5.673,1.5,1.5s-.673,1.5-1.5,1.5-1.5-.673-1.5-1.5.673-1.5,1.5-1.5Zm-3.783,10.367c-.143.797-.297,1.747-.426,2.745-1.189-1.297-2.897-2.112-4.791-2.112h-.5c-.552,0-1,.448-1,1s.448,1,1,1h.5c2.481,0,4.5,2.019,4.5,4.5v2.5h-5c-1.157,0-2.263-.497-3.032-1.363-.747-.841-1.087-1.908-.957-3.003.647-5.452,4.145-9.959,10.144-13.101.442,1.427,1.774,2.467,3.344,2.467,1.541,0,2.853-1.001,3.319-2.387,1.191.315,2.061.636,2.594.958.346.209.56.56.585.962.025.386-.133.744-.433.982-1.857,1.477-4.913,2.218-7.154,2.419-1.351.122-2.459,1.123-2.694,2.433Z" /></svg>');
|
||||
|
||||
}
|
||||
|
||||
#froghat[contenteditable] {
|
||||
outline: 0px solid transparent;
|
||||
}
|
||||
|
||||
#froghat {
|
||||
box-sizing: border-box;
|
||||
height: -webkit-fill-available;
|
||||
}
|
||||
|
||||
#froghat.edit {
|
||||
font-family: monospace;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
#froghat.wysiwyg {
|
||||
span {
|
||||
display: inline;
|
||||
border-bottom: 1px solid green;
|
||||
}
|
||||
}
|
||||
|
||||
main.editing {
|
||||
border: 1px solid green;
|
||||
border-top: 1px solid transparent;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
#toolbar {
|
||||
background: var(--toolbar-background);
|
||||
border-radius: var(--toolbar-border-radius);
|
||||
height: var(--toolbar-height);
|
||||
border: 1px solid transparent;
|
||||
margin: 0;
|
||||
margin-bottom: 0px;
|
||||
padding: var(--toolbar-spacing);
|
||||
|
||||
ul {
|
||||
display: flex;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
border-radius: 5px;
|
||||
|
||||
li {
|
||||
padding: 0px;
|
||||
margin: 2px;
|
||||
text-align: center;
|
||||
line-height: var(--toolbar-button-size);
|
||||
width: var(--toolbar-button-size);
|
||||
height: var(--toolbar-button-size);
|
||||
|
||||
button {
|
||||
opacity: 0.3;
|
||||
display: block;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: local;
|
||||
background-position: center;
|
||||
background-size: 1.5rem 1.5rem;
|
||||
border-radius: 5px;
|
||||
border: 1px solid transparent;
|
||||
width: var(--toolbar-button-size);
|
||||
height: var(--toolbar-button-size);
|
||||
}
|
||||
|
||||
button.enabled {
|
||||
opacity: 1.0;
|
||||
cursor: pointer;
|
||||
}
|
||||
button.on {
|
||||
border: var(--toolbar-button-enabled-border);
|
||||
background-color: var(--toolbar-button-enabled-background);
|
||||
}
|
||||
button:hover {
|
||||
border: var(--toolbar-button-enabled-border);
|
||||
background-color: var(--toolbar-button-active-background);
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
position-area: bottom;
|
||||
margin: 0;
|
||||
border: 0;
|
||||
width: var(--toolbar-button-size);
|
||||
min-height: var(--toolbar-button-size);
|
||||
}
|
||||
|
||||
|
||||
#bold { background-image: var(--toolbar-icon-bold); }
|
||||
#italic { background-image: var(--toolbar-icon-italic); }
|
||||
#underline { background-image: var(--toolbar-icon-underline); }
|
||||
|
||||
#header { background-image: var(--toolbar-icon-h1); anchor-name: "header"; }
|
||||
#h1 { background-image: var(--toolbar-icon-h1); }
|
||||
#h2 { background-image: var(--toolbar-icon-h2); }
|
||||
#h3 { background-image: var(--toolbar-icon-h3); }
|
||||
#h4 { background-image: var(--toolbar-icon-h4); }
|
||||
#h5 { background-image: var(--toolbar-icon-h5); }
|
||||
#h6 { background-image: var(--toolbar-icon-h6); }
|
||||
|
||||
#list { background-image: var(--toolbar-icon-unordered_list); anchor-name: "list"; }
|
||||
#unordered_list { background-image: var(--toolbar-icon-unordered_list); }
|
||||
#ordered_list { background-image: var(--toolbar-icon-ordered_list); }
|
||||
|
||||
|
||||
#line { background-image: var(--toolbar-icon-line); }
|
||||
#quote { background-image: var(--toolbar-icon-quote); }
|
||||
#link { background-image: var(--toolbar-icon-link); }
|
||||
#table { background-image: var(--toolbar-icon-table); }
|
||||
|
||||
#macro { background-image: var(--toolbar-icon-macro); anchor-name: "macro"}
|
||||
#macro_user { background-image: var(--toolbar-icon-macro_user); }
|
||||
#macro_toc { background-image: var(--toolbar-icon-macro_toc); }
|
||||
#macro_style { background-image: var(--toolbar-icon-macro_style); }
|
||||
|
||||
#markdown { background-image: var(--toolbar-icon-markdown); }
|
||||
#save { background-image: var(--toolbar-icon-save); }
|
||||
|
||||
#toggle {
|
||||
background-color: var(--toolbar-button-enabled-background);
|
||||
background-image: var(--toolbar-icon-toggle);
|
||||
-moz-transform: scaleX(-1);
|
||||
-o-transform: scaleX(-1);
|
||||
-webkit-transform: scaleX(-1);
|
||||
transform: scaleX(-1);
|
||||
filter: FlipH;
|
||||
-ms-filter: "FlipH";
|
||||
opacity: 1.0;
|
||||
cursor: pointer;
|
||||
}
|
||||
#toggle:hover {
|
||||
border: var(--toolbar-button-enabled-border);
|
||||
background-color: var(--toolbar-button-active-background);
|
||||
}
|
||||
|
||||
#header-menu { position-anchor: "header"; }
|
||||
#list-menu { position-anchor: "list"; }
|
||||
}
|
||||
|
||||
li:last-child {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#toolbar.enabled {
|
||||
background: var(--toolbar-enabled-background);
|
||||
border-radius: 0px;
|
||||
border: 1px solid green;
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
function reEscape(string) {
|
||||
return string.replaceAll('*', '\\*');
|
||||
}
|
||||
|
||||
class ToolbarButton {
|
||||
|
||||
constructor(settings) {
|
||||
this.id = settings.id;
|
||||
this.element = settings.element || document.getElementById(this.id);
|
||||
this.toolbar = settings.toolbar;
|
||||
this.isMenu = settings.isMenu || false;
|
||||
this.open = settings.open || '';
|
||||
this.close = settings.close || '';
|
||||
this.tag = settings.tag || null;
|
||||
this.onclick = settings.onclick;
|
||||
|
||||
|
||||
if (!this.isMenu) {
|
||||
if (this.open) {
|
||||
|
||||
var open = reEscape(this.open);
|
||||
var close = this.close ? reEscape(this.close) : '';
|
||||
var leading = '(?<leading>^(?:(?!' + open + ').)*)';
|
||||
var middle = '(?<middle>(?:(?!' + open;
|
||||
if (this.close && this.close != this.open) {
|
||||
middle += '|' + close + ').)+)';
|
||||
} else {
|
||||
middle += ').)*)';
|
||||
}
|
||||
this.pattern = RegExp(
|
||||
leading +
|
||||
'(?<open>' + open + ')' +
|
||||
middle +
|
||||
(close ? '(?<closed>(?:' + close + ')(\\s.*?|$))' : '') +
|
||||
'$'
|
||||
);
|
||||
console.log(this.id, this.pattern);
|
||||
}
|
||||
|
||||
this.element.addEventListener('click', (e) => {
|
||||
if (this.element.enabled) {
|
||||
if (this.onclick) {
|
||||
this.onclick({clickEvent: e, button: this });
|
||||
} else if (this.toolbar.editor.isWysiwyg()) {
|
||||
this.#applyHTMLFormatting();
|
||||
} else if (this.toolbar.editor.isMarkdown()) {
|
||||
this.#applyMarkdownFormatting();
|
||||
}
|
||||
}
|
||||
document.querySelectorAll(".dropdown-menu:popover-open").forEach(el => {
|
||||
el.hidePopover();
|
||||
});
|
||||
this.toolbar.editor.element.focus();
|
||||
});
|
||||
}
|
||||
|
||||
if (settings.enabled) {
|
||||
this.enable();
|
||||
}
|
||||
}
|
||||
|
||||
#applyHTMLFormatting() {
|
||||
if (!this.tag) {
|
||||
return;
|
||||
}
|
||||
var selection = window.getSelection();
|
||||
var range = selection.getRangeAt(0);
|
||||
var node = document.createElement(this.tag);
|
||||
try {
|
||||
range.surroundContents(node);
|
||||
range.setStartAfter(node);
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
this.toolbar.editor.moveCursorAfter(node);
|
||||
}
|
||||
|
||||
#applyMarkdownFormatting() {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
var selection = window.getSelection();
|
||||
var range = selection.getRangeAt(0);
|
||||
var node = document.createTextNode(this.open + range.toString() + this.close);
|
||||
range.deleteContents();
|
||||
range.insertNode(node);
|
||||
range.setStartAfter(node);
|
||||
this.toolbar.editor.moveCursorAfter(node);
|
||||
}
|
||||
|
||||
click() {
|
||||
if (this.element.enabled) {
|
||||
this.element.click();
|
||||
}
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.element.enabled = true;
|
||||
this.element.classList.add('enabled');
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.element.enabled = false;
|
||||
this.element.classList.remove('enabled');
|
||||
}
|
||||
|
||||
on() {
|
||||
this.element.classList.add('on');
|
||||
}
|
||||
|
||||
off() {
|
||||
this.element.classList.remove('on');
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.element.classList.toggle('on');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class FroghatToolbar {
|
||||
constructor(settings) {
|
||||
this.editor = settings.editor;
|
||||
this.element = settings.toolbar || document.getElementById('toolbar');
|
||||
this.currentContext = null;
|
||||
this.buttons = {
|
||||
'line': new ToolbarButton({ toolbar: this, id: 'line', open: '***', close: '', tag: 'HR'}),
|
||||
'bold': new ToolbarButton({ toolbar: this, id: 'bold', open: '**', close: '**', tag: 'STRONG'}),
|
||||
'italic': new ToolbarButton({ toolbar: this, id: 'italic', open: '*', close: '*', tag: 'EM'}),
|
||||
'header': new ToolbarButton({ toolbar: this, id: 'header', isMenu: true}),
|
||||
'h1': new ToolbarButton({ toolbar: this, id: 'h1', markdown: '# ', close: "", tag: 'H1' }),
|
||||
'h2': new ToolbarButton({ toolbar: this, id: 'h2', markdown: '## ', close: "", tag: 'H2' }),
|
||||
'h3': new ToolbarButton({ toolbar: this, id: 'h3', markdown: '### ', close: "", tag: 'H3' }),
|
||||
'h4': new ToolbarButton({ toolbar: this, id: 'h4', markdown: '#### ', close: "", tag: 'H4' }),
|
||||
'h5': new ToolbarButton({ toolbar: this, id: 'h5', markdown: '##### ', close: "", tag: 'H5' }),
|
||||
'h6': new ToolbarButton({ toolbar: this, id: 'h6', markdown: '###### ', close: "", tag: 'H6' }),
|
||||
'list': new ToolbarButton({ toolbar: this, id: 'list', isMenu: true}),
|
||||
'unordered_list': new ToolbarButton({ toolbar: this, id: 'unordered_list', }),
|
||||
'ordered_list': new ToolbarButton({ toolbar: this, id: 'ordered_list', }),
|
||||
'link': new ToolbarButton({ toolbar: this, id: 'link', }),
|
||||
'quote': new ToolbarButton({ toolbar: this, id: 'quote', }),
|
||||
'table': new ToolbarButton({ toolbar: this, id: 'table', }),
|
||||
'macro': new ToolbarButton({ toolbar: this, id: 'macro', isMenu: true}),
|
||||
'macro_user': new ToolbarButton({ toolbar: this, id: 'macro_user', }),
|
||||
'macro_toc': new ToolbarButton({ toolbar: this, id: 'macro_toc', }),
|
||||
'macro_style': new ToolbarButton({ toolbar: this, id: 'macro_style', }),
|
||||
'save': new ToolbarButton({ toolbar: this, id: 'save', onclick: this.#click_save }),
|
||||
'markdown': new ToolbarButton({ toolbar: this, id: 'markdown', onclick: this.#click_markdown }),
|
||||
'toggle': new ToolbarButton({ toolbar: this, id: 'toggle', onclick: this.#click_toggle, enabled: true })
|
||||
}
|
||||
}
|
||||
|
||||
getContext() {
|
||||
var context = null;
|
||||
var node = window.getSelection().baseNode;
|
||||
console.log({node});
|
||||
Object.values(this.buttons).forEach(button => {
|
||||
if (button.pattern) {
|
||||
var closed = false;
|
||||
var leading = "";
|
||||
var matched = false;
|
||||
matched = node.textContent.match(button.pattern);
|
||||
if (matched && matched.groups) {
|
||||
closed = matched.groups.closed;
|
||||
leading = matched.groups.leading;
|
||||
}
|
||||
if (closed && matched.groups.middle) {
|
||||
context = {
|
||||
button: button,
|
||||
closed: closed,
|
||||
leading: leading,
|
||||
node: node,
|
||||
element: null
|
||||
}
|
||||
}
|
||||
if (node.parentElement && node.parentElement.nodeName == button.tag) {
|
||||
if (!context) {
|
||||
context = {
|
||||
button: button,
|
||||
element: null
|
||||
};
|
||||
}
|
||||
context.element = node.parentElement
|
||||
}
|
||||
if (context) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
#click_save({clickEvent, button}) {
|
||||
}
|
||||
|
||||
#click_markdown({clickEvent, button}) {
|
||||
button.toolbar.editor.toggleMarkdown();
|
||||
}
|
||||
|
||||
#click_toggle({clickEvent, button}) {
|
||||
button.toolbar.editor.toggleView();
|
||||
}
|
||||
|
||||
enable() {
|
||||
this.element.classList.add("enabled");
|
||||
Object.values(this.buttons).forEach(button => { button.enable() });
|
||||
}
|
||||
|
||||
disable() {
|
||||
this.element.classList.remove("enabled");
|
||||
Object.values(this.buttons).forEach(button => { button.disable() });
|
||||
this.buttons.toggle.enable();
|
||||
}
|
||||
}
|
||||
|
||||
class FroghatEditor extends Froghat {
|
||||
|
||||
run() {
|
||||
this.states = {
|
||||
VIEW: 'view',
|
||||
EDIT: 'edit',
|
||||
WYSIWYG: 'wysiwyg'
|
||||
}
|
||||
|
||||
this.toolbar = new FroghatToolbar({editor: this});
|
||||
|
||||
this.#bindEvents();
|
||||
|
||||
this.plugins().forEach(plugin => { plugin.setEditable() });
|
||||
this.element.classList.add("loaded");
|
||||
this.view();
|
||||
}
|
||||
|
||||
#replaceWysiwygNode(context) {
|
||||
var offset = context.leading.length || 0;
|
||||
var slice = context.node.textContent.slice(offset);
|
||||
|
||||
var html = this.markdownToHTML(slice);
|
||||
html = html.replace("<p>", "").replace("</p>", "");
|
||||
|
||||
var el = document.createElement("span");
|
||||
el.innerHTML = context.leading + html;
|
||||
console.log(el.innerHTML);
|
||||
if (el.innerHTML != context.node.innerHTML) {
|
||||
context.node.replaceWith(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
#refreshMarkdownCache() {
|
||||
if (this.cachedMarkdown != this.element.textContent) {
|
||||
this.changed = true;
|
||||
this.toolbar.buttons.save.enable();
|
||||
this.cachedMarkdown = this.element.textContent;
|
||||
}
|
||||
}
|
||||
|
||||
#handleEditorChanges(evt) {
|
||||
var context = this.toolbar.getContext();
|
||||
if (!context) {
|
||||
if (this.toolbar.currentContext) {
|
||||
this.toolbar.currentContext.button.off();
|
||||
this.toolbar.currentContext = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.toolbar.currentContext) {
|
||||
this.toolbar.currentContext.button.off();
|
||||
}
|
||||
|
||||
context.button.on();
|
||||
this.toolbar.currentContext = context;
|
||||
|
||||
if (this.isWysiwyg()) {
|
||||
if (context.closed) {
|
||||
context.node = this.#replaceWysiwygNode(context);
|
||||
context.button.off();
|
||||
this.toolbar.currentContext = null;
|
||||
this.toolbar.editor.moveCursorAfter(context.node);
|
||||
}
|
||||
} else if (context.button) {
|
||||
context.button.on();
|
||||
}
|
||||
}
|
||||
|
||||
#bindEvents() {
|
||||
this.element.addEventListener('keydown', (evt) => {
|
||||
if (! this.isEditing()) {
|
||||
return;
|
||||
}
|
||||
this.#refreshMarkdownCache();
|
||||
this.#handleEditorChanges(evt);
|
||||
});
|
||||
};
|
||||
|
||||
toggleMarkdown() {
|
||||
if (this.getState() === this.states.EDIT) {
|
||||
this.wysiwyg();
|
||||
} else {
|
||||
this.edit();
|
||||
}
|
||||
}
|
||||
|
||||
toggleView() {
|
||||
if (this.getState() === this.states.VIEW) {
|
||||
this.toolbar.enable();
|
||||
this.wysiwyg();
|
||||
this.element.focus();
|
||||
} else {
|
||||
this.toolbar.disable();
|
||||
this.view();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
htmlToMarkdown(html) {
|
||||
return hopdown.toMarkdown(html || this.element.innerHTML);
|
||||
}
|
||||
|
||||
getMarkdown() {
|
||||
/*
|
||||
* Return the current markdown.
|
||||
*/
|
||||
if (this.getState() === this.states.EDIT) {
|
||||
var html = this.element.innerHTML;
|
||||
html = html.replaceAll(/<(?:div|br)>/ig, '');
|
||||
html = html.replaceAll(/<\/div>/ig, "\n");
|
||||
this.cachedMarkdown = decodeHtmlEntities(html);
|
||||
} else if (this.getState() === this.states.WYSIWYG) {
|
||||
this.cachedMarkdown = this.htmlToMarkdown(this.element.innerHTML);
|
||||
} if (!this.cachedMarkdown) {
|
||||
this.cachedMarkdown = this.element.textContent;
|
||||
}
|
||||
return this.cachedMarkdown;
|
||||
}
|
||||
|
||||
wysiwyg() {
|
||||
/*
|
||||
* Put the editor in WYSIWYG editing mode.
|
||||
*/
|
||||
if (this.getState() === this.states.WYSIWYG) {
|
||||
return;
|
||||
}
|
||||
this.element.contentEditable = true;
|
||||
this.element.innerHTML = this.getHTML();
|
||||
Array.from(this.element.querySelectorAll('.macro')).forEach(el => {
|
||||
if (el.dataset.editable == "false") {
|
||||
el.contentEditable = false;
|
||||
el.style.opacity = 0.5;
|
||||
}
|
||||
});
|
||||
this.setState(this.states.WYSIWYG);
|
||||
this.toolbar.buttons.markdown.off();
|
||||
this.toolbar.buttons.markdown.enable();
|
||||
document.getElementById("main").classList.add("editing");
|
||||
}
|
||||
|
||||
edit() {
|
||||
/*
|
||||
* Put the editor into source editing mode.
|
||||
*/
|
||||
if (this.state === this.states.EDIT) {
|
||||
return;
|
||||
}
|
||||
this.element.contentEditable = true;
|
||||
this.element.innerHTML = encodeHtmlEntities(this.getMarkdown());
|
||||
this.setState(this.states.EDIT);
|
||||
this.toolbar.buttons.markdown.on();
|
||||
document.getElementById("main").classList.add("editing");
|
||||
}
|
||||
|
||||
moveCursorAfter(node) {
|
||||
var dummyElement = null;
|
||||
if (!node.nextElementSibling) {
|
||||
// workaround for https://issues.chromium.org/issues/41239578
|
||||
var dummyElement = document.createElement('a');
|
||||
dummyElement.innerHTML="​";
|
||||
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 {
|
||||
}
|
||||
|
||||
|
||||
@@ -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("{{{", "{{{");
|
||||
content = content.replaceAll("}}}", "}}}");
|
||||
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;
|
||||
}
|
||||
@@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
}
|
||||
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
@@ -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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""")}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;})();
|
||||
@@ -0,0 +1,41 @@
|
||||
:root {
|
||||
/* Typography */
|
||||
--default-font-family: sans-serif;
|
||||
--default-font-size: 16px;
|
||||
--header-font: sans-serif;
|
||||
--header-weight: 700;
|
||||
--h1-size: 32px;
|
||||
--h2-size: 24px;
|
||||
--h3-size: 18.72px;
|
||||
--h4-size: 16px;
|
||||
--h5-size: 13.28px;
|
||||
--h6-size: 10.72px;
|
||||
|
||||
/* Layout */
|
||||
--content-padding: 20px;
|
||||
--nav-height: 50px;
|
||||
--nav-spacing: 15px;
|
||||
--breadcrumbs-height: 50px;
|
||||
--footer-height: 50px;
|
||||
--footer-spacing: var(--breadcrumbs-height);
|
||||
--toolbar-height: 0px;
|
||||
--toolbar-spacing: 0px;
|
||||
--masthead-spacing: 0px;
|
||||
|
||||
--max-width: 1024px;
|
||||
--min-width: calc(710px + (2 * var(--content-padding)));
|
||||
--main-height: calc(100vh - var(--nav-height) - var(--masthead-spacing) - var(--toolbar-height) - (3 * var(--toolbar-spacing)) - var(--footer-height) - var(--breadcrumbs-height) - calc(2 * var(--content-padding)) - var(--footer-spacing));
|
||||
|
||||
--content-border-radius: calc(0.5 * var(--content-padding));
|
||||
--content-border: 1px solid transparent;
|
||||
|
||||
|
||||
/* colors */
|
||||
--blue: #0ca0d6;
|
||||
--white: #FFF;
|
||||
--body-background: #EEE;
|
||||
--nav-color: #000055;
|
||||
--nav-hover-color: #FFF;
|
||||
--content-background: #FFF;
|
||||
--footer-background-color: #DDD;
|
||||
}
|
||||
@@ -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>
|
||||
+180
-14
@@ -1,22 +1,188 @@
|
||||
from ttfrog import app
|
||||
from flask import Response, render_template
|
||||
import json
|
||||
import re
|
||||
from urllib.parse import unquote
|
||||
|
||||
from flask import Response, g, jsonify, redirect, render_template, request, session, url_for
|
||||
from grung.exceptions import ValidationError
|
||||
from tinydb import where
|
||||
import logging
|
||||
|
||||
from ttfrog import app, schema
|
||||
from ttfrog.exceptions import MalformedRequestError, RecordNotFoundError, UnauthorizedError
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
def get_page(
|
||||
path: str, table: str = "Page", doc_id: int = None, create_okay: bool = False
|
||||
) -> (schema.Record | None, Exception | None):
|
||||
"""
|
||||
Get one page, including its members, but not recursively.
|
||||
"""
|
||||
try:
|
||||
page = app.get_page(g.user, table, doc_id=doc_id, uri=path)
|
||||
except (UnauthorizedError, MalformedRequestError) as e:
|
||||
return None, e
|
||||
except RecordNotFoundError as e:
|
||||
if not create_okay:
|
||||
return None, e
|
||||
except ValidationError as e:
|
||||
return None, e
|
||||
return page, None
|
||||
|
||||
|
||||
@app.web.route("/")
|
||||
def index():
|
||||
page = app.db.Page.search(where('stub') == "")
|
||||
return render_template("page.html", page=page[0])
|
||||
def api_response(response={}, messages=[], error=None):
|
||||
response_code = 200
|
||||
|
||||
if error:
|
||||
response_code = 500
|
||||
response = {}
|
||||
if isinstance(error, UnauthorizedError):
|
||||
response_code = 403
|
||||
elif isinstance(error, MalformedRequestError):
|
||||
response_code = 4000
|
||||
elif isinstance(error, RecordNotFoundError):
|
||||
response_code = 404
|
||||
|
||||
return jsonify({"messages": messages, "response": response, "code": response_code}), response_code
|
||||
|
||||
|
||||
@app.web.route("/<stub>")
|
||||
def page_view(stub):
|
||||
page = app.db.Page.search(where('stub') == stub)
|
||||
def rendered(page: schema.Record, template: str = "page.html"):
|
||||
if not page:
|
||||
logger.info(f"No page found for {stub = }")
|
||||
return Response(f"{stub}: not found", status=404)
|
||||
return render_template("page.html", page=page[0])
|
||||
return Response("Page not found", status=404)
|
||||
|
||||
root = page if page.uri == "" else get_page("")[0]
|
||||
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), root=root, user=g.user, g=g)
|
||||
|
||||
|
||||
def breadcrumbs():
|
||||
"""
|
||||
Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request.
|
||||
"""
|
||||
uri = ""
|
||||
names = (request.path.replace(app.config.VIEW_URI, "", 1).strip("/") or "/").split("/")
|
||||
for name in names:
|
||||
uri = "/".join([uri, name])
|
||||
yield (uri, name)
|
||||
|
||||
|
||||
@app.web.before_request
|
||||
def before_request():
|
||||
g.messages = []
|
||||
if not request.path.startswith("/static"):
|
||||
user_id = session.get("user_id")
|
||||
g.user = (
|
||||
app.db.User.get(doc_id=user_id, recurse=False)
|
||||
if user_id
|
||||
else app.db.User.get(where("name") == "guest", recurse=False)
|
||||
)
|
||||
session["user_id"] = user_id
|
||||
session["user"] = dict(g.user.serialize())
|
||||
|
||||
|
||||
@app.web.after_request
|
||||
def add_header(r):
|
||||
r.headers["Cache-Control"] = "no-cache, no-store, must-revalidate, public, max-age=0"
|
||||
r.headers["Pragma"] = "no-cache"
|
||||
r.headers["Expires"] = "0"
|
||||
return r
|
||||
|
||||
|
||||
@app.web.route(app.config.VIEW_URI)
|
||||
def index():
|
||||
page, error = get_page("")
|
||||
if error:
|
||||
g.messages.append(str(error))
|
||||
return rendered(page)
|
||||
|
||||
|
||||
@app.web.route("/login", methods=["GET", "POST"])
|
||||
def login():
|
||||
app.web.session_interface.regenerate(session)
|
||||
if request.method == "POST":
|
||||
username = request.form.get("username")
|
||||
password = request.form.get("password")
|
||||
user = app.authenticate(username, password)
|
||||
if user:
|
||||
g.user = user
|
||||
session["user_id"] = g.user.doc_id
|
||||
session["user"] = dict(g.user.serialize())
|
||||
return redirect(url_for("index"))
|
||||
g.messages.append(f"Invalid login for {username}")
|
||||
return rendered(schema.Page(name="Login"), "login.html")
|
||||
|
||||
|
||||
@app.web.route("/logout")
|
||||
def logout():
|
||||
if "user_id" in session:
|
||||
del session["user_id"]
|
||||
del g.user
|
||||
return redirect(url_for("index"))
|
||||
|
||||
|
||||
@app.web.route(f"{app.config.VIEW_URI}/<string:table>/<string:path>", methods=["GET"])
|
||||
@app.web.route(f"{app.config.VIEW_URI}/<string:path>", methods=["GET"], defaults={"table": "Page"})
|
||||
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)
|
||||
if error:
|
||||
app.log.error(error)
|
||||
g.messages.append(str(error))
|
||||
return rendered(page)
|
||||
|
||||
|
||||
@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"})
|
||||
def put(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:
|
||||
return redirect(url_for("put", table=clean_table, path=clean_path), 302)
|
||||
|
||||
page, error = get_page("/".join([table, path]), table=table, create_okay=True)
|
||||
if error:
|
||||
return api_response(error=error)
|
||||
|
||||
page.update(**json.loads(request.data.decode())["body"])
|
||||
updated = app.db.save(page)
|
||||
if not page.doc_id:
|
||||
parent = page.parent()
|
||||
if parent:
|
||||
parent.update(members=list(set(parent.members + [updated])))
|
||||
app.db.save(parent)
|
||||
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/", methods=["POST"], defaults={"space": None})
|
||||
def search(space):
|
||||
spaces = app.db.tables()
|
||||
if space:
|
||||
spaces = [space.lower().capitalize()]
|
||||
|
||||
query = json.loads(request.data.decode())["body"]
|
||||
app.log.debug(f"Searching for records matching query {query}")
|
||||
matches = []
|
||||
for space in spaces:
|
||||
for page in app.db.table(space).search(where("name").matches(query, re.IGNORECASE), recurse=False):
|
||||
if app.authorize(g.user, page, schema.Permissions.READ):
|
||||
app.log.debug(f"Adding search result {dict(page)}")
|
||||
matches.append(dict(page))
|
||||
return api_response(
|
||||
response=matches, error=None if matches else RecordNotFoundError(f"No records matching '{query}'")
|
||||
)
|
||||
|
||||
|
||||
@app.web.route(f"{app.config.API_URI}/get/<path:table>/<int:doc_id>", methods=["GET"])
|
||||
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})")
|
||||
page, error = get_page(g.user, table=table, doc_id=doc_id)
|
||||
return api_response(response=dict(page), error=error)
|
||||
|
||||
+73
-10
@@ -1,4 +1,10 @@
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
import pytest
|
||||
from grung.db import GrungDB
|
||||
from tinydb import where
|
||||
from tinydb.storages import MemoryStorage
|
||||
|
||||
import ttfrog.app
|
||||
from ttfrog import schema
|
||||
@@ -6,16 +12,16 @@ from ttfrog import schema
|
||||
|
||||
@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()
|
||||
ttfrog.app.initialize(db=fixture_db, force=True)
|
||||
yield ttfrog.app
|
||||
ttfrog.app.db.close()
|
||||
ttfrog.app.db.truncate()
|
||||
|
||||
|
||||
def test_create(app):
|
||||
user = schema.User(name="john", email="john@foo")
|
||||
assert user.uid
|
||||
assert user._metadata.fields["uid"].unique
|
||||
user = schema.User(name="john", email="john@foo", password="powerfulCat")
|
||||
|
||||
# insert
|
||||
john_something = app.db.save(user)
|
||||
@@ -25,7 +31,6 @@ def test_create(app):
|
||||
assert app.db.User.get(doc_id=last_insert_id) == john_something
|
||||
assert john_something.name == user.name
|
||||
assert john_something.email == user.email
|
||||
assert john_something.uid == user.uid
|
||||
|
||||
# update
|
||||
john_something.name = "james?"
|
||||
@@ -34,7 +39,65 @@ def test_create(app):
|
||||
assert after_update == john_something
|
||||
assert before_update != after_update
|
||||
|
||||
players = schema.Group(name="players", users=[john_something])
|
||||
players = app.db.save(players)
|
||||
players.users[0]["name"] = "fnord"
|
||||
app.db.save(players)
|
||||
|
||||
@pytest.mark.xfail
|
||||
def test_permissions(app):
|
||||
john = app.db.save(schema.User(name="john", email="john@foo", password="powerfulCat"))
|
||||
players = app.db.save(schema.Group(name="players", members=[john]))
|
||||
notes = app.db.save(schema.Page(name="notes"))
|
||||
note0 = notes.add_member(schema.Page(name="note0"))
|
||||
|
||||
# default no access
|
||||
assert not players.can_read(notes)
|
||||
assert not players.can_write(notes)
|
||||
assert not players.can_delete(notes)
|
||||
assert not john.can_read(notes)
|
||||
assert not john.can_write(notes)
|
||||
assert not john.can_delete(notes)
|
||||
assert not players.can_read(note0)
|
||||
assert not john.can_read(note0)
|
||||
|
||||
# set to rw, no delete
|
||||
notes.set_permissions(players, [schema.Permissions.READ, schema.Permissions.WRITE])
|
||||
notes = app.db.Page.get(doc_id=notes.doc_id)
|
||||
assert players.can_read(notes)
|
||||
assert players.can_write(notes)
|
||||
assert not players.can_delete(notes)
|
||||
|
||||
# propagated
|
||||
note0 = app.db.Page.get(doc_id=note0.doc_id)
|
||||
assert players.can_read(note0)
|
||||
assert players.can_write(note0)
|
||||
assert not players.can_delete(note0)
|
||||
assert not john.can_delete(note0)
|
||||
|
||||
# members of the group inherit group permissions
|
||||
assert john.can_read(notes)
|
||||
assert john.can_read(note0)
|
||||
assert john.can_write(notes)
|
||||
assert john.can_write(note0)
|
||||
|
||||
# permissions are the union of user + group permissions
|
||||
notes.set_permissions(john, [schema.Permissions.DELETE])
|
||||
notes = app.db.Page.get(doc_id=notes.doc_id)
|
||||
note0 = app.db.Page.get(doc_id=note0.doc_id)
|
||||
assert not players.can_delete(notes)
|
||||
assert not players.can_delete(note0)
|
||||
assert john.can_delete(notes)
|
||||
assert john.can_delete(note0)
|
||||
|
||||
# user perms always override inherited permissions
|
||||
note0.set_permissions(john, [])
|
||||
note0 = app.db.Page.get(doc_id=note0.doc_id)
|
||||
assert not john.can_read(note0)
|
||||
assert players.can_read(note0)
|
||||
|
||||
|
||||
def test_bootstrap(app):
|
||||
from ttfrog.bootstrap import bootstrap
|
||||
|
||||
bootstrap()
|
||||
|
||||
admins = app.db.Group.get(where("name") == "administrators")
|
||||
admin = app.db.User.get(where("name") == "admin")
|
||||
assert admin.reference in admins.members
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user