Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 71c5915628 | |||
| 17ae6a036e | |||
| 6c88272e58 | |||
| 0dd24e2ba4 | |||
| a6c8739044 | |||
| 3b0de79a17 | |||
| b990725af1 | |||
| 845d8c9475 | |||
| f5704be2f2 | |||
| dac4fc4861 | |||
| d4f7619bdf | |||
| 1bf0eec41f | |||
| e3dc697e42 | |||
| ea9ee5fa32 | |||
| 4eb8159869 | |||
| 0c272328f8 | |||
| 492a9ece75 | |||
| 4c242d48d8 | |||
| 29e960eec4 | |||
| 40b877a4d9 | |||
| 4121f3481c |
8
.dockerignore
Normal file
8
.dockerignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
target/
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
README.md
|
||||||
|
.env
|
||||||
|
*.md
|
||||||
@ -41,6 +41,6 @@ repos:
|
|||||||
- id: check-todos # [NOGREP]
|
- id: check-todos # [NOGREP]
|
||||||
name: check-todos # [NOGREP]
|
name: check-todos # [NOGREP]
|
||||||
language: system
|
language: system
|
||||||
entry: sh -c "! grep --color=always --binary-files=without-match --dereference-recursive --exclude-dir='notes' --exclude-dir='node_modules' --exclude-dir='.git' --exclude='.pre-commit-config.yaml' --exclude-dir='venv' 'TODO'" # [NOGREP]
|
entry: sh -c "! grep --color=always --binary-files=without-match --dereference-recursive --exclude-dir='notes' --exclude-dir='target' --exclude-dir='node_modules' --exclude-dir='.git' --exclude='.pre-commit-config.yaml' --exclude-dir='venv' 'TODO' | grep -v '[NOGREP]'" # [NOGREP]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
always_run: true
|
always_run: true
|
||||||
|
|||||||
107
CHANGELOG.md
Normal file
107
CHANGELOG.md
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
# MDPreview Changelog
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
The format is based on [Keep a
|
||||||
|
Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to
|
||||||
|
[Semantic Versioning](https://semver.org/lang/en/).
|
||||||
|
|
||||||
|
|
||||||
|
## v0.4.4 - 2026-03-29
|
||||||
|
### Fixed
|
||||||
|
- Fixed removing newlines.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.4.3 - 2026-03-28
|
||||||
|
### Changed
|
||||||
|
- Change footer.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.4.2 - 2026-03-27
|
||||||
|
### Added
|
||||||
|
- Added version and project name in footer.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## v0.4.1 - 2026-03-26
|
||||||
|
### Fixed
|
||||||
|
- Added support for rendering individual files, not just directories (e.g.,
|
||||||
|
`mdpreview dir/file.md`).
|
||||||
|
- Fixed the rendering of Markdown lists.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.4.0 - 2026-03-25
|
||||||
|
### Added
|
||||||
|
- Added a button to copy the edit command (`note edit current_note.md`).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.3.0 - 2026-03-24
|
||||||
|
### Added
|
||||||
|
- Added the `--random` flag to open the browser on a random page when used with
|
||||||
|
`--browser` (`--browser --random`).
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.2.1 - 2026-03-23
|
||||||
|
### Fixed
|
||||||
|
- Fixed an issue where listening on port 0 (`--port 0`) caused the browser to
|
||||||
|
open the wrong port.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.2.0 - 2026-03-23
|
||||||
|
### Added
|
||||||
|
- Added the `--browser` flag to automatically open the page in the browser.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.3 - 2026-03-23
|
||||||
|
### Added
|
||||||
|
- Added a "Random File" button to the header.
|
||||||
|
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Non-Markdown files now render correctly.
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Translated the interface from Russian to English.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.2 - 2026-03-22
|
||||||
|
### Added
|
||||||
|
- Added a "Random File" button.
|
||||||
|
- Added an "On Main" button.
|
||||||
|
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Changed the default port to 8080.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
## v0.1.1 - 2026-03-21
|
||||||
|
### Added
|
||||||
|
- Added page titles.
|
||||||
595
Cargo.lock
generated
595
Cargo.lock
generated
@ -67,6 +67,61 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama"
|
||||||
|
version = "0.12.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
||||||
|
dependencies = [
|
||||||
|
"askama_derive",
|
||||||
|
"askama_escape",
|
||||||
|
"humansize",
|
||||||
|
"num-traits",
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_axum"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a41603f7cdbf5ac4af60760f17253eb6adf6ec5b6f14a7ed830cf687d375f163"
|
||||||
|
dependencies = [
|
||||||
|
"askama",
|
||||||
|
"axum-core",
|
||||||
|
"http",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_derive"
|
||||||
|
version = "0.12.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
||||||
|
dependencies = [
|
||||||
|
"askama_parser",
|
||||||
|
"basic-toml",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"serde",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_escape"
|
||||||
|
version = "0.10.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama_parser"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
||||||
|
dependencies = [
|
||||||
|
"nom",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-compression"
|
name = "async-compression"
|
||||||
version = "0.4.40"
|
version = "0.4.40"
|
||||||
@ -96,6 +151,12 @@ version = "1.1.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "autocfg"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.7.9"
|
version = "0.7.9"
|
||||||
@ -157,6 +218,15 @@ version = "0.22.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "basic-toml"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bincode"
|
name = "bincode"
|
||||||
version = "1.3.3"
|
version = "1.3.3"
|
||||||
@ -178,6 +248,12 @@ version = "2.11.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.20.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
@ -246,6 +322,16 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "combine"
|
||||||
|
version = "4.6.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compression-codecs"
|
name = "compression-codecs"
|
||||||
version = "0.4.37"
|
version = "0.4.37"
|
||||||
@ -263,6 +349,22 @@ version = "0.4.31"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation"
|
||||||
|
version = "0.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation-sys",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crc32fast"
|
name = "crc32fast"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -296,6 +398,17 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "displaydoc"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@ -528,6 +641,15 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "humansize"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.8.1"
|
||||||
@ -564,6 +686,108 @@ dependencies = [
|
|||||||
"tower-service",
|
"tower-service",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_collections"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"potential_utf",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_locale_core"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"litemap",
|
||||||
|
"tinystr",
|
||||||
|
"writeable",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_normalizer_data",
|
||||||
|
"icu_properties",
|
||||||
|
"icu_provider",
|
||||||
|
"smallvec",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_normalizer_data"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
||||||
|
dependencies = [
|
||||||
|
"icu_collections",
|
||||||
|
"icu_locale_core",
|
||||||
|
"icu_properties_data",
|
||||||
|
"icu_provider",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_properties_data"
|
||||||
|
version = "2.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "icu_provider"
|
||||||
|
version = "2.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"icu_locale_core",
|
||||||
|
"writeable",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerotrie",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||||
|
dependencies = [
|
||||||
|
"idna_adapter",
|
||||||
|
"smallvec",
|
||||||
|
"utf8_iter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna_adapter"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
|
||||||
|
dependencies = [
|
||||||
|
"icu_normalizer",
|
||||||
|
"icu_properties",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indexmap"
|
name = "indexmap"
|
||||||
version = "2.13.0"
|
version = "2.13.0"
|
||||||
@ -606,6 +830,65 @@ version = "1.0.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"combine",
|
||||||
|
"jni-macros",
|
||||||
|
"jni-sys",
|
||||||
|
"log",
|
||||||
|
"simd_cesu8",
|
||||||
|
"thiserror",
|
||||||
|
"walkdir",
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-macros"
|
||||||
|
version = "0.22.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustc_version",
|
||||||
|
"simd_cesu8",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-sys"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
|
||||||
|
dependencies = [
|
||||||
|
"jni-sys-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jni-sys-macros"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||||
|
dependencies = [
|
||||||
|
"once_cell",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "kqueue"
|
name = "kqueue"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@ -638,6 +921,12 @@ version = "0.2.182"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libm"
|
||||||
|
version = "0.2.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@ -655,6 +944,12 @@ version = "0.5.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litemap"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
@ -687,8 +982,10 @@ checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mdpreview"
|
name = "mdpreview"
|
||||||
version = "0.1.0"
|
version = "0.4.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"askama",
|
||||||
|
"askama_axum",
|
||||||
"axum",
|
"axum",
|
||||||
"clap",
|
"clap",
|
||||||
"futures",
|
"futures",
|
||||||
@ -702,6 +999,7 @@ dependencies = [
|
|||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
|
"webbrowser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -726,6 +1024,12 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@ -759,6 +1063,22 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ndk-context"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nom"
|
||||||
|
version = "7.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"minimal-lexical",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "6.1.1"
|
version = "6.1.1"
|
||||||
@ -793,6 +1113,40 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.19"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f"
|
||||||
|
dependencies = [
|
||||||
|
"objc2-encode",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-encode"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "objc2-foundation"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"objc2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.3"
|
version = "1.21.3"
|
||||||
@ -887,6 +1241,15 @@ dependencies = [
|
|||||||
"time",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "potential_utf"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
||||||
|
dependencies = [
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "powerfmt"
|
name = "powerfmt"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -1006,6 +1369,15 @@ version = "0.8.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc_version"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
|
||||||
|
dependencies = [
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@ -1033,6 +1405,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "semver"
|
||||||
|
version = "1.0.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde"
|
name = "serde"
|
||||||
version = "1.0.228"
|
version = "1.0.228"
|
||||||
@ -1130,6 +1508,22 @@ version = "0.3.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simd_cesu8"
|
||||||
|
version = "1.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
|
||||||
|
dependencies = [
|
||||||
|
"rustc_version",
|
||||||
|
"simdutf8",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simdutf8"
|
||||||
|
version = "0.1.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@ -1152,6 +1546,12 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "stable_deref_trait"
|
||||||
|
version = "1.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@ -1175,6 +1575,17 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "synstructure"
|
||||||
|
version = "0.13.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syntect"
|
name = "syntect"
|
||||||
version = "5.3.0"
|
version = "5.3.0"
|
||||||
@ -1256,6 +1667,16 @@ dependencies = [
|
|||||||
"time-core",
|
"time-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinystr"
|
||||||
|
version = "0.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"zerovec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.49.0"
|
version = "1.49.0"
|
||||||
@ -1425,6 +1846,24 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8_iter"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8parse"
|
name = "utf8parse"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@ -1453,6 +1892,77 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"once_cell",
|
||||||
|
"rustversion",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.114"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-sys"
|
||||||
|
version = "0.3.91"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webbrowser"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fe985f41e291eecef5e5c0770a18d28390addb03331c043964d9e916453d6f16"
|
||||||
|
dependencies = [
|
||||||
|
"core-foundation",
|
||||||
|
"jni",
|
||||||
|
"log",
|
||||||
|
"ndk-context",
|
||||||
|
"objc2",
|
||||||
|
"objc2-foundation",
|
||||||
|
"url",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winapi-util"
|
name = "winapi-util"
|
||||||
version = "0.1.11"
|
version = "0.1.11"
|
||||||
@ -1617,6 +2127,12 @@ version = "0.53.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "writeable"
|
||||||
|
version = "0.6.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yaml-rust"
|
name = "yaml-rust"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@ -1626,6 +2142,29 @@ dependencies = [
|
|||||||
"linked-hash-map",
|
"linked-hash-map",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
||||||
|
dependencies = [
|
||||||
|
"stable_deref_trait",
|
||||||
|
"yoke-derive",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yoke-derive"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.47"
|
version = "0.8.47"
|
||||||
@ -1646,6 +2185,60 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
||||||
|
dependencies = [
|
||||||
|
"zerofrom-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerofrom-derive"
|
||||||
|
version = "0.1.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"synstructure",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerotrie"
|
||||||
|
version = "0.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
||||||
|
dependencies = [
|
||||||
|
"displaydoc",
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec"
|
||||||
|
version = "0.11.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||||
|
dependencies = [
|
||||||
|
"yoke",
|
||||||
|
"zerofrom",
|
||||||
|
"zerovec-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zerovec-derive"
|
||||||
|
version = "0.11.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zmij"
|
name = "zmij"
|
||||||
version = "1.0.21"
|
version = "1.0.21"
|
||||||
|
|||||||
15
Cargo.toml
15
Cargo.toml
@ -1,15 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "mdpreview"
|
name = "mdpreview"
|
||||||
version = "0.1.0"
|
version = "0.4.4"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
authors = ["Vladislav Kan <thek4n@yandex.ru>"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = "z" # Оптимизация именно по размеру (z > s)
|
opt-level = "z"
|
||||||
lto = true # Link Time Optimization: объединяет и оптимизирует весь код целиком
|
lto = true
|
||||||
codegen-units = 1 # Уменьшает количество параллельных единиц компиляции, позволяя лучше оптимизировать
|
codegen-units = 1
|
||||||
panic = "abort" # Отключает механизм развертывания стека при панике (экономит много места)
|
panic = "abort"
|
||||||
strip = true # Автоматически удаляет символы отладки (доступно в стабильной версии Rust 1.59+)
|
strip = true
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -26,3 +26,6 @@ clap = { version = "4.5", features = ["derive"] }
|
|||||||
tower-http = { version = "0.6.8", features = ["trace", "compression-gzip", "cors"] }
|
tower-http = { version = "0.6.8", features = ["trace", "compression-gzip", "cors"] }
|
||||||
mime_guess = "2"
|
mime_guess = "2"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
|
askama = { version = "0.12", features = ["with-axum"] }
|
||||||
|
askama_axum = "0.4"
|
||||||
|
webbrowser = "1.0"
|
||||||
|
|||||||
43
Dockerfile
Normal file
43
Dockerfile
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
FROM rust:1.94 AS chef
|
||||||
|
|
||||||
|
RUN cargo install --locked cargo-chef
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY Cargo.toml Cargo.lock .
|
||||||
|
RUN mkdir src && touch src/main.rs
|
||||||
|
|
||||||
|
RUN cargo chef prepare --recipe-path recipe.json
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FROM chef AS builder
|
||||||
|
|
||||||
|
COPY --from=chef /app/recipe.json recipe.json
|
||||||
|
|
||||||
|
RUN cargo chef cook --release --recipe-path recipe.json
|
||||||
|
|
||||||
|
|
||||||
|
COPY rust-toolchain.toml rust-toolchain.toml
|
||||||
|
|
||||||
|
RUN cargo fetch
|
||||||
|
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim AS runtime
|
||||||
|
|
||||||
|
RUN useradd -m -s /bin/bash appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/target/release/mdpreview /usr/local/bin/mdpreview
|
||||||
|
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["mdpreview"]
|
||||||
4
TODO.md
Normal file
4
TODO.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# TODO <!-- [NOGREP] -->
|
||||||
|
|
||||||
|
* [X] Сделать кнопку скопировать путь
|
||||||
|
* [X] Сделать кнопку скопировать путь вместе с командой (`note edit ...`)
|
||||||
3
rust-toolchain.toml
Normal file
3
rust-toolchain.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "stable"
|
||||||
|
components = ["clippy", "rustfmt"]
|
||||||
632
src/main.rs
632
src/main.rs
@ -1,8 +1,12 @@
|
|||||||
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::{Path, State},
|
extract::{Path as AxumPath, State},
|
||||||
http::{HeaderMap, StatusCode, header},
|
http::{
|
||||||
response::{Html, IntoResponse, Sse},
|
HeaderMap, StatusCode,
|
||||||
|
header::{self},
|
||||||
|
},
|
||||||
|
response::{IntoResponse, Redirect, Sse},
|
||||||
routing::get,
|
routing::get,
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
@ -10,9 +14,9 @@ use mime_guess::from_path;
|
|||||||
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
use notify::{Config, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
use std::fmt::Write;
|
use std::ffi::OsStr;
|
||||||
use std::net::{SocketAddr, ToSocketAddrs};
|
use std::net::{SocketAddr, ToSocketAddrs};
|
||||||
use std::path::PathBuf;
|
use std::path::{Path as StdPath, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
@ -23,6 +27,7 @@ use syntect::highlighting::ThemeSet;
|
|||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
use webbrowser;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use std::io;
|
use std::io;
|
||||||
@ -30,8 +35,42 @@ use std::io;
|
|||||||
mod markdown;
|
mod markdown;
|
||||||
use markdown::markdown_to_html;
|
use markdown::markdown_to_html;
|
||||||
|
|
||||||
const TEMPLATE_FILE: &str = include_str!("../templates/file.html");
|
mod other;
|
||||||
const TEMPLATE_DIR: &str = include_str!("../templates/dir.html");
|
use other::code_to_html;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct FileEntry {
|
||||||
|
pub name: String,
|
||||||
|
pub link: String,
|
||||||
|
pub is_dir: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "dir.html")]
|
||||||
|
pub struct DirectoryTemplate {
|
||||||
|
pub title_path: String,
|
||||||
|
pub files: Vec<FileEntry>,
|
||||||
|
pub package_name: String,
|
||||||
|
pub authors: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "file.html")]
|
||||||
|
pub struct NoteTemplate {
|
||||||
|
pub filename: String,
|
||||||
|
pub back_link: String,
|
||||||
|
pub content: String,
|
||||||
|
pub sse_url: String,
|
||||||
|
pub copy_path: String,
|
||||||
|
pub package_name: String,
|
||||||
|
pub authors: String,
|
||||||
|
pub version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
const PACKAGE_NAME: &str = env!("CARGO_PKG_NAME");
|
||||||
|
const AUTHORS: &str = env!("CARGO_PKG_AUTHORS");
|
||||||
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||||
|
|
||||||
#[derive(clap::Parser, Debug)]
|
#[derive(clap::Parser, Debug)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
@ -41,10 +80,18 @@ struct Args {
|
|||||||
host: String,
|
host: String,
|
||||||
|
|
||||||
/// Port to listen
|
/// Port to listen
|
||||||
#[arg(short, long, default_value_t = 8000)]
|
#[arg(short, long, default_value_t = 8080)]
|
||||||
port: u16,
|
port: u16,
|
||||||
|
|
||||||
/// Markdown documents directory root
|
/// Open browser
|
||||||
|
#[arg(long, default_value_t = false)]
|
||||||
|
browser: bool,
|
||||||
|
|
||||||
|
/// Open browser on random note. Requires flag --browser
|
||||||
|
#[arg(long, default_value_t = false, requires = "browser")]
|
||||||
|
random: bool,
|
||||||
|
|
||||||
|
/// Notes root (can be a directory or a single file)
|
||||||
#[arg()]
|
#[arg()]
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
}
|
}
|
||||||
@ -55,20 +102,23 @@ struct AppState {
|
|||||||
theme_set: Arc<ThemeSet>,
|
theme_set: Arc<ThemeSet>,
|
||||||
tx: Arc<broadcast::Sender<String>>,
|
tx: Arc<broadcast::Sender<String>>,
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
|
is_root_file: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
|
|
||||||
if !args.root.is_dir() {
|
if !args.root.exists() {
|
||||||
eprintln!("Root {root} is not a directory", root = args.root.display());
|
eprintln!("Root path {} does not exist", args.root.display());
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let is_root_file = args.root.is_file();
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(tracing_subscriber::EnvFilter::new(
|
.with(tracing_subscriber::EnvFilter::new(
|
||||||
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=info".into()),
|
std::env::var("RUST_LOG").unwrap_or_else(|_| "info,tower_http=warn".into()),
|
||||||
))
|
))
|
||||||
.with(tracing_subscriber::fmt::layer())
|
.with(tracing_subscriber::fmt::layer())
|
||||||
.init();
|
.init();
|
||||||
@ -82,7 +132,8 @@ async fn main() {
|
|||||||
syntax_set: Arc::new(ss),
|
syntax_set: Arc::new(ss),
|
||||||
theme_set: Arc::new(ts),
|
theme_set: Arc::new(ts),
|
||||||
tx: Arc::new(tx),
|
tx: Arc::new(tx),
|
||||||
root: args.root,
|
root: args.root.clone(),
|
||||||
|
is_root_file,
|
||||||
};
|
};
|
||||||
|
|
||||||
let watcher_state = state.clone();
|
let watcher_state = state.clone();
|
||||||
@ -91,37 +142,287 @@ async fn main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/", get(root))
|
.route("/", get(root_handler))
|
||||||
.route("/random", get(random_file))
|
.route("/random", get(random_file))
|
||||||
.route("/*path", get(serve_file))
|
.route("/*path", get(serve_file))
|
||||||
.route("/events/*path", get(sse_handler))
|
.route("/events/*path", get(sse_handler))
|
||||||
.with_state(state)
|
.with_state(state.clone())
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
let addr = resolve_addr(&args.host, args.port).unwrap();
|
let addr = resolve_addr(&args.host, args.port).expect("Failed to resolve address");
|
||||||
|
|
||||||
println!("Server started on {addr}");
|
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
|
||||||
|
let actual_addr = listener.local_addr().expect("Failed to get local address");
|
||||||
|
println!("mdpreview v{VERSION} server started on http://{actual_addr}");
|
||||||
|
|
||||||
|
if args.browser {
|
||||||
|
let mut url = format!("http://{actual_addr}");
|
||||||
|
if args.random {
|
||||||
|
if !state.is_root_file {
|
||||||
|
url = format!("{url}/random")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = webbrowser::open(url.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
axum::serve(listener, app).await.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_addr(host: &str, port: u16) -> io::Result<SocketAddr> {
|
fn resolve_addr(host: &str, port: u16) -> io::Result<SocketAddr> {
|
||||||
let addr_str = format!("{host}:{port}");
|
let addr_str = format!("{host}:{port}");
|
||||||
|
|
||||||
addr_str
|
addr_str
|
||||||
.to_socket_addrs()?
|
.to_socket_addrs()?
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Не удалось разрешить адрес"))
|
.ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "Cannot resolve addr"))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn root(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
async fn root_handler(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
render_directory_index(&state.root, "").await
|
if state.is_root_file {
|
||||||
|
match render_single_file(&state.root, &state).await {
|
||||||
|
Ok(template) => template.into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match render_directory_index(&state.root, "").await {
|
||||||
|
Ok(template) => template.into_response(),
|
||||||
|
Err(e) => e.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_single_file(
|
||||||
|
file_path: &StdPath,
|
||||||
|
state: &AppState,
|
||||||
|
) -> Result<NoteTemplate, StatusCode> {
|
||||||
|
let metadata = match fs::metadata(file_path).await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return Err(StatusCode::NOT_FOUND),
|
||||||
|
};
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
return Err(StatusCode::BAD_REQUEST);
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension = file_path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
let content = match fs::read_to_string(file_path).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
};
|
||||||
|
|
||||||
|
let display_path = "";
|
||||||
|
|
||||||
|
let html_content = if extension == "md" {
|
||||||
|
markdown_to_html(&content, &state.syntax_set, &state.theme_set, display_path)
|
||||||
|
} else {
|
||||||
|
code_to_html(
|
||||||
|
&content,
|
||||||
|
extension.as_str(),
|
||||||
|
&state.syntax_set,
|
||||||
|
&state.theme_set,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let filename = file_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.unwrap_or("Unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let back_link = "/".to_string();
|
||||||
|
|
||||||
|
let sse_url = "/events/".to_string();
|
||||||
|
let copy_path = format!("note fe {filename}").to_string();
|
||||||
|
|
||||||
|
Ok(NoteTemplate {
|
||||||
|
filename,
|
||||||
|
back_link,
|
||||||
|
content: html_content,
|
||||||
|
sse_url,
|
||||||
|
copy_path,
|
||||||
|
package_name: PACKAGE_NAME.to_string(),
|
||||||
|
authors: AUTHORS.to_string(),
|
||||||
|
version: VERSION.to_string(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn serve_file(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
AxumPath(full_path): AxumPath<String>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
if state.is_root_file {
|
||||||
|
return Err(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if full_path.is_empty() {
|
||||||
|
return Err(StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut requested_path = state.root.clone();
|
||||||
|
requested_path.push(&full_path);
|
||||||
|
|
||||||
|
let Ok(safe_path) = fs::canonicalize(&requested_path).await else {
|
||||||
|
return Err(StatusCode::NOT_FOUND);
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(base_dir) = fs::canonicalize(&state.root).await else {
|
||||||
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
|
};
|
||||||
|
|
||||||
|
if !safe_path.starts_with(&base_dir) {
|
||||||
|
eprintln!("Path traversal attempt: {}", safe_path.display());
|
||||||
|
return Err(StatusCode::FORBIDDEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
let metadata = match fs::metadata(&safe_path).await {
|
||||||
|
Ok(m) => m,
|
||||||
|
Err(_) => return Err(StatusCode::NOT_FOUND),
|
||||||
|
};
|
||||||
|
|
||||||
|
if metadata.is_dir() {
|
||||||
|
return match render_directory_index(&safe_path, &full_path).await {
|
||||||
|
Ok(t) => Ok(t.into_response()),
|
||||||
|
Err(e) => Err(e),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let extension = safe_path
|
||||||
|
.extension()
|
||||||
|
.and_then(|ext| ext.to_str())
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
let is_image = matches!(
|
||||||
|
extension.as_str(),
|
||||||
|
"png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico"
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_image {
|
||||||
|
let file_content = match fs::read(&safe_path).await {
|
||||||
|
Ok(content) => content,
|
||||||
|
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
};
|
||||||
|
let mime_type = from_path(&safe_path).first_or_octet_stream();
|
||||||
|
return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = match fs::read_to_string(&safe_path).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
};
|
||||||
|
|
||||||
|
let html_content = if extension == "md" {
|
||||||
|
markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path)
|
||||||
|
} else {
|
||||||
|
code_to_html(
|
||||||
|
&content,
|
||||||
|
extension.as_str(),
|
||||||
|
&state.syntax_set,
|
||||||
|
&state.theme_set,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let filename = safe_path
|
||||||
|
.file_name()
|
||||||
|
.and_then(OsStr::to_str)
|
||||||
|
.unwrap_or("Unknown")
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
let back_link = if let Some(pos) = full_path.rfind('/') {
|
||||||
|
let parent = &full_path[..pos];
|
||||||
|
if parent.is_empty() {
|
||||||
|
"/".to_string()
|
||||||
|
} else {
|
||||||
|
format!("/{parent}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"/".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let sse_url = format!("/events/{full_path}");
|
||||||
|
|
||||||
|
let copy_path = format!("note edit {full_path}");
|
||||||
|
|
||||||
|
let template = NoteTemplate {
|
||||||
|
filename,
|
||||||
|
back_link,
|
||||||
|
content: html_content,
|
||||||
|
sse_url,
|
||||||
|
copy_path,
|
||||||
|
package_name: PACKAGE_NAME.to_string(),
|
||||||
|
authors: AUTHORS.to_string(),
|
||||||
|
version: VERSION.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(template.into_response())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_directory_index(
|
||||||
|
dir_path: &StdPath,
|
||||||
|
request_path: &str,
|
||||||
|
) -> Result<DirectoryTemplate, StatusCode> {
|
||||||
|
let mut entries = match fs::read_dir(dir_path).await {
|
||||||
|
Ok(list) => list,
|
||||||
|
Err(_) => return Err(StatusCode::FORBIDDEN),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut files: Vec<FileEntry> = Vec::new();
|
||||||
|
|
||||||
|
while let Some(entry) = entries
|
||||||
|
.next_entry()
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
{
|
||||||
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
|
|
||||||
|
if file_name.starts_with('.') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_dir = entry.metadata().await.map(|m| m.is_dir()).unwrap_or(false);
|
||||||
|
|
||||||
|
let link = if request_path.is_empty() {
|
||||||
|
file_name.clone()
|
||||||
|
} else {
|
||||||
|
format!("{}/{}", request_path, file_name)
|
||||||
|
};
|
||||||
|
|
||||||
|
files.push(FileEntry {
|
||||||
|
name: file_name,
|
||||||
|
link,
|
||||||
|
is_dir,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
files.sort_by(|a, b| match (a.is_dir, b.is_dir) {
|
||||||
|
(true, false) => std::cmp::Ordering::Less,
|
||||||
|
(false, true) => std::cmp::Ordering::Greater,
|
||||||
|
_ => a.name.cmp(&b.name),
|
||||||
|
});
|
||||||
|
|
||||||
|
let title_path = request_path.trim_start_matches('/').to_string();
|
||||||
|
|
||||||
|
let package_name = env!("CARGO_PKG_NAME").to_string();
|
||||||
|
let authors = env!("CARGO_PKG_AUTHORS").to_string();
|
||||||
|
let version = env!("CARGO_PKG_VERSION").to_string();
|
||||||
|
|
||||||
|
Ok(DirectoryTemplate {
|
||||||
|
title_path,
|
||||||
|
files,
|
||||||
|
package_name,
|
||||||
|
authors,
|
||||||
|
version,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sse_handler(
|
async fn sse_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
Path(full_path): Path<String>,
|
AxumPath(full_path): AxumPath<String>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(
|
headers.insert(
|
||||||
@ -139,13 +440,23 @@ async fn sse_handler(
|
|||||||
|
|
||||||
let rx = state.tx.subscribe();
|
let rx = state.tx.subscribe();
|
||||||
let requested_path = full_path.clone();
|
let requested_path = full_path.clone();
|
||||||
|
let root_path_str = state.root.to_string_lossy().to_string();
|
||||||
|
|
||||||
let stream = BroadcastStream::new(rx).filter_map(move |res| {
|
let stream = BroadcastStream::new(rx).filter_map(move |res| {
|
||||||
let req_path = requested_path.clone();
|
let req_path = requested_path.clone();
|
||||||
|
let root_str = root_path_str.clone();
|
||||||
|
let is_root_file_mode = state.is_root_file;
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
match res {
|
match res {
|
||||||
Ok(changed_path) => {
|
Ok(changed_path) => {
|
||||||
if changed_path.contains(&req_path) {
|
let should_notify = if is_root_file_mode {
|
||||||
|
changed_path == root_str || changed_path.ends_with(&root_str)
|
||||||
|
} else {
|
||||||
|
changed_path.contains(&req_path) || changed_path.ends_with(&req_path)
|
||||||
|
};
|
||||||
|
|
||||||
|
if should_notify {
|
||||||
Some(Ok::<axum::response::sse::Event, Infallible>(
|
Some(Ok::<axum::response::sse::Event, Infallible>(
|
||||||
axum::response::sse::Event::default()
|
axum::response::sse::Event::default()
|
||||||
.event("reload")
|
.event("reload")
|
||||||
@ -169,233 +480,21 @@ async fn sse_handler(
|
|||||||
(headers, sse)
|
(headers, sse)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_file(
|
async fn random_file(State(state): State<AppState>) -> impl IntoResponse {
|
||||||
State(state): State<AppState>,
|
if state.is_root_file {
|
||||||
Path(full_path): Path<String>,
|
return Ok(Redirect::temporary("/"));
|
||||||
) -> Result<impl IntoResponse, StatusCode> {
|
|
||||||
if full_path.is_empty() {
|
|
||||||
return Err(StatusCode::NOT_FOUND);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut requested_path = PathBuf::from(&state.root);
|
|
||||||
requested_path.push(&full_path);
|
|
||||||
|
|
||||||
let Ok(safe_path) = fs::canonicalize(&requested_path).await else {
|
|
||||||
return Err(StatusCode::NOT_FOUND);
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(base_dir) = fs::canonicalize(&state.root).await else {
|
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
};
|
|
||||||
|
|
||||||
if !safe_path.starts_with(&base_dir) {
|
|
||||||
eprintln!("Path traversal: {}", safe_path.display());
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
|
||||||
}
|
|
||||||
|
|
||||||
let metadata = match fs::metadata(&safe_path).await {
|
|
||||||
Ok(m) => m,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error getting metadata: {e}");
|
|
||||||
return Err(StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if metadata.is_dir() {
|
|
||||||
return render_directory_index(&safe_path, &full_path)
|
|
||||||
.await
|
|
||||||
.map(|h| h.into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
let extension = safe_path
|
|
||||||
.extension()
|
|
||||||
.and_then(|ext| ext.to_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
let is_image = matches!(
|
|
||||||
extension.to_lowercase().as_str(),
|
|
||||||
"png" | "jpg" | "jpeg" | "gif" | "svg" | "webp" | "bmp" | "ico"
|
|
||||||
);
|
|
||||||
|
|
||||||
if is_image {
|
|
||||||
let file_content = match fs::read(&safe_path).await {
|
|
||||||
Ok(content) => content,
|
|
||||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mime_type = from_path(&safe_path).first_or_octet_stream();
|
|
||||||
|
|
||||||
return Ok(([(header::CONTENT_TYPE, mime_type.as_ref())], file_content).into_response());
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = match fs::read_to_string(&safe_path).await {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error reading: {e}");
|
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let back_link = if let Some(pos) = full_path.rfind('/') {
|
|
||||||
let parent = &full_path[..pos];
|
|
||||||
if parent.is_empty() {
|
|
||||||
"/".to_string()
|
|
||||||
} else {
|
|
||||||
format!("/{parent}")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
"/".to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path);
|
|
||||||
|
|
||||||
// Заполнение шаблона
|
|
||||||
let final_html = TEMPLATE_FILE
|
|
||||||
.replace("{{CONTENT}}", &html_content)
|
|
||||||
.replace("{{SSE_URL}}", &format!("/events/{full_path}"))
|
|
||||||
.replace("{{BACK_LINK}}", &back_link);
|
|
||||||
|
|
||||||
Ok(Html(final_html).into_response())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn render_directory_index(
|
|
||||||
dir_path: &PathBuf,
|
|
||||||
request_path: &str,
|
|
||||||
) -> Result<Html<String>, StatusCode> {
|
|
||||||
let mut entries = match fs::read_dir(dir_path).await {
|
|
||||||
Ok(list) => list,
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("Error directory reading: {e}");
|
|
||||||
return Err(StatusCode::FORBIDDEN);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut files: Vec<(String, String, bool)> = Vec::new();
|
|
||||||
|
|
||||||
while let Some(entry) = entries
|
|
||||||
.next_entry()
|
|
||||||
.await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
|
||||||
{
|
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
|
||||||
|
|
||||||
if file_name.starts_with('.') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let is_dir = entry.metadata().await.map(|m| m.is_dir()).unwrap_or(false);
|
|
||||||
|
|
||||||
let mut link_path = request_path.to_string();
|
|
||||||
if !link_path.ends_with('/') {
|
|
||||||
link_path.push('/');
|
|
||||||
}
|
|
||||||
link_path.push_str(&file_name);
|
|
||||||
|
|
||||||
files.push((file_name, link_path, is_dir));
|
|
||||||
}
|
|
||||||
|
|
||||||
files.sort_by(|a, b| match (a.2, b.2) {
|
|
||||||
(true, false) => std::cmp::Ordering::Less,
|
|
||||||
(false, true) => std::cmp::Ordering::Greater,
|
|
||||||
_ => a.0.cmp(&b.0),
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut list_html = String::from("<ul>");
|
|
||||||
|
|
||||||
if !request_path.is_empty() && request_path != "files" {
|
|
||||||
let parent_path = request_path.rsplit_once('/').map_or("", |(p, _)| p);
|
|
||||||
let parent_link = if parent_path.is_empty() {
|
|
||||||
"/".to_string()
|
|
||||||
} else {
|
|
||||||
format!("/{parent_path}")
|
|
||||||
};
|
|
||||||
|
|
||||||
let _ = write!(
|
|
||||||
list_html,
|
|
||||||
r#"<li>
|
|
||||||
<a href="{parent_link}" class="back-link">📁 ..</a>
|
|
||||||
</li>"#
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name, link, is_dir) in files {
|
|
||||||
let icon = if is_dir { "📁" } else { "📄" };
|
|
||||||
let _ = write!(
|
|
||||||
list_html,
|
|
||||||
r#"<li>
|
|
||||||
<a href="/{}" class="file-link">
|
|
||||||
<span class="icon">{}</span>
|
|
||||||
<span>{}</span>
|
|
||||||
</a>
|
|
||||||
</li>"#,
|
|
||||||
link.trim_start_matches('/'),
|
|
||||||
icon,
|
|
||||||
name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
list_html.push_str("</ul>");
|
|
||||||
|
|
||||||
let title_path = if request_path.is_empty() {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
request_path.trim_start_matches('/')
|
|
||||||
};
|
|
||||||
|
|
||||||
let final_html = TEMPLATE_DIR
|
|
||||||
.replace("{{TITLE_PATH}}", title_path)
|
|
||||||
.replace("{{FILE_LIST}}", &list_html);
|
|
||||||
|
|
||||||
Ok(Html(final_html))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run_file_watcher(state: AppState) {
|
|
||||||
let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::<PathBuf>(100);
|
|
||||||
|
|
||||||
let tx_fs_clone = tx_fs.clone();
|
|
||||||
let mut watcher = RecommendedWatcher::new(
|
|
||||||
move |res: Result<notify::Event, notify::Error>| {
|
|
||||||
if let Ok(event) = res
|
|
||||||
&& matches!(event.kind, EventKind::Modify(_))
|
|
||||||
{
|
|
||||||
for path in event.paths {
|
|
||||||
let _ = tx_fs_clone.blocking_send(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Config::default(),
|
|
||||||
)
|
|
||||||
.expect("Failed to create watcher");
|
|
||||||
|
|
||||||
let watch_path = state.root;
|
|
||||||
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) {
|
|
||||||
eprintln!("Ошибка настройки watcher: {e}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(path) = rx_fs.recv().await {
|
|
||||||
if let Some(path_str) = path.to_str() {
|
|
||||||
let _ = state.tx.send(path_str.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn random_file(State(state): State<AppState>) -> Result<impl IntoResponse, StatusCode> {
|
|
||||||
let mut files: Vec<PathBuf> = Vec::new();
|
let mut files: Vec<PathBuf> = Vec::new();
|
||||||
|
|
||||||
let mut stack = vec![state.root.clone()];
|
let mut stack = vec![state.root.clone()];
|
||||||
|
|
||||||
while let Some(current_dir) = stack.pop() {
|
while let Some(current_dir) = stack.pop() {
|
||||||
let mut entries = match fs::read_dir(¤t_dir).await {
|
let Ok(mut entries) = fs::read_dir(¤t_dir).await else {
|
||||||
Ok(list) => list,
|
continue;
|
||||||
Err(_) => continue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
while let Some(entry) = entries
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
.next_entry()
|
|
||||||
.await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
|
||||||
{
|
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
|
|
||||||
if entry.file_name().to_string_lossy().starts_with('.') {
|
if entry.file_name().to_string_lossy().starts_with('.') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -415,11 +514,50 @@ async fn random_file(State(state): State<AppState>) -> Result<impl IntoResponse,
|
|||||||
let mut rng = rand::thread_rng();
|
let mut rng = rand::thread_rng();
|
||||||
let random_path = files.choose(&mut rng).unwrap();
|
let random_path = files.choose(&mut rng).unwrap();
|
||||||
|
|
||||||
let relative_path = random_path
|
let relative_path = match random_path.strip_prefix(&state.root) {
|
||||||
.strip_prefix(&state.root)
|
Ok(p) => p,
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
|
};
|
||||||
|
|
||||||
let url_path = relative_path.to_string_lossy().replace('\\', "/");
|
let url_path = relative_path.to_string_lossy().replace('\\', "/");
|
||||||
|
Ok(Redirect::temporary(&format!("/{url_path}")))
|
||||||
Ok(axum::response::Redirect::temporary(&format!("/{url_path}")))
|
}
|
||||||
|
|
||||||
|
async fn run_file_watcher(state: AppState) {
|
||||||
|
let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::<PathBuf>(100);
|
||||||
|
|
||||||
|
let tx_fs_clone = tx_fs.clone();
|
||||||
|
let mut watcher = RecommendedWatcher::new(
|
||||||
|
move |res: Result<notify::Event, notify::Error>| {
|
||||||
|
if let Ok(event) = res
|
||||||
|
&& matches!(
|
||||||
|
event.kind,
|
||||||
|
EventKind::Modify(_) | EventKind::Create(_) | EventKind::Remove(_)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
for path in event.paths {
|
||||||
|
let _ = tx_fs_clone.blocking_send(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Config::default(),
|
||||||
|
)
|
||||||
|
.expect("Failed to create watcher");
|
||||||
|
|
||||||
|
let watch_result = if state.is_root_file {
|
||||||
|
watcher.watch(&state.root, RecursiveMode::NonRecursive)
|
||||||
|
} else {
|
||||||
|
watcher.watch(&state.root, RecursiveMode::Recursive)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = watch_result {
|
||||||
|
eprintln!("Failed set watcher: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(path) = rx_fs.recv().await {
|
||||||
|
if let Some(path_str) = path.to_str() {
|
||||||
|
let _ = state.tx.send(path_str.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,8 +4,64 @@ use syntect::highlighting::ThemeSet;
|
|||||||
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
||||||
use syntect::parsing::SyntaxSet;
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
|
fn normalize_lists(input: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(input.len() + input.len() / 10);
|
||||||
|
|
||||||
|
for line in input.lines() {
|
||||||
|
if let Some(first_non_ws) = line.find(|c: char| !c.is_whitespace()) {
|
||||||
|
let marker_char = line.chars().nth(first_non_ws);
|
||||||
|
|
||||||
|
if let Some(marker) = marker_char {
|
||||||
|
if marker == '*' || marker == '-' || marker == '+' {
|
||||||
|
let rest_start = first_non_ws + 1;
|
||||||
|
|
||||||
|
if rest_start < line.len() {
|
||||||
|
let next_char = line.chars().nth(rest_start).unwrap();
|
||||||
|
|
||||||
|
if !next_char.is_whitespace() {
|
||||||
|
let mut marker_count = 1;
|
||||||
|
let mut is_only_markers = true;
|
||||||
|
|
||||||
|
for c in line.chars().skip(rest_start) {
|
||||||
|
if c == marker {
|
||||||
|
marker_count += 1;
|
||||||
|
} else if !c.is_whitespace() {
|
||||||
|
is_only_markers = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(is_only_markers && marker_count >= 2) {
|
||||||
|
result.push_str(&line[..rest_start]);
|
||||||
|
result.push(' ');
|
||||||
|
result.push_str(&line[rest_start..]);
|
||||||
|
result.push('\n');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push_str(line);
|
||||||
|
result.push('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.ends_with('\n') && !input.ends_with('\n') {
|
||||||
|
result.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
|
pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_path: &str) -> String {
|
||||||
let theme = &ts.themes["base16-ocean.dark"];
|
let normalized_markdown = normalize_lists(markdown);
|
||||||
|
|
||||||
|
let theme = ts
|
||||||
|
.themes
|
||||||
|
.get("base16-ocean.dark")
|
||||||
|
.unwrap_or_else(|| ts.themes.values().next().expect("No themes available"));
|
||||||
|
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
@ -14,7 +70,7 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
options.insert(Options::ENABLE_TASKLISTS);
|
options.insert(Options::ENABLE_TASKLISTS);
|
||||||
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
options.insert(Options::ENABLE_SMART_PUNCTUATION);
|
||||||
|
|
||||||
let parser = pulldown_cmark::Parser::new_ext(markdown, options);
|
let parser = pulldown_cmark::Parser::new_ext(&normalized_markdown, options);
|
||||||
|
|
||||||
let mut processed_events: Vec<Event> = Vec::new();
|
let mut processed_events: Vec<Event> = Vec::new();
|
||||||
let mut in_code_block = false;
|
let mut in_code_block = false;
|
||||||
@ -23,6 +79,10 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
|
|
||||||
for event in parser {
|
for event in parser {
|
||||||
match event {
|
match event {
|
||||||
|
Event::Rule => {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
Event::Start(Tag::CodeBlock(kind)) => {
|
Event::Start(Tag::CodeBlock(kind)) => {
|
||||||
in_code_block = true;
|
in_code_block = true;
|
||||||
current_code.clear();
|
current_code.clear();
|
||||||
@ -56,19 +116,24 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
||||||
let mut h = HighlightLines::new(syntax, theme);
|
let mut h = HighlightLines::new(syntax, theme);
|
||||||
let mut result_html = String::new();
|
let mut result_html = String::new();
|
||||||
for line in current_code.lines() {
|
|
||||||
let line_with_newline = format!("{line}\n");
|
let lines: Vec<&str> = current_code.lines().collect();
|
||||||
match h.highlight_line(&line_with_newline, ss) {
|
|
||||||
|
for line in lines.iter() {
|
||||||
|
match h.highlight_line(&line, ss) {
|
||||||
Ok(regions) => {
|
Ok(regions) => {
|
||||||
let html_line = styled_line_to_highlighted_html(
|
let html_line = styled_line_to_highlighted_html(
|
||||||
®ions[..],
|
®ions[..],
|
||||||
IncludeBackground::No,
|
IncludeBackground::No,
|
||||||
)
|
)
|
||||||
.unwrap_or_else(|_| escape_html(&line_with_newline));
|
.unwrap_or_else(|_| escape_html(&line));
|
||||||
|
|
||||||
result_html.push_str(&html_line);
|
result_html.push_str(&html_line);
|
||||||
|
result_html.push('\n');
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
result_html.push_str(&escape_html(&line_with_newline));
|
result_html.push_str(&escape_html(&line));
|
||||||
|
result_html.push('\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -104,6 +169,7 @@ pub fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, _file_pat
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut body_html = String::new();
|
let mut body_html = String::new();
|
||||||
|
|
||||||
html::push_html(&mut body_html, processed_events.into_iter());
|
html::push_html(&mut body_html, processed_events.into_iter());
|
||||||
body_html
|
body_html
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/other.rs
Normal file
80
src/other.rs
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
use syntect::easy::HighlightLines;
|
||||||
|
use syntect::highlighting::ThemeSet;
|
||||||
|
use syntect::html::{IncludeBackground, styled_line_to_highlighted_html};
|
||||||
|
use syntect::parsing::SyntaxSet;
|
||||||
|
|
||||||
|
/// Преобразует исходный код и его язык в подсвеченный HTML.
|
||||||
|
///
|
||||||
|
/// # Аргументы
|
||||||
|
/// * `code` - Исходный код как строка.
|
||||||
|
/// * `lang` - Идентификатор языка (например, "rust", "python", "mermaid").
|
||||||
|
/// * `ss` - Набор синтаксисов (SyntaxSet).
|
||||||
|
/// * `ts` - Набор тем (ThemeSet).
|
||||||
|
///
|
||||||
|
/// # Возвращает
|
||||||
|
/// Строку HTML, содержащую обертку блока кода с заголовком и кнопкой копирования.
|
||||||
|
pub fn code_to_html(code: &str, lang: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||||
|
let theme = &ts.themes["base16-ocean.dark"];
|
||||||
|
|
||||||
|
if lang == "mermaid" {
|
||||||
|
let escaped_code = escape_html(code);
|
||||||
|
return format!(
|
||||||
|
r#"<div class="code-block-wrapper mermaid-wrapper">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="code-lang">Mermaid Diagram</span>
|
||||||
|
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
||||||
|
</div>
|
||||||
|
<div class="mermaid" style="background: transparent; padding: 20px; text-align: center;">{escaped_code}</div>
|
||||||
|
</div>"#
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let lang_display = if lang.is_empty() { "text" } else { lang };
|
||||||
|
let lang_escaped = escape_html(lang_display);
|
||||||
|
|
||||||
|
let highlighted_html = if !lang.is_empty() {
|
||||||
|
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
||||||
|
let mut h = HighlightLines::new(syntax, theme);
|
||||||
|
let mut result_html = String::new();
|
||||||
|
|
||||||
|
for line in code.lines() {
|
||||||
|
let line_with_newline = format!("{line}\n");
|
||||||
|
|
||||||
|
match h.highlight_line(&line_with_newline, ss) {
|
||||||
|
Ok(regions) => {
|
||||||
|
match styled_line_to_highlighted_html(®ions[..], IncludeBackground::No) {
|
||||||
|
Ok(html_line) => result_html.push_str(&html_line),
|
||||||
|
Err(_) => result_html.push_str(&escape_html(&line_with_newline)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
result_html.push_str(&escape_html(&line_with_newline));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result_html
|
||||||
|
} else {
|
||||||
|
escape_html(code)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
escape_html(code)
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"<div class="code-block-wrapper">
|
||||||
|
<div class="code-header">
|
||||||
|
<span class="code-lang">{lang_escaped}</span>
|
||||||
|
<button class="copy-btn" onclick="copyCode(this)">Copy</button>
|
||||||
|
</div>
|
||||||
|
<pre style="margin: 0; border-radius: 0 0 6px 6px;"><code>{highlighted_html}</code></pre>
|
||||||
|
</div>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_html(text: &str) -> String {
|
||||||
|
text.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
}
|
||||||
214
templates/base.html
Normal file
214
templates/base.html
Normal file
@ -0,0 +1,214 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>{% block title %}Note{% endblock %}</title>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #e0e0e0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 40px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-width: 800px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
a { color: #bb86fc; text-decoration: none !important; }
|
||||||
|
h1, h2, h3, h4 { color: #ffffff; margin-top: 1.5em; margin-bottom: 0.5em; }
|
||||||
|
h1 { border-bottom: 1px solid #333; padding-bottom: 10px; }
|
||||||
|
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
||||||
|
th, td { border: 1px solid #444; padding: 8px; text-align: left; }
|
||||||
|
th { background-color: #2c2c2c; }
|
||||||
|
blockquote {
|
||||||
|
border-left: 4px solid #bb86fc;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding-left: 1em;
|
||||||
|
color: #aaa;
|
||||||
|
background: #252525;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
code { font-family: 'Consolas', 'Monaco', monospace; }
|
||||||
|
p > code, li > code {
|
||||||
|
background-color: #2c2c2c;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #ff79c6;
|
||||||
|
}
|
||||||
|
.header a { padding: 1em; color: #757575; font-size: 0.95em; }
|
||||||
|
.code-block-wrapper {
|
||||||
|
margin: 1em 0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 6px;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #2b303b;
|
||||||
|
}
|
||||||
|
.code-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background-color: #232730;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-bottom: 1px solid #444;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #a0a0a0;
|
||||||
|
}
|
||||||
|
.code-lang { font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
|
.copy-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #555;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8em;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.copy-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
||||||
|
.copy-btn:active { transform: scale(0.95); }
|
||||||
|
|
||||||
|
.copy-path-btn {
|
||||||
|
background: transparent;
|
||||||
|
color: #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.8em;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
.copy-path-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
||||||
|
.copy-path-btn:active { transform: scale(0.95); }
|
||||||
|
pre { padding: 15px; overflow-x: auto; margin: 0; }
|
||||||
|
pre code { background: transparent; padding: 0; color: inherit; }
|
||||||
|
.mermaid-wrapper .mermaid {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border-top: 1px solid #333;
|
||||||
|
padding-top: 15px;
|
||||||
|
}
|
||||||
|
.footer a { padding: 1em; color: #757575; }
|
||||||
|
.back-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: #90a4ae;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.95em;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
.back-link span { margin-right: 8px; font-size: 1.2em; }
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 1.5em auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
}
|
||||||
|
ol {
|
||||||
|
padding-left: 1.5em;
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
padding: .5em 0;
|
||||||
|
}
|
||||||
|
ul > li::marker, ol > li::marker { display: inline-block; }
|
||||||
|
.file-link {
|
||||||
|
color: #64b5f6;
|
||||||
|
font-size: 1.1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.file-link:hover {
|
||||||
|
color: #90caf9;
|
||||||
|
}
|
||||||
|
.icon {
|
||||||
|
margin-right: 10px;
|
||||||
|
min-width: 24px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="content">
|
||||||
|
<div class="header">
|
||||||
|
{% block header %}
|
||||||
|
<a href="/" class="back-link" onmouseover="this.style.color='#ffffff'" onmouseout="this.style.color='#90a4ae'">
|
||||||
|
<span>←</span> Back
|
||||||
|
</a>
|
||||||
|
<a href="/random">🎲 Random note</a>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<a href="/">← Main</a>
|
||||||
|
<a href="/random">🎲 Random note</a>
|
||||||
|
{% block footer %}
|
||||||
|
<p>{{ package_name }} v{{ version }}</p>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
{% block scripts %}{% endblock %}
|
||||||
|
|
||||||
|
function copyCode(button) {
|
||||||
|
const wrapper = button.closest('.code-block-wrapper');
|
||||||
|
if (!wrapper) return;
|
||||||
|
const target = wrapper.querySelector('pre') || wrapper.querySelector('.mermaid');
|
||||||
|
if (!target) return;
|
||||||
|
const codeText = target.innerText;
|
||||||
|
|
||||||
|
navigator.clipboard.writeText(codeText).then(() => {
|
||||||
|
const originalText = button.innerText;
|
||||||
|
button.innerText = 'Copied!';
|
||||||
|
button.style.borderColor = '#2ecc71';
|
||||||
|
button.style.color = '#2ecc71';
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerText = originalText;
|
||||||
|
button.style.borderColor = '';
|
||||||
|
button.style.color = '';
|
||||||
|
}, 2000);
|
||||||
|
}).catch(err => {
|
||||||
|
console.error('Error copying:', err);
|
||||||
|
button.innerText = 'Error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPath(path) {
|
||||||
|
navigator.clipboard.writeText(path).then(() => {}).catch(err => {
|
||||||
|
console.error('Error copying:', err);
|
||||||
|
button.innerText = 'Error';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true,
|
||||||
|
theme: 'dark',
|
||||||
|
securityLevel: 'loose',
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,33 +1,34 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
{% block title %}Directory: /{{ title_path }}{% endblock %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
{% block header %}
|
||||||
<title>Обзор директории</title>
|
{% if !title_path.is_empty() %}
|
||||||
<style>
|
<a href="../" class="back-link">
|
||||||
body { background-color: #121212; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 40px 20px; display: flex; justify-content: center; }
|
<span>←</span> Up
|
||||||
.content { max-width: 800px; width: 100%; background-color: #1e1e1e; padding: 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
|
</a>
|
||||||
h1 { color: #ffffff; border-bottom: 1px solid #333; padding-bottom: 10px; margin-top: 0; }
|
{% else %}
|
||||||
a { text-decoration: none !important; transition: opacity 0.2s; }
|
<span style="color: #757575; font-size: 0.95em;">🏠 Root</span>
|
||||||
a:hover { opacity: 0.8; }
|
{% endif %}
|
||||||
ul { list-style-type: none; padding: 0; margin: 0; }
|
<a href="/random">🎲 Random note</a>
|
||||||
li { padding: 10px 0; border-bottom: 1px solid #333; transition: background-color 0.2s; }
|
{% endblock %}
|
||||||
.file-link { color: #64b5f6; text-decoration: none !important; font-size: 1.1em; display: flex; align-items: center; }
|
|
||||||
.file-link:hover { color: #90caf9; }
|
{% block content %}
|
||||||
.back-link { color: #90a4ae; font-weight: bold; text-decoration: none !important; display: block; }
|
<h1>📂 Directory: /{{ title_path }}</h1>
|
||||||
.icon { margin-right: 10px; min-width: 24px; }
|
<ul>
|
||||||
.footer { margin-top: 30px; color: #666; font-size: 0.9em; border-top: 1px solid #333; padding-top: 15px; }
|
{% if !title_path.is_empty() %}
|
||||||
.footer a { color: #757575; }
|
<li>
|
||||||
</style>
|
<a href="../" class="back-link">📁 ..</a>
|
||||||
</head>
|
</li>
|
||||||
<body>
|
{% endif %}
|
||||||
<div class="content">
|
|
||||||
<h1>📂 Обзор директории: /{{TITLE_PATH}}</h1>
|
{% for file in files %}
|
||||||
{{FILE_LIST}}
|
<li>
|
||||||
<div class="footer">
|
<a href="/{{ file.link }}" class="file-link">
|
||||||
<a href="/">← На главную</a>
|
<span class="icon">{% if file.is_dir %}📁{% else %}📄{% endif %}</span>
|
||||||
<a href="/random">🎲 Случайный файл</a>
|
<span>{{ file.name }}</span>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
</body>
|
{% endfor %}
|
||||||
</html>
|
</ul>
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -1,62 +1,21 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html lang="ru">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Markdown Preview</title>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
|
||||||
<style>
|
|
||||||
body { background-color: #121212; color: #e0e0e0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 40px 20px; display: flex; justify-content: center; line-height: 1.6; }
|
|
||||||
.content { max-width: 800px; width: 100%; background-color: #1e1e1e; padding: 40px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
|
|
||||||
a { color: #bb86fc; text-decoration: none !important; }
|
|
||||||
h1, h2, h3, h4 { color: #ffffff; margin-top: 1.5em; margin-bottom: 0.5em; }
|
|
||||||
h1 { border-bottom: 1px solid #333; padding-bottom: 10px; }
|
|
||||||
table { border-collapse: collapse; width: 100%; margin: 1em 0; }
|
|
||||||
th, td { border: 1px solid #444; padding: 8px; text-align: left; }
|
|
||||||
th { background-color: #2c2c2c; }
|
|
||||||
blockquote { border-left: 4px solid #bb86fc; margin: 1em 0; padding-left: 1em; color: #aaa; background: #252525; padding: 10px; }
|
|
||||||
code { font-family: 'Consolas', 'Monaco', monospace; }
|
|
||||||
p > code, li > code { background-color: #2c2c2c; padding: 2px 6px; border-radius: 4px; color: #ff79c6; }
|
|
||||||
|
|
||||||
.code-block-wrapper { margin: 1em 0; border: 1px solid #444; border-radius: 6px; overflow: hidden; background-color: #2b303b; }
|
{% block title %}{{ filename }}{% endblock %}
|
||||||
.code-header { display: flex; justify-content: space-between; align-items: center; background-color: #232730; padding: 6px 12px; border-bottom: 1px solid #444; font-size: 0.85em; color: #a0a0a0; }
|
|
||||||
.code-lang { font-weight: bold; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
||||||
.copy-btn { background: transparent; border: 1px solid #555; color: #ccc; padding: 2px 8px; border-radius: 4px; cursor: pointer; font-size: 0.8em; transition: all 0.2s; }
|
|
||||||
.copy-btn:hover { background-color: #444; color: #fff; border-color: #777; }
|
|
||||||
.copy-btn:active { transform: scale(0.95); }
|
|
||||||
pre { padding: 15px; overflow-x: auto; margin: 0; }
|
|
||||||
pre code { background: transparent; padding: 0; color: inherit; }
|
|
||||||
|
|
||||||
.mermaid-wrapper .mermaid { background-color: #f9f9f9; border-radius: 0 0 6px 6px; display: flex; justify-content: center; }
|
{% block header %}
|
||||||
|
<a href="{{ back_link }}" class="back-link" onmouseover="this.style.color='#ffffff'" onmouseout="this.style.color='#90a4ae'">
|
||||||
#status { position: fixed; top: 10px; right: 10px; padding: 5px 10px; border-radius: 4px; font-size: 12px; font-weight: bold; }
|
<span>←</span> Back
|
||||||
.connected { background-color: #2ecc71; color: #000; }
|
|
||||||
.disconnected { background-color: #e74c3c; color: #fff; }
|
|
||||||
.reconnecting { background-color: #f1c40f; color: #000; }
|
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
margin: 1.5em auto;
|
|
||||||
border-radius: 6px;
|
|
||||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="content">
|
|
||||||
<div style="margin-bottom: 20px;">
|
|
||||||
<a href="{{BACK_LINK}}" style="display: inline-flex; align-items: center; color: #90a4ae; text-decoration: none; font-size: 0.95em; transition: color 0.2s;"
|
|
||||||
onmouseover="this.style.color='#ffffff'" onmouseout="this.style.color='#90a4ae'">
|
|
||||||
<span style="margin-right: 8px; font-size: 1.2em;">←</span> Назад
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<a href="/random">🎲 Random note</a>
|
||||||
{{CONTENT}}
|
<a class="copy-path-btn" onclick="copyPath('{{ copy_path }}')">Copy edit command</a>
|
||||||
</div>
|
{% endblock %}
|
||||||
|
|
||||||
<script>
|
{% block content %}
|
||||||
const sseUrl = "{{SSE_URL}}";
|
{{ content|safe }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
const sseUrl = "{{ sse_url }}";
|
||||||
|
|
||||||
function connect() {
|
function connect() {
|
||||||
const evtSource = new EventSource(sseUrl);
|
const evtSource = new EventSource(sseUrl);
|
||||||
@ -65,40 +24,9 @@
|
|||||||
setTimeout(connect, 3000);
|
setTimeout(connect, 3000);
|
||||||
};
|
};
|
||||||
evtSource.addEventListener("reload", (event) => {
|
evtSource.addEventListener("reload", (event) => {
|
||||||
console.log("Получено событие обновления");
|
console.log("Get new update event");
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
connect();
|
connect();
|
||||||
|
{% endblock %}
|
||||||
function copyCode(button) {
|
|
||||||
const wrapper = button.closest('.code-block-wrapper');
|
|
||||||
if (!wrapper) return;
|
|
||||||
const target = wrapper.querySelector('pre') || wrapper.querySelector('.mermaid');
|
|
||||||
if (!target) return;
|
|
||||||
const codeText = target.innerText;
|
|
||||||
|
|
||||||
navigator.clipboard.writeText(codeText).then(() => {
|
|
||||||
const originalText = button.innerText;
|
|
||||||
button.innerText = 'Copied!';
|
|
||||||
button.style.borderColor = '#2ecc71';
|
|
||||||
button.style.color = '#2ecc71';
|
|
||||||
setTimeout(() => {
|
|
||||||
button.innerText = originalText;
|
|
||||||
button.style.borderColor = '';
|
|
||||||
button.style.color = '';
|
|
||||||
}, 2000);
|
|
||||||
}).catch(err => {
|
|
||||||
console.error('Ошибка копирования:', err);
|
|
||||||
button.innerText = 'Error';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
mermaid.initialize({
|
|
||||||
startOnLoad: true,
|
|
||||||
theme: 'dark',
|
|
||||||
securityLevel: 'loose',
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user