add sse to watch file changes
This commit is contained in:
parent
f49895c5f8
commit
40bf0f413b
308
Cargo.lock
generated
308
Cargo.lock
generated
@ -104,6 +104,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
@ -141,6 +147,21 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.6"
|
||||
@ -166,6 +187,17 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"libredox",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
@ -197,6 +229,30 @@ dependencies = [
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fsevent-sys"
|
||||
version = "4.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.32"
|
||||
@ -204,6 +260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -212,6 +269,40 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
@ -224,8 +315,13 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
@ -336,12 +432,52 @@ dependencies = [
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify-sys"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "kqueue"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
|
||||
dependencies = [
|
||||
"kqueue-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kqueue-sys"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
@ -354,6 +490,17 @@ version = "0.2.182"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d0b95e02c851351f877147b7deea7b1afb1df71b63aa5f8270716e0c5720616"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"redox_syscall 0.7.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
@ -395,11 +542,13 @@ name = "md-renderer"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"futures",
|
||||
"notify",
|
||||
"pulldown-cmark",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syntect",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
@ -425,6 +574,18 @@ dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
@ -436,6 +597,25 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@ -463,7 +643,7 @@ version = "6.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "336b9c63443aceef14bea841b899035ae3abe89b7c486aaf4c5bd8aafedac3f0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"onig_sys",
|
||||
@ -497,7 +677,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall",
|
||||
"redox_syscall 0.5.18",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
@ -560,7 +740,7 @@ version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57206b407293d2bcd3af849ce869d52068623f19e1b5ff8e8778e3309439682b"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
"getopts",
|
||||
"memchr",
|
||||
"unicase",
|
||||
@ -590,7 +770,16 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35985aa610addc02e24fc232012c86fd11f14111180f902b67e2d5331f8ebf2b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -862,7 +1051,7 @@ checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"libc",
|
||||
"mio",
|
||||
"mio 1.1.1",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
@ -882,6 +1071,31 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.5.3"
|
||||
@ -1015,13 +1229,22 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.53.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -1033,6 +1256,21 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm 0.48.5",
|
||||
"windows_aarch64_msvc 0.48.5",
|
||||
"windows_i686_gnu 0.48.5",
|
||||
"windows_i686_msvc 0.48.5",
|
||||
"windows_x86_64_gnu 0.48.5",
|
||||
"windows_x86_64_gnullvm 0.48.5",
|
||||
"windows_x86_64_msvc 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.53.5"
|
||||
@ -1040,28 +1278,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_aarch64_gnullvm 0.53.1",
|
||||
"windows_aarch64_msvc 0.53.1",
|
||||
"windows_i686_gnu 0.53.1",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_i686_msvc 0.53.1",
|
||||
"windows_x86_64_gnu 0.53.1",
|
||||
"windows_x86_64_gnullvm 0.53.1",
|
||||
"windows_x86_64_msvc 0.53.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.53.1"
|
||||
@ -1074,24 +1330,48 @@ version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.53.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.53.1"
|
||||
|
||||
@ -8,6 +8,8 @@ pulldown-cmark = "0.9"
|
||||
axum = "0.7"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
syntect = "5.0"
|
||||
notify = "6.1" # Для слежения за файлами
|
||||
tokio-stream = { version = "0.1", features = ["sync"] }
|
||||
futures = "0.3" # Утилиты для стримов
|
||||
|
||||
212
src/main.rs
212
src/main.rs
@ -2,14 +2,20 @@ use axum::{
|
||||
extract::{Path, State},
|
||||
routing::get,
|
||||
Router,
|
||||
http::StatusCode,
|
||||
response::Html,
|
||||
http::{StatusCode, HeaderMap, header},
|
||||
response::{Html, Sse, IntoResponse},
|
||||
};
|
||||
use pulldown_cmark::{Parser, Options, html, Event, Tag, CodeBlockKind};
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_stream::wrappers::BroadcastStream;
|
||||
use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher, EventKind};
|
||||
use futures::StreamExt;
|
||||
use std::convert::Infallible;
|
||||
use std::time::Duration;
|
||||
|
||||
// Импорт для syntect
|
||||
use syntect::easy::HighlightLines;
|
||||
@ -17,33 +23,42 @@ use syntect::highlighting::ThemeSet;
|
||||
use syntect::html::{styled_line_to_highlighted_html, IncludeBackground};
|
||||
use syntect::parsing::SyntaxSet;
|
||||
|
||||
/// Храним тяжелые ресурсы в состоянии приложения, чтобы не грузить их при каждом запросе
|
||||
/// Храним тяжелые ресурсы и канал для уведомлений
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
syntax_set: Arc<SyntaxSet>,
|
||||
theme_set: Arc<ThemeSet>,
|
||||
tx: Arc<broadcast::Sender<String>>,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
// Инициализация логгера
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
// Загружаем синтаксисы и темы ОДИН РАЗ при старте
|
||||
// Это может занять несколько сотен миллисекунд, поэтому делаем это до запуска сервера
|
||||
println!("Загрузка баз синтаксисов и тем...");
|
||||
let ss = SyntaxSet::load_defaults_newlines();
|
||||
let ts = ThemeSet::load_defaults();
|
||||
|
||||
// Создаем канал broadcast для SSE
|
||||
let (tx, _rx) = broadcast::channel::<String>(100);
|
||||
|
||||
let state = AppState {
|
||||
syntax_set: Arc::new(ss),
|
||||
theme_set: Arc::new(ts),
|
||||
tx: Arc::new(tx),
|
||||
};
|
||||
|
||||
// Запускаем глобальный вотчер в отдельной задаче
|
||||
let watcher_state = state.clone();
|
||||
tokio::spawn(async move {
|
||||
run_file_watcher(watcher_state).await;
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/", get(root))
|
||||
.route("/*path", get(serve_file))
|
||||
.with_state(state); // Передаем состояние в роутер
|
||||
.route("/events/*path", get(sse_handler))
|
||||
.with_state(state);
|
||||
|
||||
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
|
||||
println!("Сервер запущен на http://{}", addr);
|
||||
@ -57,6 +72,49 @@ async fn root() -> Html<&'static str> {
|
||||
Html("<h1 style='color:white; text-align:center;'>Markdown Server</h1><p style='color:#aaa; text-align:center;'>Перейдите на <a href='/files/example.md'>/files/example.md</a></p>")
|
||||
}
|
||||
|
||||
async fn sse_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(full_path): Path<String>,
|
||||
) -> impl IntoResponse {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(header::CONTENT_TYPE, header::HeaderValue::from_static("text/event-stream"));
|
||||
headers.insert(header::CACHE_CONTROL, header::HeaderValue::from_static("no-cache"));
|
||||
headers.insert(header::CONNECTION, header::HeaderValue::from_static("keep-alive"));
|
||||
|
||||
let rx = state.tx.subscribe();
|
||||
let requested_path = full_path.clone();
|
||||
|
||||
let stream = BroadcastStream::new(rx)
|
||||
.filter_map(move |res| {
|
||||
let req_path = requested_path.clone();
|
||||
async move {
|
||||
match res {
|
||||
Ok(changed_path) => {
|
||||
if changed_path.contains(&req_path) {
|
||||
// Явно указываем типы: Ok<Event, Infallible>
|
||||
Some(Ok::<axum::response::sse::Event, Infallible>(
|
||||
axum::response::sse::Event::default()
|
||||
.event("reload")
|
||||
.data("")
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let sse = Sse::new(stream)
|
||||
.keep_alive(
|
||||
axum::response::sse::KeepAlive::new()
|
||||
.interval(Duration::from_secs(15))
|
||||
.text("ping")
|
||||
);
|
||||
|
||||
(headers, sse)
|
||||
}
|
||||
async fn serve_file(
|
||||
State(state): State<AppState>,
|
||||
Path(full_path): Path<String>,
|
||||
@ -65,17 +123,14 @@ async fn serve_file(
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
// --- БЕЗОПАСНАЯ ПРОВЕРКА ПУТИ ---
|
||||
let mut requested_path = PathBuf::from("./notes");
|
||||
requested_path.push(&full_path);
|
||||
|
||||
// Канонизируем путь (разрешаем симлинки, убираем ../)
|
||||
let safe_path = match fs::canonicalize(&requested_path).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Err(StatusCode::NOT_FOUND),
|
||||
};
|
||||
|
||||
// Убеждаемся, что файл лежит внутри папки ./notes
|
||||
let base_dir = match fs::canonicalize("./notes").await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return Err(StatusCode::INTERNAL_SERVER_ERROR),
|
||||
@ -86,7 +141,6 @@ async fn serve_file(
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
// Чтение файла
|
||||
let content = match fs::read_to_string(&safe_path).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
@ -95,15 +149,50 @@ async fn serve_file(
|
||||
}
|
||||
};
|
||||
|
||||
// Конвертация Markdown -> HTML с подсветкой
|
||||
let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set);
|
||||
let html_content = markdown_to_html(&content, &state.syntax_set, &state.theme_set, &full_path);
|
||||
|
||||
Ok(Html(html_content))
|
||||
}
|
||||
|
||||
/// Основная функция конвертации
|
||||
fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
// Выбираем тему (base16-ocean.dark отлично смотрится)
|
||||
/// Запуск наблюдателя за файловой системой
|
||||
async fn run_file_watcher(state: AppState) {
|
||||
// Создаем канал MPSC для передачи путей из колбэка notify в основной цикл
|
||||
let (tx_fs, mut rx_fs) = tokio::sync::mpsc::channel::<PathBuf>(100);
|
||||
|
||||
// Настраиваем notify watcher
|
||||
// Перемещаем tx_fs внутрь замыкания через clone
|
||||
let tx_fs_clone = tx_fs.clone();
|
||||
let mut watcher = RecommendedWatcher::new(
|
||||
move |res: Result<notify::Event, notify::Error>| {
|
||||
if let Ok(event) = res {
|
||||
if 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 = PathBuf::from("./notes");
|
||||
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::Recursive) {
|
||||
eprintln!("Ошибка настройки watcher: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
println!("Watcher запущен для директории: {:?}", watch_path);
|
||||
|
||||
// Цикл обработки событий от файловой системы
|
||||
while let Some(path) = rx_fs.recv().await {
|
||||
if let Some(path_str) = path.to_str() {
|
||||
let _ = state.tx.send(path_str.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet, file_path: &str) -> String {
|
||||
let theme = &ts.themes["base16-ocean.dark"];
|
||||
|
||||
let mut options = Options::empty();
|
||||
@ -115,10 +204,7 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
|
||||
let parser = Parser::new_ext(markdown, options);
|
||||
|
||||
// --- ЭТАП 1: Обработка событий для замены блоков кода ---
|
||||
// Мы собираем новые события, где блоки кода заменены на сырой HTML с подсветкой
|
||||
let mut processed_events: Vec<Event> = Vec::new();
|
||||
|
||||
let mut in_code_block = false;
|
||||
let mut current_lang: Option<String> = None;
|
||||
let mut current_code = String::new();
|
||||
@ -137,13 +223,26 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
Event::End(Tag::CodeBlock(_)) => {
|
||||
in_code_block = false;
|
||||
|
||||
// Генерируем подсвеченный HTML
|
||||
// Исправленная подсветка синтаксиса (построчно)
|
||||
let highlighted_html = if let Some(lang) = ¤t_lang {
|
||||
if let Some(syntax) = ss.find_syntax_by_token(lang) {
|
||||
let mut h = HighlightLines::new(syntax, theme);
|
||||
let regions = h.highlight(¤t_code, ss);
|
||||
styled_line_to_highlighted_html(®ions[..], IncludeBackground::No)
|
||||
.unwrap_or_else(|_| escape_html(¤t_code))
|
||||
let mut result_html = String::new();
|
||||
|
||||
// Разбиваем код на строки и подсвечиваем каждую отдельно
|
||||
for line in current_code.lines() {
|
||||
// Добавляем перевод строки обратно, так как lines() удаляет его
|
||||
let line_with_newline = format!("{}\n", line);
|
||||
match h.highlight_line(&line_with_newline, ss) {
|
||||
Ok(regions) => {
|
||||
let html_line = styled_line_to_highlighted_html(®ions[..], IncludeBackground::No)
|
||||
.unwrap_or_else(|_| escape_html(&line_with_newline));
|
||||
result_html.push_str(&html_line);
|
||||
},
|
||||
Err(_) => result_html.push_str(&escape_html(&line_with_newline)),
|
||||
}
|
||||
}
|
||||
result_html
|
||||
} else {
|
||||
escape_html(¤t_code)
|
||||
}
|
||||
@ -151,21 +250,16 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
escape_html(¤t_code)
|
||||
};
|
||||
|
||||
// Формируем тег <pre><code>...</code></pre> с нашими стилями
|
||||
let full_html = format!(
|
||||
r#"<pre style="background-color: #2b303b; border-radius: 6px; padding: 15px; overflow-x: auto; margin: 1em 0; border: 1px solid #444;"><code>{}</code></pre>"#,
|
||||
highlighted_html
|
||||
);
|
||||
|
||||
// Вставляем как сырое HTML событие
|
||||
processed_events.push(Event::Html(full_html.into()));
|
||||
},
|
||||
Event::Text(text) if in_code_block => {
|
||||
// Накопление текста кода (не добавляем в events сразу)
|
||||
current_code.push_str(&text);
|
||||
},
|
||||
_ => {
|
||||
// Все остальные события (заголовки, параграфы, обычный текст) пропускаем как есть
|
||||
if !in_code_block {
|
||||
processed_events.push(event);
|
||||
}
|
||||
@ -173,11 +267,12 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
// --- ЭТАП 2: Рендеринг итогового HTML ---
|
||||
let mut body_html = String::new();
|
||||
html::push_html(&mut body_html, processed_events.into_iter());
|
||||
|
||||
// Оборачиваем в полный HTML документ с темной темой
|
||||
let sse_url = format!("/events/{}", file_path);
|
||||
|
||||
// Обратите внимание на двойные фигурные скобки {{ и }} для экранирования в format!
|
||||
format!(
|
||||
r#"<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
@ -186,24 +281,8 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Markdown Preview</title>
|
||||
<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);
|
||||
}}
|
||||
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; }}
|
||||
a:hover {{ text-decoration: underline; }}
|
||||
h1, h2, h3, h4 {{ color: #ffffff; margin-top: 1.5em; margin-bottom: 0.5em; }}
|
||||
@ -213,26 +292,45 @@ fn markdown_to_html(markdown: &str, ss: &SyntaxSet, ts: &ThemeSet) -> String {
|
||||
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;
|
||||
}}
|
||||
p > code, li > code {{ background-color: #2c2c2c; padding: 2px 6px; border-radius: 4px; color: #ff79c6; }}
|
||||
|
||||
#status {{ position: fixed; top: 10px; right: 10px; padding: 5px 10px; border-radius: 4px; font-size: 12px; font-weight: bold; }}
|
||||
.connected {{ background-color: #2ecc71; color: #000; }}
|
||||
.disconnected {{ background-color: #e74c3c; color: #fff; }}
|
||||
.reconnecting {{ background-color: #f1c40f; color: #000; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content">
|
||||
{}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const sseUrl = "{}";
|
||||
|
||||
function connect() {{
|
||||
const evtSource = new EventSource(sseUrl);
|
||||
|
||||
evtSource.onerror = (err) => {{
|
||||
evtSource.close();
|
||||
setTimeout(connect, 3000);
|
||||
}};
|
||||
|
||||
evtSource.addEventListener("reload", (event) => {{
|
||||
console.log("Получено событие обновления");
|
||||
location.reload();
|
||||
}});
|
||||
}}
|
||||
|
||||
connect();
|
||||
</script>
|
||||
</body>
|
||||
</html>"#,
|
||||
body_html
|
||||
body_html,
|
||||
sse_url
|
||||
)
|
||||
}
|
||||
|
||||
// Функция экранирования HTML для случаев, когда синтаксис не найден или это простой блок кода
|
||||
fn escape_html(text: &str) -> String {
|
||||
text.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user