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