├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── assets │ ├── demo.png │ ├── logo.png │ └── xhs_demo.png ├── index.md └── markdown-guide.md ├── src ├── bin │ └── medup │ │ ├── config.rs │ │ ├── main.rs │ │ ├── render_html.rs │ │ └── serve.rs ├── html.rs ├── lexer.rs ├── lib.rs ├── markdown.rs ├── parser.rs └── utils │ ├── cursor.rs │ ├── mod.rs │ └── stack.rs └── themes ├── github ├── config.json └── template.txt ├── hard ├── config.json ├── dark.css ├── highlight_code.js ├── light.css ├── reboto │ ├── Roboto-Black.ttf │ ├── Roboto-BlackItalic.ttf │ ├── Roboto-Bold.ttf │ ├── Roboto-BoldItalic.ttf │ ├── Roboto-Italic.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-LightItalic.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-MediumItalic.ttf │ ├── Roboto-Regular.ttf │ ├── Roboto-Thin.ttf │ ├── Roboto-ThinItalic.ttf │ ├── SourceCodePro-Regular.ttf │ ├── fonts.css │ ├── old-slate-colors.css │ └── slate-colors.css └── template.txt ├── notion ├── config.json ├── notion-light-enhanced.css └── template.txt └── xhs ├── config.json ├── dark.css ├── light.css ├── logo.png ├── slice_bg.png └── template.txt /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .DS_Store 3 | .VSCodeCounter 4 | 5 | crdt.md 6 | themes/_* 7 | docs/_* 8 | 9 | /posts 10 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "0.7.20" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "autocfg" 16 | version = "1.1.0" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 19 | 20 | [[package]] 21 | name = "base64" 22 | version = "0.13.1" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 25 | 26 | [[package]] 27 | name = "bitflags" 28 | version = "1.3.2" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 31 | 32 | [[package]] 33 | name = "block-buffer" 34 | version = "0.10.4" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 37 | dependencies = [ 38 | "generic-array", 39 | ] 40 | 41 | [[package]] 42 | name = "buf_redux" 43 | version = "0.8.4" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f" 46 | dependencies = [ 47 | "memchr", 48 | "safemem", 49 | ] 50 | 51 | [[package]] 52 | name = "byteorder" 53 | version = "1.4.3" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" 56 | 57 | [[package]] 58 | name = "bytes" 59 | version = "1.4.0" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" 62 | 63 | [[package]] 64 | name = "cc" 65 | version = "1.0.79" 66 | source = "registry+https://github.com/rust-lang/crates.io-index" 67 | checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" 68 | 69 | [[package]] 70 | name = "cfg-if" 71 | version = "1.0.0" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 74 | 75 | [[package]] 76 | name = "clap" 77 | version = "4.1.13" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "3c911b090850d79fc64fe9ea01e28e465f65e821e08813ced95bced72f7a8a9b" 80 | dependencies = [ 81 | "bitflags", 82 | "clap_lex", 83 | "is-terminal", 84 | "strsim", 85 | "termcolor", 86 | ] 87 | 88 | [[package]] 89 | name = "clap_lex" 90 | version = "0.3.3" 91 | source = "registry+https://github.com/rust-lang/crates.io-index" 92 | checksum = "033f6b7a4acb1f358c742aaca805c939ee73b4c6209ae4318ec7aca81c42e646" 93 | dependencies = [ 94 | "os_str_bytes", 95 | ] 96 | 97 | [[package]] 98 | name = "cpufeatures" 99 | version = "0.2.6" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" 102 | dependencies = [ 103 | "libc", 104 | ] 105 | 106 | [[package]] 107 | name = "crypto-common" 108 | version = "0.1.6" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 111 | dependencies = [ 112 | "generic-array", 113 | "typenum", 114 | ] 115 | 116 | [[package]] 117 | name = "digest" 118 | version = "0.10.6" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" 121 | dependencies = [ 122 | "block-buffer", 123 | "crypto-common", 124 | ] 125 | 126 | [[package]] 127 | name = "either" 128 | version = "1.8.1" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" 131 | 132 | [[package]] 133 | name = "email_address" 134 | version = "0.2.4" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" 137 | dependencies = [ 138 | "serde", 139 | ] 140 | 141 | [[package]] 142 | name = "errno" 143 | version = "0.2.8" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" 146 | dependencies = [ 147 | "errno-dragonfly", 148 | "libc", 149 | "winapi", 150 | ] 151 | 152 | [[package]] 153 | name = "errno-dragonfly" 154 | version = "0.1.2" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" 157 | dependencies = [ 158 | "cc", 159 | "libc", 160 | ] 161 | 162 | [[package]] 163 | name = "fastrand" 164 | version = "1.9.0" 165 | source = "registry+https://github.com/rust-lang/crates.io-index" 166 | checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" 167 | dependencies = [ 168 | "instant", 169 | ] 170 | 171 | [[package]] 172 | name = "fnv" 173 | version = "1.0.7" 174 | source = "registry+https://github.com/rust-lang/crates.io-index" 175 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 176 | 177 | [[package]] 178 | name = "form_urlencoded" 179 | version = "1.1.0" 180 | source = "registry+https://github.com/rust-lang/crates.io-index" 181 | checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" 182 | dependencies = [ 183 | "percent-encoding", 184 | ] 185 | 186 | [[package]] 187 | name = "futures-channel" 188 | version = "0.3.27" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" 191 | dependencies = [ 192 | "futures-core", 193 | "futures-sink", 194 | ] 195 | 196 | [[package]] 197 | name = "futures-core" 198 | version = "0.3.27" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" 201 | 202 | [[package]] 203 | name = "futures-sink" 204 | version = "0.3.27" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" 207 | 208 | [[package]] 209 | name = "futures-task" 210 | version = "0.3.27" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" 213 | 214 | [[package]] 215 | name = "futures-util" 216 | version = "0.3.27" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" 219 | dependencies = [ 220 | "futures-core", 221 | "futures-sink", 222 | "futures-task", 223 | "pin-project-lite", 224 | "pin-utils", 225 | "slab", 226 | ] 227 | 228 | [[package]] 229 | name = "generic-array" 230 | version = "0.14.6" 231 | source = "registry+https://github.com/rust-lang/crates.io-index" 232 | checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" 233 | dependencies = [ 234 | "typenum", 235 | "version_check", 236 | ] 237 | 238 | [[package]] 239 | name = "getrandom" 240 | version = "0.2.8" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" 243 | dependencies = [ 244 | "cfg-if", 245 | "libc", 246 | "wasi", 247 | ] 248 | 249 | [[package]] 250 | name = "h2" 251 | version = "0.3.16" 252 | source = "registry+https://github.com/rust-lang/crates.io-index" 253 | checksum = "5be7b54589b581f624f566bf5d8eb2bab1db736c51528720b6bd36b96b55924d" 254 | dependencies = [ 255 | "bytes", 256 | "fnv", 257 | "futures-core", 258 | "futures-sink", 259 | "futures-util", 260 | "http", 261 | "indexmap", 262 | "slab", 263 | "tokio", 264 | "tokio-util", 265 | "tracing", 266 | ] 267 | 268 | [[package]] 269 | name = "hashbrown" 270 | version = "0.12.3" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 273 | 274 | [[package]] 275 | name = "headers" 276 | version = "0.3.8" 277 | source = "registry+https://github.com/rust-lang/crates.io-index" 278 | checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" 279 | dependencies = [ 280 | "base64", 281 | "bitflags", 282 | "bytes", 283 | "headers-core", 284 | "http", 285 | "httpdate", 286 | "mime", 287 | "sha1", 288 | ] 289 | 290 | [[package]] 291 | name = "headers-core" 292 | version = "0.2.0" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" 295 | dependencies = [ 296 | "http", 297 | ] 298 | 299 | [[package]] 300 | name = "hermit-abi" 301 | version = "0.2.6" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" 304 | dependencies = [ 305 | "libc", 306 | ] 307 | 308 | [[package]] 309 | name = "hermit-abi" 310 | version = "0.3.1" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" 313 | 314 | [[package]] 315 | name = "http" 316 | version = "0.2.9" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" 319 | dependencies = [ 320 | "bytes", 321 | "fnv", 322 | "itoa", 323 | ] 324 | 325 | [[package]] 326 | name = "http-body" 327 | version = "0.4.5" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" 330 | dependencies = [ 331 | "bytes", 332 | "http", 333 | "pin-project-lite", 334 | ] 335 | 336 | [[package]] 337 | name = "httparse" 338 | version = "1.8.0" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 341 | 342 | [[package]] 343 | name = "httpdate" 344 | version = "1.0.2" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" 347 | 348 | [[package]] 349 | name = "hyper" 350 | version = "0.14.25" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" 353 | dependencies = [ 354 | "bytes", 355 | "futures-channel", 356 | "futures-core", 357 | "futures-util", 358 | "h2", 359 | "http", 360 | "http-body", 361 | "httparse", 362 | "httpdate", 363 | "itoa", 364 | "pin-project-lite", 365 | "socket2", 366 | "tokio", 367 | "tower-service", 368 | "tracing", 369 | "want", 370 | ] 371 | 372 | [[package]] 373 | name = "idna" 374 | version = "0.3.0" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" 377 | dependencies = [ 378 | "unicode-bidi", 379 | "unicode-normalization", 380 | ] 381 | 382 | [[package]] 383 | name = "indexmap" 384 | version = "1.9.3" 385 | source = "registry+https://github.com/rust-lang/crates.io-index" 386 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 387 | dependencies = [ 388 | "autocfg", 389 | "hashbrown", 390 | ] 391 | 392 | [[package]] 393 | name = "instant" 394 | version = "0.1.12" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" 397 | dependencies = [ 398 | "cfg-if", 399 | ] 400 | 401 | [[package]] 402 | name = "io-lifetimes" 403 | version = "1.0.9" 404 | source = "registry+https://github.com/rust-lang/crates.io-index" 405 | checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" 406 | dependencies = [ 407 | "hermit-abi 0.3.1", 408 | "libc", 409 | "windows-sys 0.45.0", 410 | ] 411 | 412 | [[package]] 413 | name = "is-terminal" 414 | version = "0.4.5" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | checksum = "8687c819457e979cc940d09cb16e42a1bf70aa6b60a549de6d3a62a0ee90c69e" 417 | dependencies = [ 418 | "hermit-abi 0.3.1", 419 | "io-lifetimes", 420 | "rustix", 421 | "windows-sys 0.45.0", 422 | ] 423 | 424 | [[package]] 425 | name = "itertools" 426 | version = "0.10.5" 427 | source = "registry+https://github.com/rust-lang/crates.io-index" 428 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 429 | dependencies = [ 430 | "either", 431 | ] 432 | 433 | [[package]] 434 | name = "itoa" 435 | version = "1.0.6" 436 | source = "registry+https://github.com/rust-lang/crates.io-index" 437 | checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" 438 | 439 | [[package]] 440 | name = "lazy_static" 441 | version = "1.4.0" 442 | source = "registry+https://github.com/rust-lang/crates.io-index" 443 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 444 | 445 | [[package]] 446 | name = "libc" 447 | version = "0.2.140" 448 | source = "registry+https://github.com/rust-lang/crates.io-index" 449 | checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" 450 | 451 | [[package]] 452 | name = "linux-raw-sys" 453 | version = "0.1.4" 454 | source = "registry+https://github.com/rust-lang/crates.io-index" 455 | checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" 456 | 457 | [[package]] 458 | name = "lock_api" 459 | version = "0.4.9" 460 | source = "registry+https://github.com/rust-lang/crates.io-index" 461 | checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" 462 | dependencies = [ 463 | "autocfg", 464 | "scopeguard", 465 | ] 466 | 467 | [[package]] 468 | name = "log" 469 | version = "0.4.17" 470 | source = "registry+https://github.com/rust-lang/crates.io-index" 471 | checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" 472 | dependencies = [ 473 | "cfg-if", 474 | ] 475 | 476 | [[package]] 477 | name = "medup" 478 | version = "0.1.0" 479 | dependencies = [ 480 | "clap", 481 | "email_address", 482 | "itertools", 483 | "lazy_static", 484 | "regex", 485 | "serde", 486 | "serde_json", 487 | "tinytemplate", 488 | "tokio", 489 | "url", 490 | "utf8_slice", 491 | "v_htmlescape", 492 | "warp", 493 | ] 494 | 495 | [[package]] 496 | name = "memchr" 497 | version = "2.5.0" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" 500 | 501 | [[package]] 502 | name = "mime" 503 | version = "0.3.17" 504 | source = "registry+https://github.com/rust-lang/crates.io-index" 505 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 506 | 507 | [[package]] 508 | name = "mime_guess" 509 | version = "2.0.4" 510 | source = "registry+https://github.com/rust-lang/crates.io-index" 511 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 512 | dependencies = [ 513 | "mime", 514 | "unicase", 515 | ] 516 | 517 | [[package]] 518 | name = "mio" 519 | version = "0.8.6" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" 522 | dependencies = [ 523 | "libc", 524 | "log", 525 | "wasi", 526 | "windows-sys 0.45.0", 527 | ] 528 | 529 | [[package]] 530 | name = "multipart" 531 | version = "0.18.0" 532 | source = "registry+https://github.com/rust-lang/crates.io-index" 533 | checksum = "00dec633863867f29cb39df64a397cdf4a6354708ddd7759f70c7fb51c5f9182" 534 | dependencies = [ 535 | "buf_redux", 536 | "httparse", 537 | "log", 538 | "mime", 539 | "mime_guess", 540 | "quick-error", 541 | "rand", 542 | "safemem", 543 | "tempfile", 544 | "twoway", 545 | ] 546 | 547 | [[package]] 548 | name = "num_cpus" 549 | version = "1.15.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" 552 | dependencies = [ 553 | "hermit-abi 0.2.6", 554 | "libc", 555 | ] 556 | 557 | [[package]] 558 | name = "once_cell" 559 | version = "1.17.1" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" 562 | 563 | [[package]] 564 | name = "os_str_bytes" 565 | version = "6.5.0" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" 568 | 569 | [[package]] 570 | name = "parking_lot" 571 | version = "0.12.1" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 574 | dependencies = [ 575 | "lock_api", 576 | "parking_lot_core", 577 | ] 578 | 579 | [[package]] 580 | name = "parking_lot_core" 581 | version = "0.9.7" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" 584 | dependencies = [ 585 | "cfg-if", 586 | "libc", 587 | "redox_syscall", 588 | "smallvec", 589 | "windows-sys 0.45.0", 590 | ] 591 | 592 | [[package]] 593 | name = "percent-encoding" 594 | version = "2.2.0" 595 | source = "registry+https://github.com/rust-lang/crates.io-index" 596 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" 597 | 598 | [[package]] 599 | name = "pin-project" 600 | version = "1.0.12" 601 | source = "registry+https://github.com/rust-lang/crates.io-index" 602 | checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" 603 | dependencies = [ 604 | "pin-project-internal", 605 | ] 606 | 607 | [[package]] 608 | name = "pin-project-internal" 609 | version = "1.0.12" 610 | source = "registry+https://github.com/rust-lang/crates.io-index" 611 | checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" 612 | dependencies = [ 613 | "proc-macro2", 614 | "quote", 615 | "syn 1.0.109", 616 | ] 617 | 618 | [[package]] 619 | name = "pin-project-lite" 620 | version = "0.2.9" 621 | source = "registry+https://github.com/rust-lang/crates.io-index" 622 | checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" 623 | 624 | [[package]] 625 | name = "pin-utils" 626 | version = "0.1.0" 627 | source = "registry+https://github.com/rust-lang/crates.io-index" 628 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 629 | 630 | [[package]] 631 | name = "ppv-lite86" 632 | version = "0.2.17" 633 | source = "registry+https://github.com/rust-lang/crates.io-index" 634 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 635 | 636 | [[package]] 637 | name = "proc-macro2" 638 | version = "1.0.53" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "ba466839c78239c09faf015484e5cc04860f88242cff4d03eb038f04b4699b73" 641 | dependencies = [ 642 | "unicode-ident", 643 | ] 644 | 645 | [[package]] 646 | name = "quick-error" 647 | version = "1.2.3" 648 | source = "registry+https://github.com/rust-lang/crates.io-index" 649 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 650 | 651 | [[package]] 652 | name = "quote" 653 | version = "1.0.26" 654 | source = "registry+https://github.com/rust-lang/crates.io-index" 655 | checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" 656 | dependencies = [ 657 | "proc-macro2", 658 | ] 659 | 660 | [[package]] 661 | name = "rand" 662 | version = "0.8.5" 663 | source = "registry+https://github.com/rust-lang/crates.io-index" 664 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 665 | dependencies = [ 666 | "libc", 667 | "rand_chacha", 668 | "rand_core", 669 | ] 670 | 671 | [[package]] 672 | name = "rand_chacha" 673 | version = "0.3.1" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 676 | dependencies = [ 677 | "ppv-lite86", 678 | "rand_core", 679 | ] 680 | 681 | [[package]] 682 | name = "rand_core" 683 | version = "0.6.4" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 686 | dependencies = [ 687 | "getrandom", 688 | ] 689 | 690 | [[package]] 691 | name = "redox_syscall" 692 | version = "0.2.16" 693 | source = "registry+https://github.com/rust-lang/crates.io-index" 694 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" 695 | dependencies = [ 696 | "bitflags", 697 | ] 698 | 699 | [[package]] 700 | name = "regex" 701 | version = "1.7.3" 702 | source = "registry+https://github.com/rust-lang/crates.io-index" 703 | checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" 704 | dependencies = [ 705 | "aho-corasick", 706 | "memchr", 707 | "regex-syntax", 708 | ] 709 | 710 | [[package]] 711 | name = "regex-syntax" 712 | version = "0.6.29" 713 | source = "registry+https://github.com/rust-lang/crates.io-index" 714 | checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" 715 | 716 | [[package]] 717 | name = "rustix" 718 | version = "0.36.11" 719 | source = "registry+https://github.com/rust-lang/crates.io-index" 720 | checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" 721 | dependencies = [ 722 | "bitflags", 723 | "errno", 724 | "io-lifetimes", 725 | "libc", 726 | "linux-raw-sys", 727 | "windows-sys 0.45.0", 728 | ] 729 | 730 | [[package]] 731 | name = "rustls-pemfile" 732 | version = "0.2.1" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" 735 | dependencies = [ 736 | "base64", 737 | ] 738 | 739 | [[package]] 740 | name = "ryu" 741 | version = "1.0.13" 742 | source = "registry+https://github.com/rust-lang/crates.io-index" 743 | checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" 744 | 745 | [[package]] 746 | name = "safemem" 747 | version = "0.3.3" 748 | source = "registry+https://github.com/rust-lang/crates.io-index" 749 | checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" 750 | 751 | [[package]] 752 | name = "scoped-tls" 753 | version = "1.0.1" 754 | source = "registry+https://github.com/rust-lang/crates.io-index" 755 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 756 | 757 | [[package]] 758 | name = "scopeguard" 759 | version = "1.1.0" 760 | source = "registry+https://github.com/rust-lang/crates.io-index" 761 | checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" 762 | 763 | [[package]] 764 | name = "serde" 765 | version = "1.0.158" 766 | source = "registry+https://github.com/rust-lang/crates.io-index" 767 | checksum = "771d4d9c4163ee138805e12c710dd365e4f44be8be0503cb1bb9eb989425d9c9" 768 | dependencies = [ 769 | "serde_derive", 770 | ] 771 | 772 | [[package]] 773 | name = "serde_derive" 774 | version = "1.0.158" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "e801c1712f48475582b7696ac71e0ca34ebb30e09338425384269d9717c62cad" 777 | dependencies = [ 778 | "proc-macro2", 779 | "quote", 780 | "syn 2.0.10", 781 | ] 782 | 783 | [[package]] 784 | name = "serde_json" 785 | version = "1.0.94" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" 788 | dependencies = [ 789 | "itoa", 790 | "ryu", 791 | "serde", 792 | ] 793 | 794 | [[package]] 795 | name = "serde_urlencoded" 796 | version = "0.7.1" 797 | source = "registry+https://github.com/rust-lang/crates.io-index" 798 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 799 | dependencies = [ 800 | "form_urlencoded", 801 | "itoa", 802 | "ryu", 803 | "serde", 804 | ] 805 | 806 | [[package]] 807 | name = "sha-1" 808 | version = "0.10.1" 809 | source = "registry+https://github.com/rust-lang/crates.io-index" 810 | checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" 811 | dependencies = [ 812 | "cfg-if", 813 | "cpufeatures", 814 | "digest", 815 | ] 816 | 817 | [[package]] 818 | name = "sha1" 819 | version = "0.10.5" 820 | source = "registry+https://github.com/rust-lang/crates.io-index" 821 | checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" 822 | dependencies = [ 823 | "cfg-if", 824 | "cpufeatures", 825 | "digest", 826 | ] 827 | 828 | [[package]] 829 | name = "signal-hook-registry" 830 | version = "1.4.1" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" 833 | dependencies = [ 834 | "libc", 835 | ] 836 | 837 | [[package]] 838 | name = "slab" 839 | version = "0.4.8" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" 842 | dependencies = [ 843 | "autocfg", 844 | ] 845 | 846 | [[package]] 847 | name = "smallvec" 848 | version = "1.10.0" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" 851 | 852 | [[package]] 853 | name = "socket2" 854 | version = "0.4.9" 855 | source = "registry+https://github.com/rust-lang/crates.io-index" 856 | checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" 857 | dependencies = [ 858 | "libc", 859 | "winapi", 860 | ] 861 | 862 | [[package]] 863 | name = "strsim" 864 | version = "0.10.0" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 867 | 868 | [[package]] 869 | name = "syn" 870 | version = "1.0.109" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 873 | dependencies = [ 874 | "proc-macro2", 875 | "quote", 876 | "unicode-ident", 877 | ] 878 | 879 | [[package]] 880 | name = "syn" 881 | version = "2.0.10" 882 | source = "registry+https://github.com/rust-lang/crates.io-index" 883 | checksum = "5aad1363ed6d37b84299588d62d3a7d95b5a5c2d9aad5c85609fda12afaa1f40" 884 | dependencies = [ 885 | "proc-macro2", 886 | "quote", 887 | "unicode-ident", 888 | ] 889 | 890 | [[package]] 891 | name = "tempfile" 892 | version = "3.4.0" 893 | source = "registry+https://github.com/rust-lang/crates.io-index" 894 | checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" 895 | dependencies = [ 896 | "cfg-if", 897 | "fastrand", 898 | "redox_syscall", 899 | "rustix", 900 | "windows-sys 0.42.0", 901 | ] 902 | 903 | [[package]] 904 | name = "termcolor" 905 | version = "1.2.0" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" 908 | dependencies = [ 909 | "winapi-util", 910 | ] 911 | 912 | [[package]] 913 | name = "thiserror" 914 | version = "1.0.40" 915 | source = "registry+https://github.com/rust-lang/crates.io-index" 916 | checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" 917 | dependencies = [ 918 | "thiserror-impl", 919 | ] 920 | 921 | [[package]] 922 | name = "thiserror-impl" 923 | version = "1.0.40" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" 926 | dependencies = [ 927 | "proc-macro2", 928 | "quote", 929 | "syn 2.0.10", 930 | ] 931 | 932 | [[package]] 933 | name = "tinytemplate" 934 | version = "1.2.1" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" 937 | dependencies = [ 938 | "serde", 939 | "serde_json", 940 | ] 941 | 942 | [[package]] 943 | name = "tinyvec" 944 | version = "1.6.0" 945 | source = "registry+https://github.com/rust-lang/crates.io-index" 946 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 947 | dependencies = [ 948 | "tinyvec_macros", 949 | ] 950 | 951 | [[package]] 952 | name = "tinyvec_macros" 953 | version = "0.1.1" 954 | source = "registry+https://github.com/rust-lang/crates.io-index" 955 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 956 | 957 | [[package]] 958 | name = "tokio" 959 | version = "1.26.0" 960 | source = "registry+https://github.com/rust-lang/crates.io-index" 961 | checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" 962 | dependencies = [ 963 | "autocfg", 964 | "bytes", 965 | "libc", 966 | "memchr", 967 | "mio", 968 | "num_cpus", 969 | "parking_lot", 970 | "pin-project-lite", 971 | "signal-hook-registry", 972 | "socket2", 973 | "tokio-macros", 974 | "windows-sys 0.45.0", 975 | ] 976 | 977 | [[package]] 978 | name = "tokio-macros" 979 | version = "1.8.2" 980 | source = "registry+https://github.com/rust-lang/crates.io-index" 981 | checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" 982 | dependencies = [ 983 | "proc-macro2", 984 | "quote", 985 | "syn 1.0.109", 986 | ] 987 | 988 | [[package]] 989 | name = "tokio-stream" 990 | version = "0.1.12" 991 | source = "registry+https://github.com/rust-lang/crates.io-index" 992 | checksum = "8fb52b74f05dbf495a8fba459fdc331812b96aa086d9eb78101fa0d4569c3313" 993 | dependencies = [ 994 | "futures-core", 995 | "pin-project-lite", 996 | "tokio", 997 | ] 998 | 999 | [[package]] 1000 | name = "tokio-tungstenite" 1001 | version = "0.17.2" 1002 | source = "registry+https://github.com/rust-lang/crates.io-index" 1003 | checksum = "f714dd15bead90401d77e04243611caec13726c2408afd5b31901dfcdcb3b181" 1004 | dependencies = [ 1005 | "futures-util", 1006 | "log", 1007 | "tokio", 1008 | "tungstenite", 1009 | ] 1010 | 1011 | [[package]] 1012 | name = "tokio-util" 1013 | version = "0.7.7" 1014 | source = "registry+https://github.com/rust-lang/crates.io-index" 1015 | checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2" 1016 | dependencies = [ 1017 | "bytes", 1018 | "futures-core", 1019 | "futures-sink", 1020 | "pin-project-lite", 1021 | "tokio", 1022 | "tracing", 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "tower-service" 1027 | version = "0.3.2" 1028 | source = "registry+https://github.com/rust-lang/crates.io-index" 1029 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1030 | 1031 | [[package]] 1032 | name = "tracing" 1033 | version = "0.1.37" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" 1036 | dependencies = [ 1037 | "cfg-if", 1038 | "log", 1039 | "pin-project-lite", 1040 | "tracing-core", 1041 | ] 1042 | 1043 | [[package]] 1044 | name = "tracing-core" 1045 | version = "0.1.30" 1046 | source = "registry+https://github.com/rust-lang/crates.io-index" 1047 | checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 1048 | dependencies = [ 1049 | "once_cell", 1050 | ] 1051 | 1052 | [[package]] 1053 | name = "try-lock" 1054 | version = "0.2.4" 1055 | source = "registry+https://github.com/rust-lang/crates.io-index" 1056 | checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" 1057 | 1058 | [[package]] 1059 | name = "tungstenite" 1060 | version = "0.17.3" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "e27992fd6a8c29ee7eef28fc78349aa244134e10ad447ce3b9f0ac0ed0fa4ce0" 1063 | dependencies = [ 1064 | "base64", 1065 | "byteorder", 1066 | "bytes", 1067 | "http", 1068 | "httparse", 1069 | "log", 1070 | "rand", 1071 | "sha-1", 1072 | "thiserror", 1073 | "url", 1074 | "utf-8", 1075 | ] 1076 | 1077 | [[package]] 1078 | name = "twoway" 1079 | version = "0.1.8" 1080 | source = "registry+https://github.com/rust-lang/crates.io-index" 1081 | checksum = "59b11b2b5241ba34be09c3cc85a36e56e48f9888862e19cedf23336d35316ed1" 1082 | dependencies = [ 1083 | "memchr", 1084 | ] 1085 | 1086 | [[package]] 1087 | name = "typenum" 1088 | version = "1.16.0" 1089 | source = "registry+https://github.com/rust-lang/crates.io-index" 1090 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" 1091 | 1092 | [[package]] 1093 | name = "unicase" 1094 | version = "2.6.0" 1095 | source = "registry+https://github.com/rust-lang/crates.io-index" 1096 | checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" 1097 | dependencies = [ 1098 | "version_check", 1099 | ] 1100 | 1101 | [[package]] 1102 | name = "unicode-bidi" 1103 | version = "0.3.13" 1104 | source = "registry+https://github.com/rust-lang/crates.io-index" 1105 | checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" 1106 | 1107 | [[package]] 1108 | name = "unicode-ident" 1109 | version = "1.0.8" 1110 | source = "registry+https://github.com/rust-lang/crates.io-index" 1111 | checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" 1112 | 1113 | [[package]] 1114 | name = "unicode-normalization" 1115 | version = "0.1.22" 1116 | source = "registry+https://github.com/rust-lang/crates.io-index" 1117 | checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" 1118 | dependencies = [ 1119 | "tinyvec", 1120 | ] 1121 | 1122 | [[package]] 1123 | name = "url" 1124 | version = "2.3.1" 1125 | source = "registry+https://github.com/rust-lang/crates.io-index" 1126 | checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" 1127 | dependencies = [ 1128 | "form_urlencoded", 1129 | "idna", 1130 | "percent-encoding", 1131 | ] 1132 | 1133 | [[package]] 1134 | name = "utf-8" 1135 | version = "0.7.6" 1136 | source = "registry+https://github.com/rust-lang/crates.io-index" 1137 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1138 | 1139 | [[package]] 1140 | name = "utf8_slice" 1141 | version = "1.0.0" 1142 | source = "registry+https://github.com/rust-lang/crates.io-index" 1143 | checksum = "44109e280ecf0f5d8e6ee671c0ee650008275c51dc9e494badd11fb39d785408" 1144 | 1145 | [[package]] 1146 | name = "v_htmlescape" 1147 | version = "0.15.8" 1148 | source = "registry+https://github.com/rust-lang/crates.io-index" 1149 | checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" 1150 | 1151 | [[package]] 1152 | name = "version_check" 1153 | version = "0.9.4" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1156 | 1157 | [[package]] 1158 | name = "want" 1159 | version = "0.3.0" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" 1162 | dependencies = [ 1163 | "log", 1164 | "try-lock", 1165 | ] 1166 | 1167 | [[package]] 1168 | name = "warp" 1169 | version = "0.3.3" 1170 | source = "registry+https://github.com/rust-lang/crates.io-index" 1171 | checksum = "ed7b8be92646fc3d18b06147664ebc5f48d222686cb11a8755e561a735aacc6d" 1172 | dependencies = [ 1173 | "bytes", 1174 | "futures-channel", 1175 | "futures-util", 1176 | "headers", 1177 | "http", 1178 | "hyper", 1179 | "log", 1180 | "mime", 1181 | "mime_guess", 1182 | "multipart", 1183 | "percent-encoding", 1184 | "pin-project", 1185 | "rustls-pemfile", 1186 | "scoped-tls", 1187 | "serde", 1188 | "serde_json", 1189 | "serde_urlencoded", 1190 | "tokio", 1191 | "tokio-stream", 1192 | "tokio-tungstenite", 1193 | "tokio-util", 1194 | "tower-service", 1195 | "tracing", 1196 | ] 1197 | 1198 | [[package]] 1199 | name = "wasi" 1200 | version = "0.11.0+wasi-snapshot-preview1" 1201 | source = "registry+https://github.com/rust-lang/crates.io-index" 1202 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1203 | 1204 | [[package]] 1205 | name = "winapi" 1206 | version = "0.3.9" 1207 | source = "registry+https://github.com/rust-lang/crates.io-index" 1208 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 1209 | dependencies = [ 1210 | "winapi-i686-pc-windows-gnu", 1211 | "winapi-x86_64-pc-windows-gnu", 1212 | ] 1213 | 1214 | [[package]] 1215 | name = "winapi-i686-pc-windows-gnu" 1216 | version = "0.4.0" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 1219 | 1220 | [[package]] 1221 | name = "winapi-util" 1222 | version = "0.1.5" 1223 | source = "registry+https://github.com/rust-lang/crates.io-index" 1224 | checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" 1225 | dependencies = [ 1226 | "winapi", 1227 | ] 1228 | 1229 | [[package]] 1230 | name = "winapi-x86_64-pc-windows-gnu" 1231 | version = "0.4.0" 1232 | source = "registry+https://github.com/rust-lang/crates.io-index" 1233 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 1234 | 1235 | [[package]] 1236 | name = "windows-sys" 1237 | version = "0.42.0" 1238 | source = "registry+https://github.com/rust-lang/crates.io-index" 1239 | checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" 1240 | dependencies = [ 1241 | "windows_aarch64_gnullvm", 1242 | "windows_aarch64_msvc", 1243 | "windows_i686_gnu", 1244 | "windows_i686_msvc", 1245 | "windows_x86_64_gnu", 1246 | "windows_x86_64_gnullvm", 1247 | "windows_x86_64_msvc", 1248 | ] 1249 | 1250 | [[package]] 1251 | name = "windows-sys" 1252 | version = "0.45.0" 1253 | source = "registry+https://github.com/rust-lang/crates.io-index" 1254 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" 1255 | dependencies = [ 1256 | "windows-targets", 1257 | ] 1258 | 1259 | [[package]] 1260 | name = "windows-targets" 1261 | version = "0.42.2" 1262 | source = "registry+https://github.com/rust-lang/crates.io-index" 1263 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" 1264 | dependencies = [ 1265 | "windows_aarch64_gnullvm", 1266 | "windows_aarch64_msvc", 1267 | "windows_i686_gnu", 1268 | "windows_i686_msvc", 1269 | "windows_x86_64_gnu", 1270 | "windows_x86_64_gnullvm", 1271 | "windows_x86_64_msvc", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "windows_aarch64_gnullvm" 1276 | version = "0.42.2" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" 1279 | 1280 | [[package]] 1281 | name = "windows_aarch64_msvc" 1282 | version = "0.42.2" 1283 | source = "registry+https://github.com/rust-lang/crates.io-index" 1284 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" 1285 | 1286 | [[package]] 1287 | name = "windows_i686_gnu" 1288 | version = "0.42.2" 1289 | source = "registry+https://github.com/rust-lang/crates.io-index" 1290 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" 1291 | 1292 | [[package]] 1293 | name = "windows_i686_msvc" 1294 | version = "0.42.2" 1295 | source = "registry+https://github.com/rust-lang/crates.io-index" 1296 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" 1297 | 1298 | [[package]] 1299 | name = "windows_x86_64_gnu" 1300 | version = "0.42.2" 1301 | source = "registry+https://github.com/rust-lang/crates.io-index" 1302 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" 1303 | 1304 | [[package]] 1305 | name = "windows_x86_64_gnullvm" 1306 | version = "0.42.2" 1307 | source = "registry+https://github.com/rust-lang/crates.io-index" 1308 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" 1309 | 1310 | [[package]] 1311 | name = "windows_x86_64_msvc" 1312 | version = "0.42.2" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" 1315 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "medup" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | itertools = "0.10.5" 10 | utf8_slice = "1.0.0" 11 | tinytemplate = "1.2.1" 12 | serde = { version = "1.0", features = ["derive"] } 13 | clap = "4.0.32" 14 | regex = "1.7.1" 15 | lazy_static = "1.4.0" 16 | url = "2.3.1" 17 | email_address = "0.2.4" 18 | serde_json = "1.0.91" 19 | v_htmlescape = "0.15.8" 20 | 21 | tokio = { version = "1", features = ["full"] } 22 | warp = "0.3" 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1.67.1-slim-buster as build 2 | 3 | RUN USER=root cargo new --bin medup 4 | WORKDIR /medup 5 | 6 | COPY ./Cargo.lock ./Cargo.lock 7 | COPY ./Cargo.toml ./Cargo.toml 8 | 9 | RUN cargo build --release && rm -r ./src 10 | 11 | COPY ./src ./src 12 | COPY ./docs ./docs 13 | COPY ./themes ./themes 14 | 15 | RUN rm ./target/release/deps/medup* && cargo build --release 16 | 17 | FROM rust:1.67.1-slim-buster 18 | 19 | COPY --from=build /medup/target/release/medup ./medup/ 20 | COPY --from=build /medup/docs ./medup/docs/ 21 | COPY --from=build /medup/themes ./medup/themes/ 22 | 23 | EXPOSE 8181 24 | ENTRYPOINT [ "/medup/medup" ] 25 | CMD ["serve", "--config-path", "/medup/themes/notion/config.json", "--dir", "/medup/docs", "--static-dir", "/medup/themes"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Medup is a markdown parser and reader! 2 | 3 | ![](./docs/assets/logo.png) 4 | 5 | 6 | ## Features 7 | * [x] Support all standard syntax of markdown 8 | * [x] Supports all major extension syntaxes 9 | * [x] Provide **crate** to convert markdown to html or Custom development based on AST directly 10 | * [x] Provide **web service** to host and parse markdown files, and create private document system or blog 11 | * [x] Support **css theme** selection or custom 12 | * [x] Support **slice mode** that you can convert a markdown file into multi slices, .e.g card, picture or slide ... 13 | * [ ] Custom new grammar so that the content can be layout horizontally 14 | * [ ] Support git as storage backend 15 | * [ ] There may be an editor, supporting VIM mode 16 | 17 | ## Demo 18 | 19 | ##### Normal Document 20 | ![](./docs/assets/demo.png) 21 | 22 | ##### XHS (小红书) Picture based on "slice mode" 23 | ![](./docs/assets/xhs_demo.png) 24 | 25 | ## Usage 26 | ``` 27 | A markdown parser and reader 28 | 29 | Usage: medup 30 | 31 | Commands: 32 | serve Provide an http service for markdown parsing 33 | help Print this message or the help of the given subcommand(s) 34 | 35 | Options: 36 | -h, --help Print help information 37 | -V, --version Print version information 38 | ``` 39 | 40 | ### Web 41 | 42 | Use the following command to start an http service on port 8181. 43 | ``` 44 | cargo run -- serve --config-path themes/notion/config.json --dir docs --static-dir themes 45 | ``` 46 | or 47 | 48 | ``` 49 | docker run -d --rm -p 8181:8181 skoowoo/medup:0.1 50 | ``` 51 | 52 | Open `http://localhost:8181` with your browser. 53 | 54 | ### Crate 55 | 56 | ```Rust 57 | // Cargo.toml 58 | // medup = {git = "https://github.com/hardhackerlabs/medup"} 59 | 60 | use medup::{config, markdown}; 61 | 62 | let html_content = markdown::Markdown::new() 63 | .config(config::Config::default()) 64 | .path("docs/markdown-guide.md") 65 | .map_mut(markdown::to_html_body); 66 | 67 | println!(html_content); 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/docs/assets/demo.png -------------------------------------------------------------------------------- /docs/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/docs/assets/logo.png -------------------------------------------------------------------------------- /docs/assets/xhs_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/docs/assets/xhs_demo.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | # Welcome to Medup! 3 | 4 | * [markdown-guide](./markdown-guide.md) -------------------------------------------------------------------------------- /docs/markdown-guide.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Medup Markdown 语法介绍 4 | 5 | **Markdown** 是一种纯文本格式的标记语言,它使用简单的标记语法来格式化文本。由 **Daring Fireball** 创建,原始语法在 [这里](https://daringfireball.net/projects/markdown/syntax);多数 markdown parser 都兼容了标准语法,但也存在一些自定义的扩展语法。**Medup** 希望能够在保证常规标准语法的同时,能够实现掉所有主流的扩展语法。 6 | 7 | ## Medup is a markdown parser and reader! 8 | ## 标题 9 | 10 | 使用 `#` 符号来定义标题。在文本开头添加 `#` 符号,可以将文本转换为标题。数量越多,标题级别越低。 11 | 12 | ```markdown 13 | # 一级标题 14 | ## 二级标题 15 | ### 三级标题 16 | #### 四级标题 17 | ##### 五级标题 18 | ###### 六级标题 19 | ``` 20 | 21 | `#` 和文本之间存在至少一个空格。 22 | `#` 前面除了空白字符以外,不能有其他任何字符。 23 | 标题上除了普通文本以外,还可以是链接、图片等。 24 | 25 | #### 渲染效果 26 | 27 | > ### 三级标题 28 | > #### 四级标题 29 | > ##### 五级标题 30 | 31 | ## 无序列表 32 | 33 | 使用 `-`, `+` 或 `*` 来创建无序列表;同时,还可以使用两个空格或者 tab 编写嵌套的列表项。 34 | 35 | ```markdown 36 | - 项目 1 37 | - 项目 2 38 | - 项目 3 39 | 40 | * 项目 1 41 | * 项目 2 42 | * 嵌套项目 1 43 | * 项目 3 44 | ``` 45 | 46 | `-` `+` 和 `*` 都可以用于创建无序列表,根据自己的习惯选择就行。 47 | 48 | #### 渲染效果 49 | 50 | * 项目 1 51 | * 嵌套项目 1 52 | * 项目 2 53 | * 项目 3 54 | 55 | ## 有序列表 56 | 57 | 使用数字和 `.` 创建有序列表。 58 | 59 | #### 方式一 60 | ```markdown 61 | 1. 项目 1 62 | 2. 项目 2 63 | 3. 项目 3 64 | ``` 65 | 66 | #### 方式二 67 | ```markdown 68 | 1. 项目 1 69 | 1. 项目 2 70 | 1. 项目 3 71 | ``` 72 | 73 | 数字后面紧跟 `.` 号,之间不能有空格存在。 74 | `.` 号和文本项之间至少需要一个空白字符。 75 | 除了可以用 1 2 3 ... 的数字序列方式一之外,还可以只有用一个数字的方式二;根据自己的习惯选择。 76 | 77 | #### 渲染效果 78 | 79 | 1. 项目 1 80 | 2. 项目 2 81 | 3. 项目 3 82 | 83 | ## 强调 84 | 使用 `*` 或 `_` 来定义文本强调,包括加粗和斜体。 单个 `*` 或 `_` 包围的文本被斜体表示,两个 `**` 或 `__` 是加粗表示,三个 `***` 或 `___` 是斜体同时加粗表示。 85 | 86 | ```markdown 87 | *这是斜体* 88 | _这是斜体_ 89 | 90 | **这是粗体** 91 | __这是粗体__ 92 | 93 | ***这是粗体+斜体*** 94 | ___这是粗体+斜体___ 95 | ``` 96 | 97 | 一些 markdown 解析要求 `*`, `_` 和文本之间不能有空白字符存在,比如:`\* 斜体 \*`, `\_ 斜体 \_`, `\*\* 粗体 \*\*` ,但是 Medup 的实现是允许这种写法的。Medup 是在保证严格语法的同时,尽量提供轻松书写,兼容一些误差。 98 | 99 | #### 渲染效果 100 | 101 | *这是斜体* 102 | _这是斜体_ 103 | **这是粗体** 104 | __这是粗体__ 105 | ***这是粗体+斜体*** 106 | ___这是粗体+斜体___ 107 | 108 | ## 删除 109 | 使用 `~~` 删除一段文本。注意是两个波浪符号。 110 | 111 | ```markdown 112 | ~~删除这里的文本~~ 113 | ``` 114 | 115 | #### 渲染效果 116 | 117 | ~~删除这里的文本~~ 118 | 119 | ## 链接 120 | 121 | 使用 `\[]()` 来创建链接。这是比较常用的链接创建方式,除了这种方式以外,还有另外两种方式,下面介绍。 122 | 123 | ```markdown 124 | [Example](https://example.com) 125 | ``` 126 | 127 | `[ ]` 方括号内是显式文本,`( )` 圆括号内是 URL 地址。 128 | `](` 两个符号之间不能有空白字符存在,比如:`[Example] (https://example.com)` 就是无效的链接。 129 | 130 | #### 渲染效果 131 | 132 | 这是一个链接:[Example](https://example.com) 133 | 134 | ## 快速链接 135 | 136 | 使用 `<>` 快速创建 URL 链接。 137 | 138 | ```markdown 139 | 140 | 141 | ``` 142 | 143 | `<>` 中必须是一个有效的 URL 或者 email 地址。 144 | 145 | #### 渲染效果 146 | 147 | 148 | 149 | 150 | ## 引用链接 151 | 152 | 使用 `\[][]` 和 `\[]: ` 的组合语法创建引用式的链接,也有叫它参考链接的。引用链接的优势是可以很方便的在文章的多处内容使用同一个链接地址,链接地址被修改后,所有引用的地方都会保持同步更新。 153 | 154 | ```markdown 155 | [Example1][example_link] 156 | [Example2][example_link] 157 | 158 | [example_link]: https://example.com "title" 159 | ``` 160 | 161 | `\[][]` 处可以称为链接引用,`\[]: ` 处可以称为链接定义,在链接定义中 title 部分是可选的。通常我们可以将链接定义部分放到文章内容的尾部。 162 | 163 | #### 渲染效果 164 | 165 | [Example1][example_link] 166 | [Example2][example_link] 167 | 168 | [example_link]: https://example.com "title" 169 | 170 | ## 图片 171 | 172 | 使用 `\!\[]()` 来插入图片。 173 | 174 | ```markdown 175 | ![替代文本](https://example.com/img.jpg) 176 | ``` 177 | 178 | 和普通链接的语法不同之处只是在开头多了一个感叹号 `\!`,其他部分都一样。 179 | 180 | #### 渲染效果 181 | 182 | ![这是一个图片](https://markdown.land/wp-content/uploads/2021/06/markdown-512px.png) 183 | 184 | ## 引用 185 | 使用 `>` 来引用文本。比如:本文的 *渲染效果* 部分都是引用。 186 | 187 | ```markdown 188 | > 这是一个单行引用文本 189 | ``` 190 | ```markdown 191 | > 这是一个多行引用文本 192 | > 这是一个多行引用文本 193 | > 这是一个多行引用文本 194 | ``` 195 | 196 | `>` 和文本之间需要至少一个空白字符,并且 `>` 前面不能有任何其他非空白字符。引用文本中可以再使用所有的 markdown 语法,比如: 197 | 198 | ```markdown 199 | > 这是一个单行引用文本 200 | > 201 | > * 引用中嵌套项目 1 202 | > * 引用中嵌套项目 2 203 | > * 引用中嵌套项目 3 204 | > 205 | ``` 206 | 207 | #### 渲染效果 208 | 209 | > 这是一个多行引用文本 210 | > 这是一个多行引用文本 211 | > 这是一个多行引用文本 212 | 213 | > * 引用中嵌套项目 1 214 | > * 引用中嵌套项目 2 215 | > * 引用中嵌套项目 3 216 | >> 嵌套一个引用 217 | 218 | 219 | ## 代码 220 | 221 | 使用 `\`` 来标记文本中的代码。 222 | 223 | ```markdown 224 | 这是一段文本,里面包含了一段代码: `print("Hello, world!")`。 225 | ``` 226 | 227 | 代码标记 `\`` 和斜体,粗体的用法类似。 228 | 229 | #### 渲染效果 230 | 231 | 这是一段文本,里面包含了一段代码: `print("Hello, world!")`。 232 | 233 | ## 代码块 234 | 235 | 使用 `\`\`\`` 来标记多行组成的一个代码块。本文 markdown 语法介绍部分都是在代码块中。 236 | 237 | \``` 238 | 这里放你的代码段 239 | \``` 240 | 241 | #### 渲染效果 242 | ```rust 243 | // This is the main function 244 | fn main() { 245 | // Print text to the console 246 | println!("Hello World!"); 247 | } 248 | ``` 249 | 250 | ## 转义 251 | 使用 `\\` 可以转义一些特殊字符,被转义后的字符将不会作为 markdown 语法标记进行解析,而是当做普通字符。可以被转义的字符包含: 252 | * ~ 253 | * : 254 | * * 255 | * _ 256 | * ` 257 | * # 258 | * + 259 | * - 260 | * . 261 | * ! 262 | * [ 263 | * ] 264 | * ( 265 | * ) 266 | * < 267 | * > 268 | * \ 269 | 270 | ``` 271 | 转义掉字符 "`": \`print("Hello, world!")\`。 272 | ``` 273 | 274 | #### 渲染效果 275 | 转义掉字符 "`": \`print("Hello, world!")\`。 276 | 277 | ## TODO 列表 278 | 我们可以在无序列表的基础上,通过添加 `[ ]` 或者 `[x]` 来实现 TODO 列表,`[ ]` 表示未完成项,`[x]` 表示完成项。 279 | 280 | ```markdown 281 | - [ ] 购买食材 282 | - [ ] 鸡蛋 283 | - [x] 做晚餐 284 | - [ ] 洗碗 285 | ``` 286 | 287 | #### 渲染效果 288 | - [ ] 购买食材 289 | - [ ] 鸡蛋 290 | - [x] 做晚餐 291 | - [ ] 洗碗 292 | 293 | ## 分割线 294 | 可以使用 `---`, `___`, `***` 实现分割线,需要注意的点是 `---` 等语法行的前后需要至少有一个空白行,否则会被解析成普通的文本。另外,小横线、下划线或者星号的个数并不是必须 3 个,只要大于等于 3 就行。 295 | 296 | #### 渲染效果 297 | 298 | --- 299 | 300 | 上面是一条很帅的渐变分割线 -------------------------------------------------------------------------------- /src/bin/medup/config.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fs::File, io::Read, path::Path}; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | const TEMPLATE_FILE_NAME: &str = "template.txt"; 6 | 7 | #[derive(Default, Debug, Serialize, Deserialize, Clone)] 8 | pub(crate) struct ConfigJson { 9 | #[serde(default)] 10 | pub css_href: String, 11 | pub body_min_width: i32, 12 | pub body_max_width: i32, 13 | pub use_slice_mode: bool, 14 | pub slice_header: String, 15 | } 16 | 17 | #[derive(Default, Debug, Clone)] 18 | pub(crate) struct Config { 19 | config_json: ConfigJson, 20 | template: String, 21 | } 22 | 23 | impl Config { 24 | pub(crate) fn css_href(&self) -> &str { 25 | &self.config_json.css_href 26 | } 27 | 28 | pub(crate) fn body_min_width(&self) -> i32 { 29 | self.config_json.body_min_width 30 | } 31 | 32 | pub(crate) fn body_max_width(&self) -> i32 { 33 | self.config_json.body_max_width 34 | } 35 | 36 | pub(crate) fn use_slice_mode(&self) -> bool { 37 | self.config_json.use_slice_mode 38 | } 39 | 40 | pub(crate) fn slice_header(&self) -> &str { 41 | &self.config_json.slice_header 42 | } 43 | 44 | pub(crate) fn template(&self) -> &str { 45 | &self.template 46 | } 47 | 48 | pub(crate) fn read(path: &str) -> Result> { 49 | let mut buf = String::new(); 50 | File::open(path)?.read_to_string(&mut buf)?; 51 | 52 | let mut cj: ConfigJson = serde_json::from_str(&buf)?; 53 | if cj.body_min_width == 0 { 54 | cj.body_min_width = 200; 55 | } 56 | if cj.body_max_width == 0 { 57 | cj.body_max_width = 900; 58 | } 59 | 60 | // read template 61 | let mut buf = String::new(); 62 | Path::new(path).parent().map(|p| { 63 | let mut template_path = p.to_path_buf(); 64 | template_path.push(TEMPLATE_FILE_NAME); 65 | File::open(template_path) 66 | .map(|mut f| f.read_to_string(&mut buf)) 67 | .ok(); 68 | }); 69 | 70 | Ok(Config { 71 | config_json: cj, 72 | template: buf, 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/bin/medup/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod render_html; 3 | mod serve; 4 | 5 | use crate::serve::proc_serve; 6 | 7 | use clap::{arg, Command}; 8 | 9 | fn main() { 10 | let matches = cli().get_matches(); 11 | match matches.subcommand() { 12 | Some(("serve", sub_matches)) => proc_serve(sub_matches), 13 | _ => unreachable!(), 14 | } 15 | } 16 | 17 | fn cli() -> Command { 18 | Command::new("medup") 19 | .version("0.1") 20 | .about("A markdown parser and reader") 21 | .subcommand_required(true) 22 | .subcommand( 23 | Command::new("serve") 24 | .about("Provide an http service for markdown parsing") 25 | .arg(arg!(-l --"listen-addr" [LISTEN_ADDR] r#"Specify the listening address of the http server, default ":8181"."#)) 26 | .arg(arg!(-c --"config-path" [CONFIG_PATH] "Specify path of the config file, it's optional.")) 27 | .arg(arg!(-d --dir [DIR] "Specify the directory where markdown files are stored.")) 28 | .arg(arg!(-s --"static-dir" [STATIC_DIR] "Specify the directory where static resources are stored.")) 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/bin/medup/render_html.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use serde::Serialize; 4 | use tinytemplate::TinyTemplate; 5 | 6 | use crate::config::Config; 7 | 8 | const TPL_HTML_NAME: &str = "template"; 9 | 10 | #[derive(Serialize)] 11 | struct HtmlContext<'html_context> { 12 | title: &'html_context str, 13 | css_href: &'html_context str, 14 | body_min_width: i32, 15 | body_max_width: i32, 16 | use_slice_mode: bool, 17 | slice_header: &'html_context str, 18 | content: &'html_context str, 19 | slices: &'html_context Vec, 20 | } 21 | 22 | pub(crate) struct RenderHtml<'render_html> { 23 | tt: TinyTemplate<'render_html>, 24 | } 25 | 26 | impl<'render_html> RenderHtml<'render_html> { 27 | pub(crate) fn new(template: &'render_html str) -> Result> { 28 | let mut tt = TinyTemplate::new(); 29 | tt.add_template(TPL_HTML_NAME, template)?; 30 | tt.set_default_formatter(&tinytemplate::format_unescaped); 31 | Ok(RenderHtml { tt }) 32 | } 33 | 34 | pub(crate) fn exec(&self, cfg: &Config, data: &Vec) -> Result> { 35 | let content = if !cfg.use_slice_mode() { 36 | data.join("") 37 | } else { 38 | "".to_string() 39 | }; 40 | 41 | let ctx = HtmlContext { 42 | title: "medup", 43 | css_href: cfg.css_href(), 44 | body_min_width: cfg.body_min_width(), 45 | body_max_width: cfg.body_max_width(), 46 | use_slice_mode: cfg.use_slice_mode(), 47 | slice_header: &cfg.slice_header(), 48 | content: &content, 49 | slices: data, 50 | }; 51 | 52 | let s = self.tt.render(TPL_HTML_NAME, &ctx)?; 53 | Ok(s) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/bin/medup/serve.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::net::Ipv4Addr; 3 | use std::path::Path; 4 | 5 | use crate::config::Config; 6 | use crate::render_html::RenderHtml; 7 | use medup::markdown::{self, Markdown}; 8 | 9 | use clap::ArgMatches; 10 | use warp::filters::BoxedFilter; 11 | use warp::hyper::StatusCode; 12 | use warp::{Filter, Reply}; 13 | 14 | #[tokio::main] 15 | pub async fn proc_serve(matches: &ArgMatches) { 16 | let dir = get_dir(matches, "dir"); 17 | println!( 18 | "---> the directory where markdown files are stored: \"{}\"", 19 | dir 20 | ); 21 | 22 | let sdir = get_dir(matches, "static-dir"); 23 | println!( 24 | "---> the directory where static resources are stored: \"{}\"", 25 | sdir 26 | ); 27 | 28 | // All filters are used to match requests 29 | let cfg: Config = load_config(matches).expect("failed to load config"); 30 | let filters = static_filter(sdir.to_string()) 31 | .or(articles_filter(cfg.clone(), dir.to_string())) 32 | .or(index_filter(cfg.clone(), dir.to_string())); 33 | 34 | let (addr, port) = parse_ip_port(matches).unwrap(); 35 | println!("---> start to listen on address: \"{}:{}\"", addr, port); 36 | 37 | warp::serve(filters).run((addr, port)).await 38 | } 39 | 40 | // Get /static/* 41 | fn static_filter(dir: String) -> BoxedFilter<(impl Reply,)> { 42 | warp::get() 43 | .and(warp::path("static")) 44 | .and(warp::fs::dir(dir)) 45 | .with(warp::cors().allow_any_origin()) 46 | .boxed() 47 | } 48 | 49 | // Get /:name (.e.g /demo.md) 50 | fn articles_filter(cfg: Config, dir: String) -> BoxedFilter<(impl Reply,)> { 51 | warp::get() 52 | .and(warp::path::param::()) 53 | .and(warp::any().map(move || cfg.clone())) 54 | .and(warp::any().map(move || dir.to_string())) 55 | .map(|mut name: String, cfg: Config, dir: String| { 56 | if !name.ends_with(".md") { 57 | name.push_str(".md"); 58 | } 59 | match RenderHtml::new(cfg.template()) { 60 | Err(e) => error_repsonse( 61 | StatusCode::INTERNAL_SERVER_ERROR, 62 | format!("failed to add html template: {}", e), 63 | ), 64 | Ok(render) => match Path::new(&dir).join(&name).to_str() { 65 | None => error_repsonse( 66 | StatusCode::BAD_REQUEST, 67 | format!(r#"failed to join the path: {}, {}"#, dir, name), 68 | ), 69 | Some(path) => { 70 | let func = if cfg.use_slice_mode() { 71 | markdown::to_slice 72 | } else { 73 | markdown::to_body 74 | }; 75 | 76 | match Markdown::new().path(path).map_mut(func) { 77 | Err(e) => error_repsonse( 78 | StatusCode::INTERNAL_SERVER_ERROR, 79 | format!("failed to generate body from markdown: {}", e), 80 | ), 81 | Ok(v) => match render.exec(&cfg, &v) { 82 | Err(e) => error_repsonse( 83 | StatusCode::INTERNAL_SERVER_ERROR, 84 | format!("failed to render html: {}", e), 85 | ), 86 | Ok(s) => warp::reply::html(s).into_response(), 87 | }, 88 | } 89 | } 90 | }, 91 | } 92 | }) 93 | .with(warp::cors().allow_any_origin()) 94 | .boxed() 95 | } 96 | 97 | // Get /, then read the index.md file 98 | fn index_filter(cfg: Config, dir: String) -> BoxedFilter<(impl Reply,)> { 99 | warp::get() 100 | .and(warp::path::end()) 101 | .and(warp::any().map(move || cfg.clone())) 102 | .and(warp::any().map(move || dir.to_string())) 103 | .map( 104 | |cfg: Config, dir: String| match RenderHtml::new(cfg.template()) { 105 | Err(e) => error_repsonse( 106 | StatusCode::INTERNAL_SERVER_ERROR, 107 | format!("failed to add html template: {}", e), 108 | ), 109 | Ok(render) => match Path::new(&dir).join("index.md").to_str() { 110 | None => error_repsonse( 111 | StatusCode::BAD_REQUEST, 112 | format!(r#"failed to join the path: {}, index.md"#, dir), 113 | ), 114 | Some(path) => match Markdown::new().path(path).map_mut(markdown::to_body) { 115 | Err(e) => error_repsonse( 116 | StatusCode::INTERNAL_SERVER_ERROR, 117 | format!("failed to generate body from markdown: {}", e), 118 | ), 119 | Ok(v) => match render.exec(&cfg, &v) { 120 | Err(e) => error_repsonse( 121 | StatusCode::INTERNAL_SERVER_ERROR, 122 | format!("failed to render html: {}", e), 123 | ), 124 | Ok(s) => warp::reply::html(s).into_response(), 125 | }, 126 | }, 127 | }, 128 | }, 129 | ) 130 | .with(warp::cors().allow_any_origin()) 131 | .boxed() 132 | } 133 | 134 | fn get_dir<'get_dir>(matches: &'get_dir ArgMatches, name: &str) -> &'get_dir str { 135 | match matches.get_one::(name) { 136 | None => ".", 137 | Some(path) => path, 138 | } 139 | } 140 | 141 | fn load_config(matches: &ArgMatches) -> Result> { 142 | // read config path from cli 143 | let cfg = match matches.get_one::("config-path") { 144 | None => Config::default(), 145 | Some(path) => Config::read(path) 146 | .map_err(|e| (format!("failed to read config \"{}\": {}", path, e)))?, 147 | }; 148 | Ok(cfg) 149 | } 150 | 151 | fn parse_ip_port(matches: &ArgMatches) -> Result<(Ipv4Addr, u16), Box> { 152 | // read listen addr from the args of command line 153 | let s = match matches.get_one::("listen-addr") { 154 | None => ":8181", 155 | Some(s) => s, 156 | }; 157 | 158 | let fields: Vec<&str> = s.split(':').collect(); 159 | if fields.len() != 2 { 160 | return Err(format!("invalid address format: {}", s).into()); 161 | } 162 | let ipaddr: Ipv4Addr = if fields[0].is_empty() { 163 | "0.0.0.0" 164 | .parse() 165 | .map_err(|e| format!("failed to parse ip addr 0.0.0.0: {}", e))? 166 | } else { 167 | fields[0] 168 | .parse() 169 | .map_err(|e| format!(r#"failed to parse ip addr "{}": {}"#, fields[0], e))? 170 | }; 171 | let port: u16 = fields[1] 172 | .parse() 173 | .map_err(|e| format!("failed to parse port: {}: {}", fields[1], e))?; 174 | 175 | Ok((ipaddr, port)) 176 | } 177 | 178 | fn error_repsonse(status_code: StatusCode, err_msg: String) -> warp::reply::Response { 179 | warp::http::Response::builder() 180 | .header("X-Powered-By", "Medup") 181 | .status(status_code) 182 | .body(err_msg) 183 | .into_response() 184 | } 185 | -------------------------------------------------------------------------------- /src/html.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::error::Error; 3 | 4 | use crate::lexer::{Token, TokenKind}; 5 | use crate::utils::stack; 6 | use crate::Generate; 7 | use crate::SharedLine; 8 | 9 | use serde::Serialize; 10 | use tinytemplate::TinyTemplate; 11 | use v_htmlescape as htmlescape; 12 | 13 | pub(crate) struct Generator<'generator> { 14 | template: TinyTemplate<'generator>, 15 | ref_link_tags: &'generator HashMap, 16 | } 17 | 18 | impl<'generator> Generator<'generator> { 19 | pub(crate) fn new( 20 | ref_link_tags: &'generator HashMap, 21 | ) -> Result> { 22 | let mut g = Generator { 23 | template: TinyTemplate::new(), 24 | ref_link_tags, 25 | }; 26 | g.init() 27 | .map_err(|e| format!("failed to init the html generator: {}", e))?; 28 | Ok(g) 29 | } 30 | 31 | fn init(&mut self) -> Result<(), Box> { 32 | let templates = vec![ 33 | (TP_ORDERED_LIST_NAME, TP_ORDERED_LIST), 34 | (TP_UNORDERED_LIST_NAME, TP_UNORDERED_LIST), 35 | (TP_TITLE_NAME, TP_TITLE), 36 | (TP_QUOTE_NAME, TP_QUOTE), 37 | (TP_IMG_NAME, TP_IMG), 38 | (TP_LINK_NAME, TP_LINK), 39 | (TP_CODE_NAME, TP_CODE), 40 | (TP_PLAIN_TEXT_NAME, TP_PLAIN_TEXT), 41 | ]; 42 | for (name, tp) in templates { 43 | self.template.add_template(name, tp)?; 44 | } 45 | self.template 46 | .set_default_formatter(&tinytemplate::format_unescaped); 47 | Ok(()) 48 | } 49 | 50 | fn render_inline(&self, tokens: &Vec, escape_text: bool) -> String { 51 | let mut stack: stack::Stack<(TokenKind, &str)> = stack::Stack::new(); 52 | let mut buff = String::new(); 53 | 54 | for t in tokens { 55 | match t.kind() { 56 | TokenKind::Text | TokenKind::LineBreak => { 57 | if escape_text { 58 | buff.push_str(t.html_escaped_value().as_str()) 59 | } else { 60 | buff.push_str(t.value()); 61 | } 62 | } 63 | TokenKind::CodeMark => { 64 | let matched = stack.pop_or_push((t.kind(), t.value()), |e| { 65 | e.0 == t.kind() && e.1 == t.value() 66 | }); 67 | if matched.is_some() { 68 | buff.push_str(""); 69 | } else { 70 | buff.push_str(""); 71 | } 72 | } 73 | TokenKind::BoldMark => { 74 | let matched = stack.pop_or_push((t.kind(), t.value()), |e| { 75 | e.0 == t.kind() && e.1 == t.value() 76 | }); 77 | if matched.is_some() { 78 | buff.push_str(""); 79 | } else { 80 | buff.push_str(""); 81 | } 82 | } 83 | TokenKind::ItalicMark => { 84 | let matched = stack.pop_or_push((t.kind(), t.value()), |e| { 85 | e.0 == t.kind() && e.1 == t.value() 86 | }); 87 | if matched.is_some() { 88 | buff.push_str(""); 89 | } else { 90 | buff.push_str(""); 91 | } 92 | } 93 | TokenKind::ItalicBoldMark => { 94 | let matched = stack.pop_or_push((t.kind(), t.value()), |e| { 95 | e.0 == t.kind() && e.1 == t.value() 96 | }); 97 | if matched.is_some() { 98 | buff.push_str(""); 99 | } else { 100 | buff.push_str(""); 101 | } 102 | } 103 | TokenKind::DeleteMark => { 104 | let matched = stack.pop_or_push((t.kind(), t.value()), |e| { 105 | e.0 == t.kind() && e.1 == t.value() 106 | }); 107 | if matched.is_some() { 108 | buff.push_str(""); 109 | } else { 110 | buff.push_str(""); 111 | } 112 | } 113 | TokenKind::Link | TokenKind::QuickLink | TokenKind::Image => { 114 | let link = t.as_generic_link(); 115 | 116 | let s = self.render_inline(&link.name_to_tokens(), false); 117 | let name = if s.is_empty() { 118 | link.name() 119 | } else { 120 | s.as_str() 121 | }; 122 | let location = link.location(); 123 | 124 | if !name.is_empty() && !location.is_empty() { 125 | let s = if t.kind() == TokenKind::Image { 126 | self.render_image(name, location) 127 | } else { 128 | self.render_link(name, location) 129 | }; 130 | buff.push_str(&s); 131 | } 132 | } 133 | TokenKind::RefLink => { 134 | let link = t.as_generic_link(); 135 | let (name, tag) = (link.name(), link.tag()); 136 | 137 | let default = (String::from(""), String::from("")); 138 | let (location, _title) = self.ref_link_tags.get(tag).unwrap_or(&default); 139 | if !name.is_empty() && !location.is_empty() { 140 | let s = self.render_link(name, location); 141 | buff.push_str(&s); 142 | } 143 | } 144 | TokenKind::UnorderedMark => match t.second_kind() { 145 | Some(TokenKind::TodoDoneMark) => { 146 | buff.push_str(r#" "#); 147 | } 148 | Some(TokenKind::TodoUndoneMark) => { 149 | buff.push_str(r#" "#); 150 | } 151 | _ => (), 152 | }, 153 | _ => (), 154 | } 155 | } 156 | 157 | buff 158 | } 159 | 160 | fn render_link(&self, show_name: &str, location: &str) -> String { 161 | self.template 162 | .render( 163 | TP_LINK_NAME, 164 | &LinkContext { 165 | show_name, 166 | location, 167 | }, 168 | ) 169 | .unwrap() 170 | } 171 | 172 | fn render_image(&self, alt: &str, location: &str) -> String { 173 | self.template 174 | .render(TP_IMG_NAME, &ImageContext { alt, location }) 175 | .unwrap() 176 | } 177 | } 178 | 179 | impl<'generator> Generate for Generator<'generator> { 180 | fn render_title(&self, l: &SharedLine) -> String { 181 | let l = l.borrow(); 182 | let level = l.mark_token().len(); 183 | let value = self.render_inline(l.all(), true); 184 | 185 | let ctx = TitleContext { 186 | is_l1: level == 1, 187 | is_l2: level == 2, 188 | is_l3: level == 3, 189 | is_l4: level == 4, 190 | is_l5: level == 5, 191 | is_l6: level == 6, 192 | id: l.anchor().0, 193 | text: value, 194 | }; 195 | 196 | self.template.render(TP_TITLE_NAME, &ctx).unwrap() 197 | } 198 | 199 | fn render_dividing(&self, _l: &SharedLine) -> String { 200 | String::from("
") 201 | } 202 | 203 | fn render_plain_text(&self, ls: &[SharedLine]) -> String { 204 | let mut lines: Vec = ls 205 | .iter() 206 | .map(|l| { 207 | let mut s = self.render_inline(l.borrow().all(), false); 208 | if !s.ends_with("
") { 209 | s.push_str("
"); 210 | } 211 | s 212 | }) 213 | .collect(); 214 | 215 | // remove the trailing '
' of the last element 216 | // TODO: optimize 217 | if let Some(last) = lines.pop() { 218 | lines.push(last.trim_end_matches("
").to_string()); 219 | } 220 | 221 | self.template 222 | .render(TP_PLAIN_TEXT_NAME, &PlainTextContext { lines }) 223 | .unwrap() 224 | } 225 | 226 | fn render_blank(&self, _ls: &[SharedLine]) -> String { 227 | String::from("") 228 | } 229 | 230 | fn render_ordered_list(&self, ls: &[SharedLine]) -> String { 231 | let list: Vec = ls 232 | .iter() 233 | .map(|l| { 234 | let leader = self.render_inline(l.borrow().all(), true); 235 | let nesting = l 236 | .borrow() 237 | .enter_nested_blocks(&Generator::new(self.ref_link_tags).unwrap()); 238 | if !nesting.is_empty() { 239 | leader + "\n" + nesting.as_str() 240 | } else { 241 | leader 242 | } 243 | }) 244 | .collect(); 245 | self.template 246 | .render(TP_ORDERED_LIST_NAME, &OrderedListContext { list }) 247 | .unwrap() 248 | } 249 | 250 | fn render_unordered_list(&self, ls: &[SharedLine]) -> String { 251 | let list = ls 252 | .iter() 253 | .map(|l| { 254 | let leader = self.render_inline(l.borrow().all(), true); 255 | let nesting = l 256 | .borrow() 257 | .enter_nested_blocks(&Generator::new(self.ref_link_tags).unwrap()); 258 | if !nesting.is_empty() { 259 | leader + "\n" + nesting.as_str() 260 | } else { 261 | leader 262 | } 263 | }) 264 | .collect(); 265 | self.template 266 | .render(TP_UNORDERED_LIST_NAME, &UnorderedListContext { list }) 267 | .unwrap() 268 | } 269 | 270 | fn render_quote(&self, s: &str) -> String { 271 | self.template 272 | .render(TP_QUOTE_NAME, &QuoteContext { text: s }) 273 | .unwrap() 274 | } 275 | 276 | fn render_code(&self, ls: &[SharedLine]) -> String { 277 | debug_assert!(ls.len() >= 2); 278 | 279 | let first = &ls[0]; 280 | let text: String = ls[1..ls.len() - 1] // skip the first and last elements 281 | .iter() 282 | .map(|l| htmlescape::escape(l.borrow().text()).to_string()) 283 | .collect(); 284 | 285 | self.template 286 | .render( 287 | TP_CODE_NAME, 288 | &CodeBlockContext { 289 | name: first.borrow().get(1).map(|t| t.value()).unwrap_or(""), 290 | text: &text, 291 | }, 292 | ) 293 | .unwrap() 294 | } 295 | } 296 | 297 | // title 298 | const TP_TITLE_NAME: &str = "title"; 299 | const TP_TITLE: &str = "\ 300 | {{ if is_l1 }}

{text}

{{ endif }}\ 301 | {{ if is_l2 }}

{text}

{{ endif }}\ 302 | {{ if is_l3 }}

{text}

{{ endif }}\ 303 | {{ if is_l4 }}

{text}

{{ endif }}\ 304 | {{ if is_l5 }}
{text}
{{ endif }}\ 305 | {{ if is_l6 }}
{text}
{{ endif }}"; 306 | 307 | #[derive(Serialize)] 308 | struct TitleContext { 309 | is_l1: bool, 310 | is_l2: bool, 311 | is_l3: bool, 312 | is_l4: bool, 313 | is_l5: bool, 314 | is_l6: bool, 315 | id: String, 316 | text: String, 317 | } 318 | 319 | // ordered list 320 | const TP_ORDERED_LIST_NAME: &str = "ordered_list"; 321 | const TP_ORDERED_LIST: &str = "\ 322 |
    \ 323 | {{ for item in list }} 324 |
  1. {item}
  2. \ 325 | {{ endfor }} 326 |
"; 327 | 328 | #[derive(Serialize)] 329 | struct OrderedListContext { 330 | list: Vec, 331 | } 332 | 333 | // unordered list 334 | const TP_UNORDERED_LIST_NAME: &str = "unordered_list"; 335 | const TP_UNORDERED_LIST: &str = "\ 336 |
    \ 337 | {{ for item in list }} 338 |
  • {item}
  • \ 339 | {{ endfor }} 340 |
"; 341 | 342 | #[derive(Serialize)] 343 | struct UnorderedListContext { 344 | list: Vec, 345 | } 346 | 347 | // link 348 | const TP_LINK_NAME: &str = "link"; 349 | const TP_LINK: &str = r#"{show_name}"#; 350 | 351 | #[derive(Serialize)] 352 | struct LinkContext<'link_context> { 353 | show_name: &'link_context str, 354 | location: &'link_context str, 355 | } 356 | 357 | // image 358 | const TP_IMG_NAME: &str = "img"; 359 | const TP_IMG: &str = r#"{alt}"#; 360 | 361 | #[derive(Serialize)] 362 | struct ImageContext<'image_context> { 363 | alt: &'image_context str, 364 | location: &'image_context str, 365 | } 366 | 367 | // code block 368 | const TP_CODE_NAME: &str = "code_block"; 369 | const TP_CODE: &str = "
{text}
"; 370 | 371 | #[derive(Serialize)] 372 | struct CodeBlockContext<'code_block_context> { 373 | name: &'code_block_context str, 374 | text: &'code_block_context str, 375 | } 376 | 377 | // plain text 378 | const TP_PLAIN_TEXT_NAME: &str = "plain_text"; 379 | const TP_PLAIN_TEXT: &str = "\ 380 |

\ 381 | {{ for text in lines}}\ 382 | {text}\ 383 | {{ endfor }}\ 384 |

"; 385 | 386 | #[derive(Serialize)] 387 | struct PlainTextContext { 388 | lines: Vec, 389 | } 390 | 391 | // quote block 392 | const TP_QUOTE_NAME: &str = "quote"; 393 | const TP_QUOTE: &str = "\ 394 |

395 | {text} 396 |

"; 397 | 398 | #[derive(Serialize)] 399 | struct QuoteContext<'quote_context> { 400 | text: &'quote_context str, 401 | } 402 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::rc::Rc; 3 | 4 | mod html; 5 | mod lexer; 6 | pub mod markdown; 7 | mod parser; 8 | pub mod utils; 9 | 10 | pub type SharedLine = Rc>; 11 | 12 | pub trait Generate { 13 | fn render_title(&self, l: &SharedLine) -> String { 14 | l.borrow().text().trim().to_string() 15 | } 16 | 17 | fn render_dividing(&self, _l: &SharedLine) -> String { 18 | "".to_string() 19 | } 20 | 21 | fn render_plain_text(&self, _ls: &[SharedLine]) -> String { 22 | "".to_string() 23 | } 24 | 25 | fn render_blank(&self, _ls: &[SharedLine]) -> String { 26 | "".to_string() 27 | } 28 | 29 | fn render_ordered_list(&self, _ls: &[SharedLine]) -> String { 30 | "".to_string() 31 | } 32 | 33 | fn render_unordered_list(&self, _ls: &[SharedLine]) -> String { 34 | "".to_string() 35 | } 36 | 37 | fn render_quote(&self, _s: &str) -> String { 38 | "".to_string() 39 | } 40 | 41 | fn render_code(&self, _ls: &[SharedLine]) -> String { 42 | "".to_string() 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/markdown.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use crate::html; 4 | use crate::parser::Ast; 5 | 6 | #[derive(Debug)] 7 | pub struct Markdown<'markdown> { 8 | ast: Ast, 9 | path: Option<&'markdown str>, 10 | text: Option<&'markdown str>, 11 | } 12 | 13 | impl<'markdown> Default for Markdown<'markdown> { 14 | fn default() -> Self { 15 | Self::new() 16 | } 17 | } 18 | 19 | impl<'markdown> Markdown<'markdown> { 20 | pub fn new() -> Self { 21 | Markdown { 22 | ast: Ast::new(), 23 | path: None, 24 | text: None, 25 | } 26 | } 27 | 28 | // Specify the path of a markdown file, then read the file and parse it 29 | pub fn path(&mut self, path: &'markdown str) -> &mut Self { 30 | self.path = Some(path); 31 | self 32 | } 33 | 34 | // Provide the content of a markdown file, then parse it directly 35 | pub fn text(&mut self, text: &'markdown str) -> &mut Self { 36 | self.text = Some(text); 37 | self 38 | } 39 | 40 | // Use 'f' function to convert markdown ast into a string, .e.g html document 41 | pub fn map_mut(&mut self, f: F) -> Result, Box> 42 | where 43 | F: Fn(&Ast) -> Result, Box>, 44 | { 45 | self.parse()?; 46 | let s = f(&self.ast)?; 47 | Ok(s) 48 | } 49 | 50 | fn parse(&mut self) -> Result<&Self, Box> { 51 | match self.text { 52 | Some(s) => self.ast.parse_string(s)?, 53 | None => match self.path { 54 | Some(p) => self.ast.parse_file(p)?, 55 | None => return Err("not found path or text to parse".into()), 56 | }, 57 | } 58 | Ok(self) 59 | } 60 | } 61 | 62 | // Convert markdown ast into body part of the html and it contains toc 63 | pub fn to_body_toc(ast: &Ast) -> Result, Box> { 64 | let body = ast.generate_content(&html::Generator::new(ast.ref_link_tags())?); 65 | let toc = ast.generate_toc(&html::Generator::new(ast.ref_link_tags())?); 66 | let v = vec![toc, body]; 67 | Ok(v) 68 | } 69 | 70 | // Convert markdown ast into body part of the html 71 | pub fn to_body(ast: &Ast) -> Result, Box> { 72 | let body = ast.generate_content(&html::Generator::new(ast.ref_link_tags())?); 73 | let v = vec![body]; 74 | Ok(v) 75 | } 76 | 77 | // Generate the toc part of the html from markdown ast 78 | pub fn to_toc(ast: &Ast) -> Result, Box> { 79 | let toc = ast.generate_toc(&html::Generator::new(ast.ref_link_tags())?); 80 | let v = vec![toc]; 81 | Ok(v) 82 | } 83 | 84 | // Generate the slice of markdown 85 | pub fn to_slice(ast: &Ast) -> Result, Box> { 86 | Ok(ast.generate_slice(&html::Generator::new(ast.ref_link_tags())?)) 87 | } 88 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::collections::HashMap; 3 | use std::fmt::Debug; 4 | use std::fs::File; 5 | use std::io::{BufRead, BufReader}; 6 | use std::rc::Rc; 7 | use std::{fmt, io}; 8 | 9 | use crate::lexer::{Lexer, Token, TokenKind}; 10 | use crate::Generate; 11 | use crate::SharedLine; 12 | 13 | use itertools::Itertools; 14 | 15 | // Ast represents the abstract syntax tree of the markdown file, it structurally represents the entire file. 16 | pub struct Ast { 17 | // Store all parsed line structs in order 18 | document: Vec, 19 | // Related lines are compressed into the same block 20 | blocks: Vec, 21 | // Store all tags of the ref link, the map is "tag -> (location, title)" 22 | ref_link_tags: HashMap, 23 | // The block of the table of contents, it's a unordered list 24 | toc: Vec, 25 | } 26 | 27 | impl Ast { 28 | // Create a Ast instance. 29 | pub(crate) fn new() -> Self { 30 | Ast { 31 | document: vec![Rc::new(RefCell::new(Line::meta()))], 32 | blocks: vec![], 33 | ref_link_tags: HashMap::new(), 34 | toc: vec![], 35 | } 36 | } 37 | 38 | // TODO: 39 | pub(crate) fn _insert_line(&mut self, _ln: usize, _s: &str) {} 40 | 41 | // TODO: 42 | pub(crate) fn _update_line(&mut self, _ln: usize, _s: &str) {} 43 | 44 | // TODO: 45 | pub(crate) fn _delete_line(&mut self, _ln: usize) {} 46 | 47 | // Parse markdown document from a file, the 'path' argument is the file path. 48 | pub(crate) fn parse_file(&mut self, path: &str) -> Result<(), io::Error> { 49 | let file = File::open(path)?; 50 | self.parse_from(&mut BufReader::new(file)) 51 | } 52 | 53 | // Parse markdown document from a string. 54 | pub(crate) fn parse_string(&mut self, s: &str) -> Result<(), io::Error> { 55 | self.parse_from(&mut s.as_bytes()) 56 | } 57 | 58 | // Parse markdown document from a reader, the 'reader' may be a file reader, byte buff or network socket etc. 59 | pub(crate) fn parse_from(&mut self, reader: &mut dyn BufRead) -> Result<(), io::Error> { 60 | let mut is_lazy = false; 61 | let mut lazy_queue: Vec = vec![]; 62 | 63 | let mut ln: usize = 0; 64 | 65 | loop { 66 | let (n, buf) = Self::read_line(reader)?; 67 | if n == 0 { 68 | break; 69 | } 70 | ln += 1; 71 | 72 | let l = Line::new(ln, buf); 73 | let lref = Rc::new(RefCell::new(l)); 74 | 75 | // add the scope block to reduce the lifecycle of the 'l' (l = lrc.borrow_mut()) 76 | { 77 | let mut l = lref.borrow_mut(); 78 | 79 | match l.pre_parse() { 80 | Kind::TocPosition => l.weak_parse(Kind::TocPosition), 81 | Kind::CodeBlockMark => { 82 | l.strong_parse(); 83 | if is_lazy { 84 | lazy_queue.clear(); 85 | } 86 | is_lazy = !is_lazy; 87 | } 88 | Kind::PlainText => { 89 | if is_lazy { 90 | // lazy parsing 91 | lazy_queue.push(Rc::clone(&lref)); 92 | l.weak_parse(Kind::CodeBlock); 93 | } else { 94 | l.strong_parse(); 95 | } 96 | } 97 | _ => unreachable!(), 98 | } 99 | 100 | // postpone 101 | l.pick_reflink_tags(&mut self.ref_link_tags); 102 | } 103 | self.document.push(lref); 104 | 105 | debug_assert_eq!(self.count_lines(), ln); 106 | debug_assert_eq!(self.document[ln].borrow().num, ln); 107 | } // end of loop 108 | 109 | lazy_queue 110 | .iter() 111 | .for_each(|l| l.borrow_mut().strong_parse()); 112 | lazy_queue.clear(); 113 | self.init_content_block(); 114 | self.init_toc_block(); 115 | 116 | Ok(()) 117 | } 118 | 119 | fn read_line(reader: &mut dyn BufRead) -> Result<(usize, String), io::Error> { 120 | let mut buf = String::new(); 121 | let num_bytes = reader.read_line(&mut buf)?; 122 | if num_bytes == 0 { 123 | return Ok((0, "".to_string())); 124 | } 125 | if !buf.ends_with('\n') { 126 | buf.push('\n'); 127 | } 128 | Ok((num_bytes, buf)) 129 | } 130 | 131 | // Generate the contents of the document 132 | pub(crate) fn generate_content(&self, generator: &impl Generate) -> String { 133 | self.generate(self.content_blocks(), generator) 134 | } 135 | 136 | // Generate the table of contents based on the title blocks and we skipped the level 1 title 137 | pub(crate) fn generate_toc(&self, generator: &impl Generate) -> String { 138 | self.generate(&self.toc, generator) 139 | } 140 | 141 | // Generate the slice of the contents of the document based on the dividing blocks 142 | pub(crate) fn generate_slice(&self, generator: &impl Generate) -> Vec { 143 | let mut v = vec![]; 144 | let mut start = 0; 145 | for (i, b) in self.blocks.iter().enumerate() { 146 | if b.kind() == Kind::Dividing { 147 | v.push(self.generate(&self.blocks[start..i], generator)); 148 | start = i + 1; 149 | } 150 | } 151 | v.push(self.generate(&self.blocks[start..], generator)); 152 | v 153 | } 154 | 155 | // Iterate through each block of the Ast and process the block into a 'html' string 156 | fn generate(&self, blocks: &[Block], generator: &impl Generate) -> String { 157 | blocks 158 | .iter() 159 | .filter(|b| b.kind() != Kind::Meta__ && b.kind() != Kind::ListNesting__) 160 | .map(|b| match b.kind() { 161 | Kind::Title => generator.render_title(b.first()), 162 | Kind::PlainText => generator.render_plain_text(b.contains()), 163 | Kind::Dividing => generator.render_dividing(b.first()), 164 | Kind::CodeBlock => generator.render_code(b.contains()), 165 | Kind::UnorderedList => generator.render_unordered_list(b.contains()), 166 | Kind::Blank => generator.render_blank(b.contains()), 167 | Kind::Quote => { 168 | let s = b 169 | .quote_ast 170 | .as_ref() 171 | .map(|a| a.generate_content(generator)) 172 | .unwrap_or_else(|| "".to_string()); 173 | generator.render_quote(&s) 174 | } 175 | Kind::OrderedList => generator.render_ordered_list(b.contains()), 176 | Kind::CodeBlockMark => generator.render_plain_text(b.contains()), // treat code block mark as plain text 177 | Kind::TocPosition => self.generate_toc(generator), // Note: here is a toc position 178 | _ => unreachable!(), 179 | }) 180 | .filter(|s| !s.is_empty()) 181 | .join("\n\n") 182 | } 183 | 184 | // Count the lines in ast 185 | pub(crate) fn count_lines(&self) -> usize { 186 | self.document.len() - 1 187 | } 188 | 189 | pub(crate) fn ref_link_tags(&self) -> &HashMap { 190 | &self.ref_link_tags 191 | } 192 | 193 | fn content_blocks(&self) -> &Vec { 194 | &self.blocks 195 | } 196 | 197 | fn init_toc_block(&mut self) { 198 | const MIN_LEVEL: usize = 1; 199 | const MAX_LEVEL: usize = 3; 200 | 201 | let mut lines: Vec = vec![]; 202 | 203 | for l in self.document.iter().filter(|l| { 204 | l.borrow().kind == Kind::Title 205 | && l.borrow().mark_token().len() >= MIN_LEVEL 206 | && l.borrow().mark_token().len() <= MAX_LEVEL 207 | }) { 208 | let mut buff: Vec = vec![]; 209 | 210 | // create a new indent token with white space. 211 | let level = l.borrow().mark_token().len(); 212 | if level > MIN_LEVEL { 213 | buff.push(Token::new( 214 | " ".repeat(level - MIN_LEVEL), 215 | TokenKind::WhiteSpace, 216 | )); 217 | } 218 | 219 | // create a new unordered mark token 220 | buff.push(Token::new("*".to_string(), TokenKind::UnorderedMark)); 221 | 222 | // create a new link token 223 | let (id, name) = l.borrow().anchor(); 224 | let location = format!("#{}", id); 225 | 226 | let mut t = Token::new(format!("[{}]({})", name, location), TokenKind::Link); 227 | t.as_generic_link_mut().insert_name(&name); 228 | t.as_generic_link_mut().insert_location(&location); 229 | buff.push(t); 230 | 231 | // create a new line for toc 232 | let l2 = Line { 233 | kind: Kind::UnorderedList, 234 | buff, 235 | num: l.borrow().num, 236 | text: name, 237 | nested_lines: vec![], 238 | nested_blocks: vec![], 239 | }; 240 | 241 | lines.push(Rc::new(RefCell::new(l2))); 242 | } 243 | 244 | self.toc = Self::establish_blocks(&lines); 245 | } 246 | 247 | fn init_content_block(&mut self) { 248 | self.blocks = Self::establish_blocks(&self.document); 249 | } 250 | 251 | fn establish_blocks(lines: &[SharedLine]) -> Vec { 252 | let mut blocks: Vec = vec![]; 253 | 254 | let mut leader: Option<&SharedLine> = None; 255 | let mut state: Option = None; 256 | 257 | let mut iter = lines 258 | .iter() 259 | .filter(|l| l.borrow().kind != Kind::Meta__) 260 | .peekable(); 261 | 262 | while let Some(l) = iter.next() { 263 | let mut curr_line = l.borrow_mut(); 264 | let curr_state = state.unwrap_or(curr_line.kind); 265 | 266 | match curr_state { 267 | Kind::UnorderedList | Kind::OrderedList => { 268 | if let Some(b) = blocks.last_mut().filter(|b| b.kind() == curr_line.kind) { 269 | b.push(Rc::clone(l)); 270 | } else { 271 | Self::insert_block(&mut blocks, Block::new(Rc::clone(l), curr_line.kind)); 272 | } 273 | 274 | // determine whether the next line is a list nesting 275 | if let Some(next) = iter.peek() { 276 | if next.borrow().is_nested(&curr_line) > 0 { 277 | state = Some(Kind::ListNesting__); 278 | leader = Some(l); // save the previous line object as leader 279 | } 280 | } 281 | } 282 | Kind::ListNesting__ => { 283 | // leader must not be None 284 | debug_assert!(leader.is_some()); 285 | 286 | if let Some(ld) = leader { 287 | let mut ld = ld.borrow_mut(); 288 | ld.nested_lines.push(Rc::clone(l)); 289 | 290 | if let Some(next) = iter.peek() { 291 | if next.borrow().is_nested(&ld) <= 0 { 292 | (state, leader) = (None, None); 293 | } 294 | } 295 | } 296 | } 297 | Kind::CodeBlockMark => { 298 | let mut k: Option = None; 299 | if let Some(next) = iter.peek() { 300 | let next = next.borrow(); 301 | if next.kind == Kind::CodeBlockMark || next.kind == Kind::CodeBlock { 302 | k = Some(Kind::CodeBlock); 303 | state = Some(Kind::CodeBlock); 304 | } 305 | } 306 | Self::insert_block( 307 | &mut blocks, 308 | Block::new(Rc::clone(l), k.unwrap_or(Kind::CodeBlockMark)), 309 | ); 310 | } 311 | Kind::CodeBlock => { 312 | let b = blocks.last_mut().filter(|b| b.kind() == Kind::CodeBlock); 313 | debug_assert!(b.is_some()); 314 | 315 | if let Some(b) = b { 316 | b.push(Rc::clone(l)); 317 | } 318 | 319 | if curr_line.kind == Kind::CodeBlockMark { 320 | // close this code block 321 | state = None; 322 | } 323 | } 324 | Kind::Blank | Kind::Quote | Kind::PlainText => { 325 | if let Some(b) = blocks.last_mut().filter(|b| b.kind() == curr_line.kind) { 326 | b.push(Rc::clone(l)); 327 | } else { 328 | Self::insert_block(&mut blocks, Block::new(Rc::clone(l), curr_line.kind)); 329 | } 330 | } 331 | Kind::Dividing => { 332 | // get kind of the previous line 333 | let prev = lines.get(curr_line.num - 1).map(|v| v.borrow().kind); 334 | // get kind of the next line 335 | let next = iter.peek().map(|v| v.borrow().kind); 336 | 337 | if prev.unwrap_or(Kind::Blank) == Kind::Blank 338 | && next.unwrap_or(Kind::Blank) == Kind::Blank 339 | { 340 | Self::insert_block(&mut blocks, Block::new(Rc::clone(l), Kind::Dividing)) 341 | } else { 342 | // convert dividing to plain text 343 | curr_line.kind = Kind::PlainText; 344 | curr_line 345 | .buff 346 | .iter_mut() 347 | .for_each(|t| t.downgrade_to_text()); 348 | 349 | if let Some(b) = blocks.last_mut().filter(|b| b.kind() == Kind::PlainText) { 350 | b.push(Rc::clone(l)); 351 | } else { 352 | Self::insert_block( 353 | &mut blocks, 354 | Block::new(Rc::clone(l), Kind::PlainText), 355 | ); 356 | } 357 | } 358 | } 359 | Kind::Title => { 360 | Self::insert_block(&mut blocks, Block::new(Rc::clone(l), Kind::Title)); 361 | } 362 | Kind::TocPosition => { 363 | Self::insert_block(&mut blocks, Block::new(Rc::clone(l), Kind::TocPosition)); 364 | } 365 | Kind::Meta__ => unreachable!(), 366 | } // end of match 367 | } // end of while 368 | 369 | // build nested lists recursively 370 | for b in blocks 371 | .iter() 372 | .filter(|b| b.kind() == Kind::UnorderedList || b.kind() == Kind::OrderedList) 373 | { 374 | b.contains() 375 | .iter() 376 | .filter(|l| !l.borrow().nested_lines.is_empty()) 377 | .for_each(|l| { 378 | let mut l = l.borrow_mut(); 379 | let mut bs = Self::establish_blocks(&l.nested_lines); 380 | l.nested_blocks.append(&mut bs); 381 | }); 382 | } 383 | 384 | Self::parse_quote_block(&mut blocks); 385 | blocks 386 | } 387 | 388 | // Parse quote block into a new ast 389 | fn parse_quote_block(blocks: &mut [Block]) { 390 | for b in blocks.iter_mut().filter(|b| b.kind() == Kind::Quote) { 391 | let mut ast = Ast::new(); 392 | 393 | // Since there is a newline(\n) character at the end of each line, so we use empty string ("") to join them 394 | let text = b 395 | .contains() 396 | .iter() 397 | .map(|e| { 398 | let e = e.borrow(); 399 | let last = e.last_token(); 400 | if last.kind() == TokenKind::Text { 401 | last.value().to_string() 402 | } else { 403 | "\n".to_string() 404 | } 405 | }) 406 | .collect::>() 407 | .join(""); 408 | 409 | ast.parse_string(&text).unwrap_or_else(|_e| unreachable!()); 410 | b.quote_ast = Some(ast); 411 | } 412 | } 413 | 414 | fn insert_block(blocks: &mut Vec, mut b: Block) { 415 | b.seq = blocks.len(); 416 | blocks.push(b); 417 | } 418 | } 419 | 420 | impl Default for Ast { 421 | fn default() -> Self { 422 | Self::new() 423 | } 424 | } 425 | 426 | impl Debug for Ast { 427 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 428 | let mut debug = String::new(); 429 | for line in self.document.iter() { 430 | debug.push_str(format!("[{}, {:?}]: ", line.borrow().num, line.borrow().kind).as_str()); 431 | for t in line.borrow().all() { 432 | let s = format!("{:?} ", t); 433 | debug.push_str(&s); 434 | } 435 | debug.push('\n'); 436 | } 437 | writeln!(f, "{}", debug) 438 | } 439 | } 440 | 441 | #[derive(Debug, PartialEq, Copy, Clone)] 442 | enum Kind { 443 | PlainText, 444 | Blank, 445 | Title, 446 | UnorderedList, 447 | OrderedList, 448 | Dividing, 449 | Quote, 450 | CodeBlockMark, 451 | CodeBlock, 452 | TocPosition, 453 | Meta__, 454 | ListNesting__, 455 | } 456 | 457 | // Block is a combination of associated lines. 458 | #[derive(Debug)] 459 | struct Block { 460 | seq: usize, 461 | kind: Kind, 462 | contains: Vec, 463 | quote_ast: Option, 464 | } 465 | 466 | impl Block { 467 | fn new(l: SharedLine, kind: Kind) -> Self { 468 | Block { 469 | contains: vec![l], 470 | kind, 471 | seq: 0, 472 | quote_ast: None, 473 | } 474 | } 475 | 476 | fn contains(&self) -> &Vec { 477 | &self.contains 478 | } 479 | 480 | fn first(&self) -> &SharedLine { 481 | &self.contains[0] 482 | } 483 | 484 | fn kind(&self) -> Kind { 485 | self.kind 486 | } 487 | 488 | fn push(&mut self, l: SharedLine) { 489 | self.contains.push(l) 490 | } 491 | } 492 | 493 | // Line is a line of the markdown file, it be parsed into some tokens. 494 | #[derive(Debug)] 495 | pub struct Line { 496 | buff: Vec, 497 | kind: Kind, 498 | num: usize, 499 | text: String, 500 | // 501 | // The lines in nesting: 502 | // Kind::UnorderedList 503 | // Kind::OrderedList 504 | // Kind::Quote 505 | // Kind::Normal 506 | nested_lines: Vec, 507 | nested_blocks: Vec, 508 | } 509 | 510 | impl Line { 511 | fn new(ln: usize, line: String) -> Self { 512 | Line { 513 | num: ln, 514 | text: line, 515 | kind: Kind::PlainText, 516 | buff: vec![], 517 | nested_lines: vec![], // Note: fill the nested lines when establish blocks 518 | nested_blocks: vec![], // Note: fill the nested blocks when establish blocks 519 | } 520 | } 521 | 522 | // Not parse the line text into tokens, just set the kind of the line. 523 | fn weak_parse(&mut self, kind: Kind) { 524 | self.kind = kind; 525 | } 526 | 527 | // Parse a line of text into 'Line' struct that contains multi tokens. 528 | // Line's kind is determinded by the mark token's kind. 529 | fn strong_parse(&mut self) { 530 | self.buff = Lexer::new(&self.text).split(); 531 | 532 | self.kind = match self.mark_token().kind() { 533 | TokenKind::BlankLine => Kind::Blank, 534 | TokenKind::TitleMark => Kind::Title, 535 | TokenKind::UnorderedMark => Kind::UnorderedList, 536 | TokenKind::OrderedMark => Kind::OrderedList, 537 | TokenKind::DividingMark => Kind::Dividing, 538 | TokenKind::QuoteMark => Kind::Quote, 539 | TokenKind::CodeBlockMark => Kind::CodeBlockMark, 540 | _ => Kind::PlainText, 541 | }; 542 | 543 | debug_assert!(!self.all().is_empty()); 544 | } 545 | 546 | // Try to parse the line text simply 547 | fn pre_parse(&self) -> Kind { 548 | let l = self.text().trim(); 549 | if l.starts_with("```") { 550 | // To parse the line of code block mark 551 | Kind::CodeBlockMark 552 | } else if l.starts_with("") { 553 | // To parse the line of toc position () 554 | let x: &[_] = &['<', '>', '!', '-', ' ']; 555 | if l.trim_matches(x).eq_ignore_ascii_case("toc") { 556 | Kind::TocPosition 557 | } else { 558 | Kind::PlainText 559 | } 560 | } else { 561 | Kind::PlainText 562 | } 563 | } 564 | 565 | // Get number of the indent, two white space(' ') or one '\t' is a indent 566 | fn indents(&self) -> isize { 567 | let first = self.first_token(); 568 | if first.kind() != TokenKind::WhiteSpace { 569 | return 0; 570 | } 571 | let sum: isize = first 572 | .value() 573 | .chars() 574 | .map(|c| if c == '\t' { 2 } else { 1 }) 575 | .sum(); 576 | sum / 2 577 | } 578 | 579 | // Determine whether the current line is a nested line of 'parent' 580 | // The return value is the number of nested indents, it's not a nested if less than or equal to 0. 581 | fn is_nested(&self, parent: &Line) -> isize { 582 | if self.kind == Kind::Blank 583 | || self.kind == Kind::Title 584 | || self.kind == Kind::Dividing 585 | || self.kind == Kind::CodeBlockMark 586 | || self.kind == Kind::CodeBlock 587 | { 588 | 0 589 | } else { 590 | self.indents() - parent.indents() 591 | } 592 | } 593 | 594 | fn first_token(&self) -> &Token { 595 | &self.buff[0] 596 | } 597 | 598 | fn last_token(&self) -> &Token { 599 | self.all().last().unwrap_or_else(|| self.first_token()) 600 | } 601 | 602 | fn pick_reflink_tags(&self, tags: &mut HashMap) { 603 | for t in self 604 | .all() 605 | .iter() 606 | .filter(|t| t.kind() == TokenKind::RefLinkDef) 607 | { 608 | let gl = t.as_generic_link(); 609 | let (tag, location, title) = (gl.tag(), gl.location(), gl.title()); 610 | if !tag.is_empty() { 611 | tags.insert(tag.to_string(), (location.to_string(), title.to_string())); 612 | } 613 | } 614 | } 615 | 616 | fn meta() -> Self { 617 | let mut l = Self::new(0, "meta".to_string()); 618 | l.weak_parse(Kind::Meta__); 619 | l 620 | } 621 | 622 | pub(crate) fn enter_nested_blocks(&self, generator: &impl Generate) -> String { 623 | self.nested_blocks 624 | .iter() 625 | .filter(|b| { 626 | b.kind() != Kind::Meta__ 627 | && b.kind() != Kind::ListNesting__ 628 | && b.kind() != Kind::Blank 629 | && b.kind() != Kind::Title 630 | && b.kind() != Kind::Dividing 631 | && b.kind() != Kind::CodeBlockMark 632 | && b.kind() != Kind::CodeBlock 633 | }) 634 | .map(|b| match b.kind() { 635 | Kind::Title => generator.render_title(b.first()), 636 | Kind::PlainText => generator.render_plain_text(b.contains()), 637 | Kind::Dividing => generator.render_dividing(b.first()), 638 | Kind::CodeBlock => generator.render_code(b.contains()), 639 | Kind::UnorderedList => generator.render_unordered_list(b.contains()), 640 | Kind::Blank => generator.render_blank(b.contains()), 641 | Kind::Quote => { 642 | let s = b 643 | .quote_ast 644 | .as_ref() 645 | .map(|a| a.generate_content(generator)) 646 | .unwrap_or_else(|| "".to_string()); 647 | generator.render_quote(&s) 648 | } 649 | Kind::OrderedList => generator.render_ordered_list(b.contains()), 650 | _ => "".to_string(), 651 | }) 652 | .join("\n") 653 | } 654 | 655 | // Get the mark token in the Line, the mark token may be the first or second 656 | pub(crate) fn mark_token(&self) -> &Token { 657 | let first = self.first_token(); 658 | if first.kind() == TokenKind::WhiteSpace { 659 | // if the first token is 'WhiteSpace', the second token must be exist 660 | &self.buff[1] 661 | } else { 662 | first 663 | } 664 | } 665 | 666 | // Get the Nth Token in the Line 667 | pub(crate) fn get(&self, at: usize) -> Option<&Token> { 668 | self.buff.get(at) 669 | } 670 | 671 | // Get all tokens in the Line 672 | pub(crate) fn all(&self) -> &Vec { 673 | &self.buff 674 | } 675 | 676 | // Get the line text 677 | pub(crate) fn text(&self) -> &str { 678 | &self.text 679 | } 680 | 681 | // create a anchor name and id for the line 682 | pub(crate) fn anchor(&self) -> (String, String) { 683 | if self.kind != Kind::Title { 684 | panic!("Only title line can create anchor"); 685 | } 686 | let ss: Vec = self 687 | .all() 688 | .iter() 689 | .filter(|t| t.kind() != TokenKind::WhiteSpace && t.kind() != TokenKind::TitleMark) 690 | .map(|t| t.html_escaped_value()) 691 | .collect(); 692 | 693 | let name = ss.join(""); 694 | ( 695 | format!("{}-{}", &name.to_lowercase().replace(' ', "-"), self.num), 696 | name, 697 | ) 698 | } 699 | } 700 | 701 | #[cfg(test)] 702 | mod tests { 703 | use super::*; 704 | 705 | struct MockGenerator {} 706 | impl Generate for MockGenerator { 707 | fn render_unordered_list(&self, ls: &[SharedLine]) -> String { 708 | let list: Vec = ls 709 | .iter() 710 | .map(|l| { 711 | let leader = l.borrow().text().trim().to_string(); 712 | let nesting = l.borrow().enter_nested_blocks(&MockGenerator {}); 713 | if !nesting.is_empty() { 714 | leader + nesting.as_str() 715 | } else { 716 | leader 717 | } 718 | }) 719 | .map(|s| format!("
  • {}
  • ", s)) 720 | .collect(); 721 | format!("
      {}
    ", list.join("")) 722 | } 723 | } 724 | 725 | fn exec_document_cases(doc: &Vec) -> Vec<(Kind, usize, usize, usize)> { 726 | doc.iter() 727 | .skip(1) 728 | .map(|x| { 729 | let x = x.borrow(); 730 | (x.kind, x.num, x.nested_lines.len(), x.nested_blocks.len()) 731 | }) 732 | .collect() 733 | } 734 | 735 | fn exec_blocks_cases(blocks: &Vec) -> Vec<(Kind, usize, Option)> { 736 | blocks 737 | .iter() 738 | .map(|x| { 739 | ( 740 | x.kind(), 741 | x.contains.len(), 742 | x.quote_ast 743 | .as_ref() 744 | .map(|a| Some(a.count_lines())) 745 | .unwrap_or(None), 746 | ) 747 | }) 748 | .collect() 749 | } 750 | 751 | #[test] 752 | fn test_code_block() { 753 | let md = r#"这是一个代码块的例子: 754 | ``` 755 | let s = \"hello world\"; 756 | let s1 = s.to_string(); 757 | ``` 758 | ``` 759 | let s; 760 | assert_eq!(ast.doc[0].sequence[1].kind, TokenKind::Url); 761 | assert_eq!(ast.doc[0].sequence[1].value, url.to_string());"#; 762 | let mut ast = Ast::new(); 763 | ast.parse_string(md).unwrap(); 764 | 765 | // (line kind, line number, nested line count, nested block count) 766 | let document = vec![ 767 | (Kind::PlainText, 1, 0, 0), 768 | (Kind::CodeBlockMark, 2, 0, 0), 769 | (Kind::CodeBlock, 3, 0, 0), 770 | (Kind::CodeBlock, 4, 0, 0), 771 | (Kind::CodeBlockMark, 5, 0, 0), 772 | (Kind::CodeBlockMark, 6, 0, 0), 773 | (Kind::PlainText, 7, 0, 0), 774 | (Kind::PlainText, 8, 0, 0), 775 | (Kind::PlainText, 9, 0, 0), 776 | ]; 777 | assert_eq!(exec_document_cases(&ast.document), document); 778 | 779 | // (block kind, line count in block, line count of quote ast) 780 | let blocks = vec![ 781 | (Kind::PlainText, 1, None), 782 | (Kind::CodeBlock, 4, None), 783 | (Kind::CodeBlockMark, 1, None), 784 | (Kind::PlainText, 3, None), 785 | ]; 786 | assert_eq!(exec_blocks_cases(ast.content_blocks()), blocks); 787 | } 788 | 789 | #[test] 790 | fn test_quote_block() { 791 | { 792 | let md = r#"# header1 793 | > hello, rust 794 | > hello, world 795 | >> hello, nested 796 | 797 | 798 | "#; 799 | 800 | let mut ast = Ast::new(); 801 | ast.parse_string(md).unwrap(); 802 | 803 | // (line kind, line number, nested line count, nested block count) 804 | let document = vec![ 805 | (Kind::Title, 1, 0, 0), 806 | (Kind::Quote, 2, 0, 0), 807 | (Kind::Quote, 3, 0, 0), 808 | (Kind::Quote, 4, 0, 0), 809 | (Kind::Blank, 5, 0, 0), 810 | (Kind::PlainText, 6, 0, 0), 811 | ]; 812 | assert_eq!(exec_document_cases(&ast.document), document); 813 | 814 | // (block kind, line count in block, line count of quote ast) 815 | let blocks = vec![ 816 | (Kind::Title, 1, None), 817 | (Kind::Quote, 3, Some(3)), 818 | (Kind::Blank, 1, None), 819 | (Kind::PlainText, 1, None), 820 | ]; 821 | assert_eq!(exec_blocks_cases(ast.content_blocks()), blocks); 822 | } 823 | } 824 | 825 | #[test] 826 | fn test_nested_list() { 827 | let md = r#"## 无序列表 828 | - 列表项 1 829 | 嵌入文本 1 830 | - 嵌入项 1 831 | - 嵌入项 2 832 | - 列表项 2 833 | - 列表项 3"#; 834 | let mut ast = Ast::new(); 835 | ast.parse_string(md).unwrap(); 836 | 837 | let document = vec![ 838 | (Kind::Title, 1, 0, 0), 839 | (Kind::UnorderedList, 2, 3, 2), 840 | (Kind::PlainText, 3, 0, 0), 841 | (Kind::UnorderedList, 4, 0, 0), 842 | (Kind::UnorderedList, 5, 0, 0), 843 | (Kind::UnorderedList, 6, 0, 0), 844 | (Kind::UnorderedList, 7, 0, 0), 845 | ]; 846 | assert_eq!(exec_document_cases(&ast.document), document); 847 | 848 | // (block kind, line count in block, line count of quote ast) 849 | let blocks = vec![(Kind::Title, 1, None), (Kind::UnorderedList, 3, None)]; 850 | assert_eq!(exec_blocks_cases(ast.content_blocks()), blocks); 851 | } 852 | 853 | #[test] 854 | fn test_ref_link_tags() { 855 | let md = r#"## 链接 856 | [Example][link]
    857 | [link]: https://www.example.com "example""#; 858 | let mut ast = Ast::new(); 859 | ast.parse_string(md).unwrap(); 860 | 861 | assert_eq!(ast.ref_link_tags().len(), 1); 862 | assert_eq!( 863 | ast.ref_link_tags().get("link"), 864 | Some(&("https://www.example.com".to_string(), "example".to_string())) 865 | ); 866 | } 867 | 868 | #[test] 869 | fn test_generate_toc() { 870 | let md = r#" 871 | # header1 872 | ## header2 873 | ## header2 874 | ### header3 875 | #### header4 876 | ## header2 877 | "#; 878 | 879 | let dest = "
      \ 880 |
    • header1\ 881 |
        \ 882 |
      • header2
      • \ 883 |
      • header2\ 884 |
          \ 885 |
        • header3\ 886 |
        • \ 887 |
        \ 888 |
      • \ 889 |
      • header2
      • \ 890 |
      \ 891 |
    • \ 892 |
    "; 893 | let mut ast = Ast::new(); 894 | ast.parse_string(md).unwrap(); 895 | 896 | let s = ast.generate_toc(&MockGenerator {}); 897 | assert_eq!(s, dest); 898 | } 899 | } 900 | -------------------------------------------------------------------------------- /src/utils/cursor.rs: -------------------------------------------------------------------------------- 1 | pub(crate) struct Cursor<'cursor> { 2 | s: &'cursor str, 3 | index: usize, 4 | } 5 | 6 | impl<'cursor> Cursor<'cursor> { 7 | // Create a cursor instance 8 | pub(crate) fn new(s: &'cursor str) -> Self { 9 | Cursor { s, index: 0 } 10 | } 11 | 12 | pub(crate) fn index(&self) -> usize { 13 | self.index 14 | } 15 | 16 | pub(crate) fn move_one(&mut self) { 17 | self.index += 1; 18 | } 19 | 20 | pub(crate) fn rest_slice(&self) -> &str { 21 | utf8_slice::from(self.s, self.index) 22 | } 23 | 24 | // Get the sub-string after the cursor, contains the position of the cursor 25 | // not contains the end position. 26 | pub(crate) fn slice_to(&self, end: usize) -> &str { 27 | if self.index > end { 28 | panic!("begin > end: {}, {}", self.index, end); 29 | } 30 | utf8_slice::slice(self.s, self.index, end) 31 | } 32 | 33 | // Get the sub-string before the cursor, not contains the position of the cursor 34 | // contains the begin position. 35 | pub(crate) fn _before_slice(&self, begin: usize) -> &str { 36 | if begin > self.index { 37 | panic!("begine > end: {}, {}", begin, self.index); 38 | } 39 | utf8_slice::slice(self.s, begin, self.index) 40 | } 41 | 42 | // Move the cursor to the end position 43 | pub(crate) fn consume_to(&mut self, end: usize, mut f: F) 44 | where 45 | F: FnMut(&str), 46 | { 47 | let sub = self.slice_to(end); 48 | if !sub.is_empty() { 49 | f(sub); 50 | } 51 | self.index = end; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | use email_address::EmailAddress; 2 | use lazy_static::lazy_static; 3 | use regex::Regex; 4 | use url::Url; 5 | 6 | pub(crate) mod cursor; 7 | pub(crate) mod stack; 8 | 9 | // This regex is used to match a string with double quotes("") or single quotes('') 10 | lazy_static! { 11 | static ref D_QUOTED_STRING_RE: Regex = Regex::new("^\"([^\"\\\\]|\\\\.)*\"$").unwrap(); 12 | static ref S_QUOTED_STRING_RE: Regex = Regex::new("^\'([^\'\\\\]|\\\\.)*\'$").unwrap(); 13 | } 14 | 15 | pub fn is_quoted_string(s: &str) -> bool { 16 | D_QUOTED_STRING_RE.is_match(s) || S_QUOTED_STRING_RE.is_match(s) 17 | } 18 | 19 | pub fn is_url(s: &str) -> bool { 20 | Url::try_from(s).is_ok() 21 | } 22 | 23 | pub fn is_email(s: &str) -> bool { 24 | EmailAddress::is_valid(s) 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/stack.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | pub(crate) struct Stack { 4 | queue: Vec, 5 | } 6 | 7 | impl Stack 8 | where 9 | T: Debug, 10 | { 11 | pub(crate) fn new() -> Self { 12 | Stack { queue: vec![] } 13 | } 14 | 15 | // Insert a new element at the top of the stack 16 | pub(crate) fn push(&mut self, e: T) { 17 | self.queue.push(e) 18 | } 19 | 20 | // Remove the element from the top of the stack and return it 21 | pub(crate) fn pop(&mut self) -> Option { 22 | self.queue.pop() 23 | } 24 | 25 | // Get the element at the top of the stack 26 | pub(crate) fn _top(&self) -> Option<&T> { 27 | self.queue.last() 28 | } 29 | 30 | // Get the element number in the stack 31 | pub(crate) fn _len(&self) -> usize { 32 | self.queue.len() 33 | } 34 | 35 | // Remove the element from the top of the stack, if 'f' return true. Otherwise insert the 36 | // argument 'e' at the top of the stack. 37 | pub(crate) fn pop_or_push(&mut self, e: T, f: F) -> Option 38 | where 39 | F: Fn(&T) -> bool, 40 | { 41 | if let Some(e) = self.queue.last() { 42 | if f(e) { 43 | return self.pop(); 44 | } 45 | } 46 | self.push(e); 47 | None 48 | } 49 | 50 | // Remove and return all elements of a range from 'f' being true to the top of the stack. 51 | pub(crate) fn pop_range(&mut self, f: F) -> Vec 52 | where 53 | F: Fn(&T) -> bool, 54 | { 55 | let mut pops: Vec = vec![]; 56 | let mut position: Option = None; 57 | 58 | for (ix, e) in self.queue.iter().rev().enumerate() { 59 | if f(e) { 60 | position = Some(self.queue.len() - 1 - ix); 61 | break; 62 | } 63 | } 64 | if let Some(p) = position { 65 | loop { 66 | if let Some(pop) = self.pop() { 67 | pops.push(pop); 68 | if self.queue.len() == p { 69 | break; 70 | } 71 | } else { 72 | unreachable!() 73 | } 74 | } 75 | pops.reverse(); 76 | } 77 | pops 78 | } 79 | 80 | pub(crate) fn all_mut(&mut self) -> &mut Vec { 81 | &mut self.queue 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /themes/github/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "body_min_width": 200, 3 | "body_max_width": 900, 4 | "slice_header": "", 5 | "use_slice_mode": false 6 | } -------------------------------------------------------------------------------- /themes/github/template.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {title} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 |
    24 | {{ if use_slice_mode }} 25 | 26 | {{ for slice_content in slices }} 27 |
    28 |
    {slice_header}
    29 |
    {slice_content}
    30 |
    31 | {{ endfor }} 32 | {{ else }} 33 | 34 | { content } 35 | {{ endif }} 36 |
    37 | 38 | -------------------------------------------------------------------------------- /themes/hard/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "body_min_width": 200, 3 | "body_max_width": 900, 4 | "slice_header": "", 5 | "use_slice_mode": false 6 | } -------------------------------------------------------------------------------- /themes/hard/dark.css: -------------------------------------------------------------------------------- 1 | @import "reboto/fonts.css"; 2 | 3 | :root { 4 | /* color define */ 5 | --main-color: #c408f8; 6 | --stress-color: #01c8ee; 7 | --main-background-color: #0e162c; 8 | --secondary-background-color: #1a243f; 9 | --help2-color: #d6116d; 10 | 11 | --text-color: #b7b1bf; 12 | --text-color2: #68646c; 13 | --secondary-text-color: #3f4447; 14 | 15 | /* Text */ 16 | --light-trait-100: var(--main-color); 17 | --light-trait-200: var(--main-color); 18 | --light-trait-300: var(--main-text-color); 19 | --light-trait-400: var(--main-text-color); 20 | 21 | --url-text-color: var(--stress-color); 22 | --url-underline-color: var(--stress-color); 23 | 24 | /* General */ 25 | --font-size: 16px; 26 | --font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 27 | --monospace: "Zed Mono", "Source Code Pro", "SF Mono Medium", "Fira Code", "Cousine", "Consolas", monospace; 28 | --border-radius: 4px; 29 | 30 | 31 | /* Code variables */ 32 | --cm-line: #b8babb; 33 | --cm-variable: var(--text-color); 34 | --cm-keyword: #d1949e; 35 | --cm-tag: #d1949e; 36 | --cm-bracket: #d1949e; 37 | --cm-error: #ff5a5a; 38 | --cm-attribute: #d1949e; 39 | --cm-def: #eceded; 40 | --cm-comment: #998066; 41 | --cm-string: #bde052; 42 | --cm-operator: #f5b83d; 43 | --cm-number: #d1949e; 44 | --cm-meta: var(--text-color); 45 | --cm-atom: #845dc4; 46 | --cm-builtin: #bde052; 47 | --cm-property: var(--text-color); 48 | --cm-variable-2: var(--text-color); 49 | --cm-variable-3: #bde052; 50 | --cm-gutter: #f1f3f450; 51 | } 52 | 53 | html { 54 | font-size: var(--font-size); 55 | } 56 | 57 | body { 58 | font-family: var(--font-family); 59 | -webkit-font-smoothing: antialiased; 60 | color: var(--text-color); 61 | background-color: var(--main-background-color); 62 | line-height: 1.6; 63 | } 64 | 65 | strong, 66 | em { 67 | color: var(--main-text-color); 68 | } 69 | 70 | /* code */ 71 | code { 72 | color: var(--stress-color); 73 | background-color: transparent; 74 | padding: 0px 2px 0px 2px; 75 | font-family: var(--monospace); 76 | font-size: 85%; 77 | } 78 | 79 | pre code { 80 | color: var(--text-color); 81 | background-color: transparent; 82 | padding: 0px; 83 | } 84 | 85 | pre { 86 | overflow: auto; 87 | line-height: 1.45; 88 | background-color: var(--secondary-background-color); 89 | border-radius: var(--border-radius); 90 | margin-top: 0; 91 | margin-bottom: 16px; 92 | word-wrap: normal; 93 | } 94 | 95 | /* overwrite highlight css */ 96 | .hljs { 97 | padding: 1em; 98 | background-color: var(--secondary-background-color); 99 | } 100 | 101 | /* title */ 102 | h1, 103 | h2, 104 | h3, 105 | h4, 106 | h5, 107 | h6 { 108 | position: relative; 109 | margin-top: 2rem; 110 | margin-bottom: 1rem; 111 | font-weight: 700; 112 | line-height: 1.4; 113 | cursor: text; 114 | } 115 | 116 | h1:before, 117 | h2:before, 118 | h3:before, 119 | h4:before, 120 | h5:before, 121 | h6:before { 122 | color: var(--secondary-text-color); 123 | font-size: 8px; 124 | } 125 | 126 | h1:hover a.anchor, 127 | h2:hover a.anchor, 128 | h3:hover a.anchor, 129 | h4:hover a.anchor, 130 | h5:hover a.anchor, 131 | h6:hover a.anchor { 132 | text-decoration: none; 133 | } 134 | 135 | h1 code, 136 | h2 code, 137 | h3 code, 138 | h4 code, 139 | h5 code, 140 | h6 code { 141 | font-size: inherit; 142 | } 143 | 144 | h1 { 145 | padding-top: 0.1em; 146 | padding-bottom: 0.1em; 147 | font-size: 2.2em; 148 | line-height: 1.3; 149 | color: var(--light-trait-100); 150 | background: linear-gradient(to right, rgb(72, 0, 255), #fa0397); 151 | -webkit-background-clip: text; 152 | -webkit-text-fill-color: transparent; 153 | /* 154 | border-bottom: 1px solid var(--main-color); 155 | border-image: linear-gradient(to right, rgb(72, 0, 255), #d503fa) 1; 156 | */ 157 | margin-bottom: 1em; 158 | } 159 | 160 | h1:before { 161 | content: "h1 "; 162 | } 163 | 164 | h2 { 165 | padding-top: 0.3em; 166 | padding-bottom: 0.3em; 167 | font-size: 1.75em; 168 | line-height: 1.225; 169 | color: var(--light-trait-200); 170 | background: linear-gradient(to right, rgb(132, 10, 239), #c209d7); 171 | -webkit-background-clip: text; 172 | -webkit-text-fill-color: transparent; 173 | } 174 | 175 | h2:before { 176 | content: "h2 "; 177 | } 178 | 179 | h3 { 180 | font-size: 1.4em; 181 | line-height: 1.43; 182 | color: var(--light-trait-300); 183 | } 184 | 185 | h3:before { 186 | content: "h3 "; 187 | } 188 | 189 | h4 { 190 | font-size: 1.2em; 191 | color: var(--light-trait-400); 192 | } 193 | 194 | h4:before { 195 | content: "h4 "; 196 | } 197 | 198 | h5 { 199 | font-size: 1em; 200 | color: var(--light-trait-400); 201 | } 202 | 203 | h5:before { 204 | content: "h5 "; 205 | } 206 | 207 | h6 { 208 | font-size: 1em; 209 | color: var(--light-trait-400); 210 | } 211 | 212 | h6:before { 213 | content: "h6 "; 214 | } 215 | 216 | /* */ 217 | p, 218 | blockquote, 219 | ul, 220 | ol, 221 | dl, 222 | table { 223 | margin: 0.8em 0; 224 | } 225 | 226 | /* list */ 227 | li>ol, 228 | li>ul { 229 | margin: 0 0; 230 | } 231 | 232 | li p.first { 233 | display: inline-block; 234 | } 235 | 236 | ul li::marker, 237 | ol li::marker { 238 | color: var(--main-color); 239 | } 240 | 241 | ul, 242 | ol { 243 | padding-left: 15px; 244 | } 245 | 246 | ul ul { 247 | list-style-type: disc; 248 | } 249 | 250 | ul:first-child, 251 | ol:first-child { 252 | margin-top: 0.35%; 253 | } 254 | 255 | ul:last-child, 256 | ol:last-child { 257 | margin-bottom: 0; 258 | } 259 | 260 | /* dividling */ 261 | hr { 262 | height: 1.5px; 263 | border: none; 264 | background: linear-gradient(to right, rgb(72, 0, 255), #fa0397); 265 | } 266 | 267 | /* link */ 268 | a { 269 | color: var(--url-text-color); 270 | text-decoration: none; 271 | border-bottom: 0.05em solid; 272 | border-color: var(--url-underline-color); 273 | opacity: 0.8; 274 | transition: all .1s ease-in; 275 | } 276 | 277 | a:hover { 278 | text-decoration: none; 279 | opacity: 1; 280 | } 281 | 282 | /* delete line */ 283 | del { 284 | padding: 0; 285 | color: var(--help2-color); 286 | } 287 | 288 | 289 | mark { 290 | border-radius: var(--border-radius); 291 | color: var(--text-highlight-color); 292 | font-weight: inherit; 293 | background-color: var(--text-highlight-bg); 294 | padding-left: 4px; 295 | padding-right: 4px; 296 | padding-top: 2px; 297 | padding-bottom: 2px; 298 | margin-left: 2px; 299 | margin-right: 2px; 300 | } 301 | 302 | blockquote { 303 | color: var(--text-color2); 304 | font-size: 85%; 305 | border-left: 4px solid var(--main-color); 306 | border-radius: var(--border-radius); 307 | border-image: linear-gradient(to bottom, rgb(95, 9, 243), #b901b9) 1; 308 | padding: 5px 15px 5px 20px; 309 | /* change the quote highlight */ 310 | background-color: var(--secondary-background-color); 311 | margin: 0.8em 0; 312 | } 313 | 314 | /* checkbox, todo list */ 315 | input[type=checkbox] { 316 | cursor: pointer; 317 | position: relative; 318 | } 319 | 320 | input[type=checkbox]::after { 321 | position: absolute; 322 | top: 0; 323 | background-color: var(--text-color); 324 | color: var(--text-color); 325 | width: 14px; 326 | height: 14px; 327 | display: inline-block; 328 | visibility: visible; 329 | padding-left: 0px; 330 | text-align: center; 331 | content: ' '; 332 | border-radius: 2px; 333 | box-sizing: border-box; 334 | border: 1px solid var(--main-color); 335 | } 336 | 337 | input[type=checkbox]:checked::after { 338 | content: ""; 339 | background-color: var(--main-color); 340 | border-color: var(--main-color); 341 | background-color: var(--main-color); 342 | } 343 | 344 | input[type=checkbox]:checked::before { 345 | content: ''; 346 | position: absolute; 347 | top: 1px; 348 | left: 5px; 349 | width: 3px; 350 | height: 8px; 351 | border: solid var(--text-color); 352 | border-width: 0 2px 2px 0; 353 | transform: rotate(45deg); 354 | z-index: 1; 355 | } 356 | 357 | ul :has(input[type="checkbox"]) { 358 | list-style: none; 359 | } 360 | 361 | mark { 362 | border-radius: var(--border-radius); 363 | color: var(--text-highlight-color); 364 | font-weight: inherit; 365 | background-color: var(--text-highlight-bg); 366 | padding-left: 4px; 367 | padding-right: 4px; 368 | padding-top: 2px; 369 | padding-bottom: 2px; 370 | margin-left: 2px; 371 | margin-right: 2px; 372 | } 373 | 374 | /* Alternating color rows in table*/ 375 | table tr:nth-child(2n) { 376 | background-color: var(--table-primary-color); 377 | } 378 | 379 | table tr:nth-child(2n + 1) { 380 | background-color: var(--table-secondary-color); 381 | } 382 | 383 | /* Alternating color rows in table*/ 384 | 385 | table { 386 | padding: 0; 387 | word-break: initial; 388 | } 389 | 390 | table tr { 391 | border-top: 1px solid var(--text-accent-color); 392 | margin: 0; 393 | padding: 0; 394 | } 395 | 396 | table tr th { 397 | font-weight: bold; 398 | border: 1px solid var(--text-accent-color); 399 | border-bottom: 0; 400 | margin: 0; 401 | padding: 6px 13px; 402 | } 403 | 404 | table tr td { 405 | border: 1px solid var(--text-accent-color); 406 | margin: 0; 407 | padding: 6px 13px; 408 | } 409 | 410 | table tr th:first-child, 411 | table tr td:first-child { 412 | margin-top: 0; 413 | } 414 | 415 | table tr th:last-child, 416 | table tr td:last-child { 417 | margin-bottom: 0; 418 | } -------------------------------------------------------------------------------- /themes/hard/highlight_code.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", function () { 2 | var codeBlocks = document.querySelectorAll("pre code"); 3 | 4 | for (var i = 0; i < codeBlocks.length; i++) { 5 | hljs.highlightBlock(codeBlocks[i]); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /themes/hard/light.css: -------------------------------------------------------------------------------- 1 | @import "reboto/fonts.css"; 2 | 3 | :root { 4 | /* color define */ 5 | --main-color: #7d3ffa; 6 | --stress-color: #0291aa; 7 | --main-background-color: #f9f9fa; 8 | --secondary-background-color: #f2f2f3; 9 | --help2-color: #F9265F; 10 | 11 | --text-color: #181818; 12 | --text-color2: #616063; 13 | --secondary-text-color: #dadcdd; 14 | 15 | /* Text */ 16 | --light-trait-100: var(--main-color); 17 | --light-trait-200: var(--main-color); 18 | --light-trait-300: var(--main-text-color); 19 | --light-trait-400: var(--main-text-color); 20 | 21 | --url-text-color: var(--stress-color); 22 | --url-underline-color: var(--stress-color); 23 | 24 | /* General */ 25 | --font-size: 16px; 26 | --font-family: "Roboto", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 27 | --monospace: "Zed Mono", "Source Code Pro", "SF Mono Medium", "Fira Code", "Cousine", "Consolas", monospace; 28 | --border-radius: 4px; 29 | 30 | 31 | /* Code variables */ 32 | --cm-line: #b8babb; 33 | --cm-variable: var(--text-color); 34 | --cm-keyword: #d1949e; 35 | --cm-tag: #d1949e; 36 | --cm-bracket: #d1949e; 37 | --cm-error: #ff5a5a; 38 | --cm-attribute: #d1949e; 39 | --cm-def: #eceded; 40 | --cm-comment: #998066; 41 | --cm-string: #bde052; 42 | --cm-operator: #f5b83d; 43 | --cm-number: #d1949e; 44 | --cm-meta: var(--text-color); 45 | --cm-atom: #845dc4; 46 | --cm-builtin: #bde052; 47 | --cm-property: var(--text-color); 48 | --cm-variable-2: var(--text-color); 49 | --cm-variable-3: #bde052; 50 | --cm-gutter: #f1f3f450; 51 | } 52 | 53 | html { 54 | font-size: var(--font-size); 55 | } 56 | 57 | body { 58 | font-family: var(--font-family); 59 | -webkit-font-smoothing: antialiased; 60 | color: var(--text-color); 61 | background-color: var(--main-background-color); 62 | line-height: 1.6; 63 | } 64 | 65 | strong, 66 | em { 67 | color: var(--main-text-color); 68 | } 69 | 70 | /* code */ 71 | code { 72 | color: var(--stress-color); 73 | background-color: transparent; 74 | padding: 0px 2px 0px 2px; 75 | font-family: var(--monospace); 76 | font-size: 85%; 77 | } 78 | 79 | pre code { 80 | color: var(--text-color); 81 | background-color: transparent; 82 | padding: 0px; 83 | } 84 | 85 | pre { 86 | overflow: auto; 87 | line-height: 1.45; 88 | background-image: linear-gradient(to left, #f5dcf5, #e3d9f4); 89 | border-radius: var(--border-radius); 90 | margin-top: 0; 91 | margin-bottom: 16px; 92 | word-wrap: normal; 93 | } 94 | 95 | /* overwrite highlight css */ 96 | .hljs { 97 | padding: 1em; 98 | background-color: transparent; 99 | } 100 | 101 | /* title */ 102 | h1, 103 | h2, 104 | h3, 105 | h4, 106 | h5, 107 | h6 { 108 | position: relative; 109 | margin-top: 2rem; 110 | margin-bottom: 1rem; 111 | font-weight: 700; 112 | line-height: 1.4; 113 | cursor: text; 114 | } 115 | 116 | h1:before, 117 | h2:before, 118 | h3:before, 119 | h4:before, 120 | h5:before, 121 | h6:before { 122 | color: var(--secondary-text-color); 123 | font-size: 8px; 124 | } 125 | 126 | h1:hover a.anchor, 127 | h2:hover a.anchor, 128 | h3:hover a.anchor, 129 | h4:hover a.anchor, 130 | h5:hover a.anchor, 131 | h6:hover a.anchor { 132 | text-decoration: none; 133 | } 134 | 135 | h1 code, 136 | h2 code, 137 | h3 code, 138 | h4 code, 139 | h5 code, 140 | h6 code { 141 | font-size: inherit; 142 | } 143 | 144 | h1 { 145 | padding-top: 0.1em; 146 | padding-bottom: 0.1em; 147 | font-size: 2.2em; 148 | line-height: 1.3; 149 | color: var(--light-trait-100); 150 | background: linear-gradient(to right, #6c05f2, #f30347, #f3b303); 151 | -webkit-background-clip: text; 152 | -webkit-text-fill-color: transparent; 153 | /* 154 | border-bottom: 1px solid var(--main-color); 155 | border-image: linear-gradient(to right, rgb(72, 0, 255), #d503fa) 1; 156 | */ 157 | margin-bottom: 1em; 158 | } 159 | 160 | h1:before { 161 | content: "h1 "; 162 | } 163 | 164 | h2 { 165 | padding-top: 0.3em; 166 | padding-bottom: 0.3em; 167 | font-size: 1.75em; 168 | line-height: 1.225; 169 | color: var(--light-trait-200); 170 | background: linear-gradient(to right, #6c05f2, #f30347, #f3b303); 171 | -webkit-background-clip: text; 172 | -webkit-text-fill-color: transparent; 173 | } 174 | 175 | h2:before { 176 | content: "h2 "; 177 | } 178 | 179 | h3 { 180 | font-size: 1.4em; 181 | line-height: 1.43; 182 | color: var(--light-trait-300); 183 | } 184 | 185 | h3:before { 186 | content: "h3 "; 187 | } 188 | 189 | h4 { 190 | font-size: 1.2em; 191 | color: var(--light-trait-400); 192 | } 193 | 194 | h4:before { 195 | content: "h4 "; 196 | } 197 | 198 | h5 { 199 | font-size: 1em; 200 | color: var(--light-trait-400); 201 | } 202 | 203 | h5:before { 204 | content: "h5 "; 205 | } 206 | 207 | h6 { 208 | font-size: 1em; 209 | color: var(--light-trait-400); 210 | } 211 | 212 | h6:before { 213 | content: "h6 "; 214 | } 215 | 216 | /* */ 217 | p, 218 | blockquote, 219 | ul, 220 | ol, 221 | dl, 222 | table { 223 | margin: 0.8em 0; 224 | } 225 | 226 | /* list */ 227 | li>ol, 228 | li>ul { 229 | margin: 0 0; 230 | } 231 | 232 | li p.first { 233 | display: inline-block; 234 | } 235 | 236 | ul li::marker, 237 | ol li::marker { 238 | color: var(--main-color); 239 | } 240 | 241 | ul, 242 | ol { 243 | padding-left: 15px; 244 | } 245 | 246 | ul ul { 247 | list-style-type: disc; 248 | } 249 | 250 | ul:first-child, 251 | ol:first-child { 252 | margin-top: 0.35%; 253 | } 254 | 255 | ul:last-child, 256 | ol:last-child { 257 | margin-bottom: 0; 258 | } 259 | 260 | /* dividling */ 261 | hr { 262 | height: 1.5px; 263 | border: none; 264 | background: linear-gradient(to right, #6c05f2, #f30347, #f3b303); 265 | } 266 | 267 | /* link */ 268 | a { 269 | color: var(--url-text-color); 270 | text-decoration: none; 271 | border-bottom: 0.05em solid; 272 | border-color: var(--url-underline-color); 273 | opacity: 0.8; 274 | transition: all .1s ease-in; 275 | } 276 | 277 | a:hover { 278 | text-decoration: none; 279 | opacity: 1; 280 | } 281 | 282 | /* delete line */ 283 | del { 284 | padding: 0; 285 | color: var(--help2-color); 286 | } 287 | 288 | 289 | mark { 290 | border-radius: var(--border-radius); 291 | color: var(--text-highlight-color); 292 | font-weight: inherit; 293 | background-color: var(--text-highlight-bg); 294 | padding-left: 4px; 295 | padding-right: 4px; 296 | padding-top: 2px; 297 | padding-bottom: 2px; 298 | margin-left: 2px; 299 | margin-right: 2px; 300 | } 301 | 302 | blockquote { 303 | color: var(--text-color2); 304 | font-size: 85%; 305 | border-left: 4px solid var(--main-color); 306 | border-radius: var(--border-radius); 307 | border-image: linear-gradient(to bottom, #6c05f2, #f303b7) 1; 308 | padding: 5px 15px 5px 20px; 309 | /* change the quote highlight */ 310 | background-color: var(--secondary-background-color); 311 | background-image: linear-gradient(to left, #f5dcf5, #e3d9f4); 312 | margin: 0.8em 0; 313 | } 314 | 315 | /* checkbox, todo list */ 316 | input[type=checkbox] { 317 | cursor: pointer; 318 | position: relative; 319 | } 320 | 321 | input[type=checkbox]::after { 322 | position: absolute; 323 | top: 0; 324 | background-color: var(--main-background-color); 325 | color: var(--main-background-color); 326 | width: 14px; 327 | height: 14px; 328 | display: inline-block; 329 | visibility: visible; 330 | padding-left: 0px; 331 | text-align: center; 332 | content: ' '; 333 | border-radius: 2px; 334 | box-sizing: border-box; 335 | border: 1px solid var(--main-color); 336 | } 337 | 338 | input[type=checkbox]:checked::after { 339 | content: ""; 340 | background-color: var(--main-color); 341 | border-color: var(--main-color); 342 | background-color: var(--main-color); 343 | } 344 | 345 | input[type=checkbox]:checked::before { 346 | content: ''; 347 | position: absolute; 348 | top: 1px; 349 | left: 5px; 350 | width: 3px; 351 | height: 8px; 352 | border: solid var(--main-background-color); 353 | border-width: 0 2px 2px 0; 354 | transform: rotate(45deg); 355 | z-index: 1; 356 | } 357 | 358 | ul :has(input[type="checkbox"]) { 359 | list-style: none; 360 | } 361 | 362 | mark { 363 | border-radius: var(--border-radius); 364 | color: var(--text-highlight-color); 365 | font-weight: inherit; 366 | background-color: var(--text-highlight-bg); 367 | padding-left: 4px; 368 | padding-right: 4px; 369 | padding-top: 2px; 370 | padding-bottom: 2px; 371 | margin-left: 2px; 372 | margin-right: 2px; 373 | } 374 | 375 | /* Alternating color rows in table*/ 376 | table tr:nth-child(2n) { 377 | background-color: var(--table-primary-color); 378 | } 379 | 380 | table tr:nth-child(2n + 1) { 381 | background-color: var(--table-secondary-color); 382 | } 383 | 384 | /* Alternating color rows in table*/ 385 | 386 | table { 387 | padding: 0; 388 | word-break: initial; 389 | } 390 | 391 | table tr { 392 | border-top: 1px solid var(--text-accent-color); 393 | margin: 0; 394 | padding: 0; 395 | } 396 | 397 | table tr th { 398 | font-weight: bold; 399 | border: 1px solid var(--text-accent-color); 400 | border-bottom: 0; 401 | margin: 0; 402 | padding: 6px 13px; 403 | } 404 | 405 | table tr td { 406 | border: 1px solid var(--text-accent-color); 407 | margin: 0; 408 | padding: 6px 13px; 409 | } 410 | 411 | table tr th:first-child, 412 | table tr td:first-child { 413 | margin-top: 0; 414 | } 415 | 416 | table tr th:last-child, 417 | table tr td:last-child { 418 | margin-bottom: 0; 419 | } -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/SourceCodePro-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/hard/reboto/SourceCodePro-Regular.ttf -------------------------------------------------------------------------------- /themes/hard/reboto/fonts.css: -------------------------------------------------------------------------------- 1 | /* roboto-300 - latin */ 2 | @font-face { 3 | font-family: 'Roboto'; 4 | font-style: normal; 5 | font-weight: 300; 6 | src: local('Roboto Light'), local('Roboto-Light'), 7 | url('Roboto-Light.ttf') format('truetype'); 8 | } 9 | /* roboto-300italic - latin */ 10 | @font-face { 11 | font-family: 'Roboto'; 12 | font-style: italic; 13 | font-weight: 300; 14 | src: local('Roboto Light Italic'), local('Roboto-LightItalic'), 15 | url('Roboto-LightItalic.ttf') format('truetype'); 16 | } 17 | /* roboto-regular - latin */ 18 | @font-face { 19 | font-family: 'Roboto'; 20 | font-style: normal; 21 | font-weight: 400; 22 | src: local('Roboto'), local('Roboto-Regular'), 23 | url('Roboto-Regular.ttf') format('truetype'); 24 | } 25 | /* roboto-italic - latin */ 26 | @font-face { 27 | font-family: 'Roboto'; 28 | font-style: italic; 29 | font-weight: 400; 30 | src: local('Roboto Italic'), local('Roboto-Italic'), 31 | url('Roboto-Italic.ttf') format('truetype'); 32 | } 33 | /* roboto-500 - latin */ 34 | @font-face { 35 | font-family: 'Roboto'; 36 | font-style: normal; 37 | font-weight: 500; 38 | src: local('Roboto Medium'), local('Roboto-Medium'), 39 | url('Roboto-Medium.ttf') format('truetype'); 40 | } 41 | /* roboto-500italic - latin */ 42 | @font-face { 43 | font-family: 'Roboto'; 44 | font-style: italic; 45 | font-weight: 500; 46 | src: local('Roboto Medium Italic'), local('Roboto-MediumItalic'), 47 | url('Roboto-MediumItalic.ttf') format('truetype'); 48 | } 49 | /* roboto-700 - latin */ 50 | @font-face { 51 | font-family: 'Roboto'; 52 | font-style: normal; 53 | font-weight: 700; 54 | src: local('Roboto Bold'), local('Roboto-Bold'), 55 | url('Roboto-Bold.ttf') format('truetype'); 56 | } 57 | /* roboto-700italic - latin */ 58 | @font-face { 59 | font-family: 'Roboto'; 60 | font-style: italic; 61 | font-weight: 700; 62 | src: local('Roboto Bold Italic'), local('Roboto-BoldItalic'), 63 | url('Roboto-BoldItalic.ttf') format('truetype'); 64 | } 65 | /* roboto-900 - latin */ 66 | @font-face { 67 | font-family: 'Roboto'; 68 | font-style: normal; 69 | font-weight: 900; 70 | src: local('Roboto Black'), local('Roboto-Black'), 71 | url('Roboto-Black.ttf') format('truetype'); 72 | } 73 | /* roboto-900italic - latin */ 74 | @font-face { 75 | font-family: 'Roboto'; 76 | font-style: italic; 77 | font-weight: 900; 78 | src: local('Roboto Black Italic'), local('Roboto-BlackItalic'), 79 | url('Roboto-BlackItalic.ttf') format('truetype'); 80 | } 81 | /* sorucecodepro-400 - latin */ 82 | @font-face { 83 | font-family: 'Source Code Pro'; 84 | font-style: normal; 85 | font-weight: 400; 86 | src: local('Source Code Pro'), local('SourceCodePro-Regular'), 87 | url('SourceCodePro-Regular.ttf') format('truetype'); 88 | } -------------------------------------------------------------------------------- /themes/hard/reboto/old-slate-colors.css: -------------------------------------------------------------------------------- 1 | /*by h16nning*/ 2 | 3 | :root { 4 | --bg-color: #242932; 5 | --side-bar-bg-color: #29303c; 6 | --control-text-color: #e6ecf1; 7 | --primary-color: rgb(56, 132, 255); 8 | --primary-btn-border-color: #3884ff; 9 | --active-file-bg-color: #5d6574; 10 | --active-file-text-color: inherit; 11 | --active-file-border-color: #3884ff; 12 | --item-hover-text-color: #8092af; 13 | --item-hover-bg-color: #29303c; 14 | --window-border: 1px solid #353c49; 15 | --select-text-font-color: #e6ecf1; 16 | --select-text-bg-color: #3277e5; 17 | 18 | --md-char-color: #8092af; 19 | --heading-char-color: #8092af; 20 | --meta-content-color: #3783ff; 21 | 22 | --borders: #353c49; 23 | --table-border-color: #353c49; 24 | --boxes: #29303c; 25 | --boxes-darker: #424b5a; 26 | --boxes-darker2: #485364; 27 | --boxes-darkest: #5b697e; 28 | --drag-placeholder-color: #424b5a; 29 | 30 | --text-color: #e6ecf1; 31 | --heading-text-color: white; 32 | --light-text-color: #5b697e; 33 | --light-text-color-brighter: #768292; 34 | --codeboxes: #183055; 35 | --codeboxes-lighter: #1c375f; 36 | --rawblock-edit-panel-bd: transparent; 37 | 38 | --primary-color-white: #5272a7; 39 | --primary-color-white-darker: #5a83c5; 40 | --primary-color-darker: #1f65d6; 41 | --focus-ring-color: #3783ff; 42 | 43 | --danger-color: rgb(255, 70, 66); 44 | 45 | --node-fill: #5252ad; 46 | --node-border: #3f3fb8; 47 | --cluster-fill: #ffffde; 48 | --cluster-border: #aaaa33; 49 | --note-fill: #fff5ad; 50 | --note-border: #aaaa33; 51 | 52 | /*****************************/ 53 | 54 | --font-family: "Roboto", sans-serif; 55 | --code-font-family: "Source Code Pro"; 56 | --monospace: "Source Code Pro"; 57 | } 58 | -------------------------------------------------------------------------------- /themes/hard/reboto/slate-colors.css: -------------------------------------------------------------------------------- 1 | /*by h16nning*/ 2 | 3 | :root { 4 | --bg-color: hsl(217, 18%, 9%); 5 | --side-bar-bg-color: hsl(217, 17%, 13%); 6 | --control-text-color: #e6ecf1; 7 | --primary-color: #3884ff; 8 | --primary-btn-border-color: #3884ff; 9 | --active-file-bg-color: hsl(217, 17%, 13%); 10 | --active-file-text-color: inherit; 11 | --active-file-border-color: #3884ff; 12 | --item-hover-text-color: #8092af; 13 | --item-hover-bg-color: #1c2026; 14 | --window-border: 1px solid #272c35; 15 | --select-text-font-color: #e6e7eb; 16 | --select-text-bg-color: #1968e6; 17 | 18 | --md-char-color: #8092af; 19 | --heading-char-color: #8092af; 20 | --meta-content-color: #3783ff; 21 | 22 | --borders: #272c35; 23 | --table-border-color: #272c35; 24 | --boxes: #1c2027; 25 | --boxes-darker: #353b46; 26 | --boxes-darker2: #3e4551; 27 | --boxes-darkest: #4f5764; 28 | --drag-placeholder-color: #3e4551; 29 | 30 | --text-color: #e6e7eb; 31 | --heading-text-color: white; 32 | --light-text-color: #8a92a3; 33 | --light-text-color-brighter: #9aa2b1; 34 | --codeboxes: #183055; 35 | --codeboxes-lighter: #1c375f; 36 | --rawblock-edit-panel-bd: transparent; 37 | 38 | --primary-color-darkest: #001f50; 39 | --primary-color-darker2: #00307c; 40 | --primary-color-darker: #1968e6; 41 | --focus-ring-color: #3783ff; 42 | 43 | --danger-color: rgb(255, 70, 66); 44 | 45 | --node-fill: #5252ad; 46 | --node-border: #3f3fb8; 47 | --cluster-fill: #ffffde; 48 | --cluster-border: #aaaa33; 49 | --note-fill: #fff5ad; 50 | --note-border: #aaaa33; 51 | 52 | /*****************************/ 53 | 54 | --font-family: "Roboto", sans-serif; 55 | --code-font-family: "Source Code Pro"; 56 | --monospace: "Source Code Pro"; 57 | } 58 | -------------------------------------------------------------------------------- /themes/hard/template.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {title} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 23 | 24 | 25 | 26 |
    27 | {{ if use_slice_mode }} 28 | 29 | {{ for slice_content in slices }} 30 |
    31 |
    {slice_header}
    32 |
    {slice_content}
    33 |
    34 | {{ endfor }} 35 | {{ else }} 36 | 37 | { content } 38 | {{ endif }} 39 |
    40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /themes/notion/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "body_min_width": 200, 3 | "body_max_width": 900, 4 | "slice_header": "", 5 | "use_slice_mode": false 6 | } -------------------------------------------------------------------------------- /themes/notion/notion-light-enhanced.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Text editor */ 3 | --bg-color: #ffffff; 4 | --text-color: #37352f; 5 | --body-text-color: #37352f; 6 | --text-cursor-color: var(--text-color); 7 | 8 | --secondary-text-color: #73726e; 9 | --text-accent-color: #e9e9e7; 10 | --bg-color-dark: #f7f6f3; 11 | 12 | --text-highlight-color: #4f3b2a; 13 | --text-highlight-bg: #feecc8; 14 | 15 | --code-fence-text-color: #37352f; 16 | --code-fence-bg-color: #f7f6f3; 17 | 18 | --blockquote-bg-color: #f7f7f7; 19 | --blockquote-accent-color: var(--text-color); 20 | --blockquote-text-color: var(--text-color); 21 | 22 | --light-trait-100: #38352f; 23 | --light-trait-200: #37352f; 24 | --light-trait-300: #37352f; 25 | --light-trait-400: #37352f; 26 | 27 | --todo-bg-color: #2eaadc; 28 | --todo-tick-color: #fff; 29 | 30 | --search-select-bg-color: #edf3ec; 31 | --search-select-text-color: #448361; 32 | 33 | --search-hit-text-color: #d44c47; 34 | --search-hit-bg-color: #fdebec; 35 | 36 | --url-text-color: black; 37 | --url-underline-color: #37352f; 38 | 39 | --footnote-text-color: #f7f6f3; 40 | --footnote-bg-color: #888884; 41 | 42 | --table-primary-color: #f7f7f7; 43 | --table-secondary-color: #fdfdfd; 44 | 45 | --select-text-bg-color: #c0e5f4; 46 | 47 | --code-color: #9a6e3a; 48 | --code-cursor-color: var(--text-color); 49 | 50 | --kbd-text-color: #73726e; 51 | --kbd-bg-color: var(--bg-color-dark); 52 | 53 | /* Menu system */ 54 | --side-bar-bg-color: var(--bg-color-dark); 55 | --side-bar-text-color: var(--text-color); 56 | --item-hover-bg-color: #e8e7e4; 57 | --window-border: 1px solid var(--bg-color); 58 | 59 | --active-file-bg-color: #e9e7e4; 60 | --active-file-border-color: var(--active-file-bg-color); 61 | --active-file-text-color: var(--text-color); 62 | 63 | --footer-bg-color: var(--side-bar-bg-color); 64 | 65 | --control-text-color: #72706b; 66 | 67 | /* General */ 68 | --font-size: 16px; 69 | --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 70 | --monospace: 'SF Mono Medium', 'Fira Code', 'Cousine', 'Consolas', monospace; 71 | --heading-char-color: var(--light-trait-400); 72 | --color-popover-bg-color: var(--text-color); 73 | --rawblock-edit-panel-bd: var(--bg-color-dark); 74 | --meta-content-color: var(--body-text-color); 75 | --primary-btn-border-color: var(--body-text-color); 76 | --border-radius: 4px; 77 | 78 | 79 | /* Code variables */ 80 | --cm-line: #999999; 81 | --cm-variable: #37352f; 82 | --cm-keyword: #0277aa; 83 | --cm-tag: #ff5a5a; 84 | --cm-bracket: #ff5a5a; 85 | --cm-error: #ff5a5a; 86 | --cm-attribute: #0277aa; 87 | --cm-def: #dc4a68; 88 | --cm-comment: #708090; 89 | --cm-string: #669900; 90 | --cm-operator: #9a6e3b; 91 | --cm-number: #990055; 92 | --cm-meta: var(--text-color); 93 | --cm-atom: #845dc4; 94 | --cm-builtin: #669900; 95 | --cm-property: var(--text-color); 96 | --cm-variable-2: var(--text-color); 97 | --cm-variable-3: #0277aa; 98 | --cm-gutter: #f1f3f450; 99 | } 100 | 101 | html { 102 | font-size: var(--font-size); 103 | } 104 | 105 | body { 106 | font-family: var(--font-family); 107 | -webkit-font-smoothing: antialiased; 108 | color: var(--body-text-color); 109 | background-color: var(--bg-color); 110 | line-height: 1.6; 111 | } 112 | 113 | code { 114 | background-color: var(--code-fence-bg-color); 115 | padding: 2px 2px 2px 2px; 116 | } 117 | 118 | #write { 119 | max-width: 860px; 120 | margin: 0 auto; 121 | padding: 30px; 122 | padding-bottom: 100px; 123 | } 124 | 125 | @media only screen and (min-width: 1400px) { 126 | #write { 127 | max-width: 1024px; 128 | } 129 | } 130 | 131 | @media only screen and (min-width: 1800px) { 132 | #write { 133 | max-width: 1200px; 134 | } 135 | } 136 | 137 | #write>ul:first-child, 138 | #write>ol:first-child { 139 | margin-top: 30px; 140 | } 141 | 142 | a { 143 | color: var(--text-color); 144 | } 145 | 146 | h1, 147 | h2, 148 | h3, 149 | h4, 150 | h5, 151 | h6 { 152 | position: relative; 153 | margin-top: 2rem; 154 | margin-bottom: 1rem; 155 | font-weight: 700; 156 | line-height: 1.4; 157 | cursor: text; 158 | } 159 | 160 | cursor { 161 | color: var(--text-cursor-color) 162 | } 163 | 164 | h1:hover a.anchor, 165 | h2:hover a.anchor, 166 | h3:hover a.anchor, 167 | h4:hover a.anchor, 168 | h5:hover a.anchor, 169 | h6:hover a.anchor { 170 | text-decoration: none; 171 | } 172 | 173 | h1 tt, 174 | h1 code, 175 | h2 tt, 176 | h2 code, 177 | h3 tt, 178 | h3 code, 179 | h4 tt, 180 | h4 code, 181 | h5 tt, 182 | h5 code, 183 | h6 tt, 184 | h6 code { 185 | font-size: inherit; 186 | } 187 | 188 | h1 { 189 | padding-bottom: 0.3em; 190 | font-size: 2.2em; 191 | line-height: 1.3; 192 | color: var(--light-trait-100); 193 | } 194 | 195 | h2 { 196 | padding-bottom: 0.3em; 197 | font-size: 1.75em; 198 | line-height: 1.225; 199 | color: var(--light-trait-200); 200 | } 201 | 202 | h3 { 203 | font-size: 1.4em; 204 | line-height: 1.43; 205 | color: var(--light-trait-300); 206 | } 207 | 208 | h4 { 209 | font-size: 1.2em; 210 | color: var(--light-trait-400); 211 | } 212 | 213 | h5 { 214 | font-size: 1em; 215 | color: var(--light-trait-400); 216 | } 217 | 218 | h6 { 219 | font-size: 1em; 220 | color: var(--light-trait-400); 221 | } 222 | 223 | p, 224 | blockquote, 225 | ul, 226 | ol, 227 | dl, 228 | table { 229 | margin: 0.8em 0; 230 | } 231 | 232 | li>ol, 233 | li>ul { 234 | margin: 0 0; 235 | } 236 | 237 | hr { 238 | background-color: var(--light-trait-100); 239 | height: 1.5px; 240 | border: none 241 | } 242 | 243 | a, 244 | .md-def-url { 245 | color: var(--url-text-color); 246 | text-decoration: none; 247 | border-bottom: 0.05em solid; 248 | border-color: var(--url-underline-color); 249 | opacity: 0.6; 250 | transition: all .1s ease-in; 251 | } 252 | 253 | a:hover { 254 | text-decoration: none; 255 | opacity: 1; 256 | } 257 | 258 | sup.md-footnote { 259 | background-color: var(--footnote-text-color); 260 | color: var(--footnote-bg-color); 261 | } 262 | 263 | li p.first { 264 | display: inline-block; 265 | } 266 | 267 | ul, 268 | ol { 269 | padding-left: 30px; 270 | } 271 | 272 | ul:first-child, 273 | ol:first-child { 274 | margin-top: 0.35%; 275 | } 276 | 277 | ul:last-child, 278 | ol:last-child { 279 | margin-bottom: 0; 280 | } 281 | 282 | mark, 283 | .ty-file-search-match-text, 284 | .md-search-hit { 285 | background: var(--search-hit-bg-color); 286 | color: var(--search-hit-text-color); 287 | } 288 | 289 | mark { 290 | border-radius: 4px; 291 | color: var(--text-highlight-color); 292 | font-weight: inherit; 293 | background-color: var(--text-highlight-bg); 294 | padding-left: 4px; 295 | padding-right: 4px; 296 | padding-top: 2px; 297 | padding-bottom: 2px; 298 | margin-left: 2px; 299 | margin-right: 2px; 300 | } 301 | 302 | .md-search-hit * { 303 | background: var(--search-select-bg-color); 304 | color: var(--search-select-text-color); 305 | } 306 | 307 | /* Search highlight */ 308 | .cm-search-hit.CodeMirror-selectedtext, 309 | .md-search-hit.md-search-select, 310 | .md-search-select { 311 | outline: 0px solid var(--search-select-text-color); 312 | } 313 | 314 | .outline-item.select, 315 | .ty-search-item-line.select, 316 | .ty-search-item.select { 317 | outline-width: 2px; 318 | } 319 | 320 | .outline-item.select { 321 | outline-offset: 0px; 322 | } 323 | 324 | blockquote { 325 | color: var(--blockquote-text-color); 326 | margin-left: 1.75px; 327 | margin-right: 0px; 328 | border-left: 4px solid var(--blockquote-accent-color); 329 | padding: 10px 14px 7px 22px; 330 | /* change the quote highlight */ 331 | background-color: var(--blockquote-bg-color); 332 | } 333 | 334 | blockquote blockquote { 335 | padding-right: 0; 336 | } 337 | 338 | /* Alternating color rows in table*/ 339 | table tr:nth-child(2n) { 340 | background-color: var(--table-primary-color); 341 | } 342 | 343 | table tr:nth-child(2n + 1) { 344 | background-color: var(--table-secondary-color); 345 | } 346 | 347 | /* Alternating color rows in table*/ 348 | 349 | table { 350 | padding: 0; 351 | word-break: initial; 352 | } 353 | 354 | table tr { 355 | border-top: 1px solid var(--text-accent-color); 356 | margin: 0; 357 | padding: 0; 358 | } 359 | 360 | table tr th { 361 | font-weight: bold; 362 | border: 1px solid var(--text-accent-color); 363 | border-bottom: 0; 364 | margin: 0; 365 | padding: 6px 13px; 366 | } 367 | 368 | table tr td { 369 | border: 1px solid var(--text-accent-color); 370 | margin: 0; 371 | padding: 6px 13px; 372 | } 373 | 374 | table tr th:first-child, 375 | table tr td:first-child { 376 | margin-top: 0; 377 | } 378 | 379 | table tr th:last-child, 380 | table tr td:last-child { 381 | margin-bottom: 0; 382 | } 383 | 384 | pre { 385 | padding: 16px; 386 | overflow: auto; 387 | font-size: 85%; 388 | line-height: 1.45; 389 | background-color: var(--code-fence-bg-color); 390 | border-radius: 6px; 391 | margin-top: 0; 392 | margin-bottom: 16px; 393 | font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, 394 | Liberation Mono, monospace; 395 | word-wrap: normal; 396 | } 397 | 398 | pre code { 399 | background-color: transparent; 400 | } 401 | 402 | kbd { 403 | font-size: 0.875rem; 404 | background: var(--kbd-bg-color); 405 | border: 1px solid var(--text-accent-color); 406 | box-shadow: 0 2px 0 var(--text-accent-color); 407 | color: var(--kbd-text-color); 408 | } 409 | 410 | .md-fences, 411 | code, 412 | tt { 413 | border: none; 414 | color: var(--code-fence-text-color); 415 | background-color: var(--code-fence-bg-color); 416 | border-radius: var(--border-radius); 417 | padding: 2px 4px 0px 4px; 418 | font-size: 0.975em; 419 | font-weight: medium; 420 | font-family: var(--monospace) 421 | } 422 | 423 | .md-fences { 424 | margin-bottom: 15px; 425 | margin-top: 15px; 426 | padding-top: 8px; 427 | padding-bottom: 6px; 428 | } 429 | 430 | #write pre.md-meta-block { 431 | padding: 1rem; 432 | font-size: 85%; 433 | line-height: 1.45; 434 | background-color: var(--bg-color-dark); 435 | border: 0; 436 | border-radius: var(--border-radius); 437 | color: var(--secondary-text-color); 438 | margin-top: 0 !important; 439 | } 440 | 441 | #write .mathjax-block .md-rawblock-tooltip { 442 | border-top-left-radius: var(--border-radius); 443 | border-top-right-radius: var(--border-radius); 444 | } 445 | 446 | 447 | #write .mathjax-block .md-math-container { 448 | border-top-left-radius: var(--border-radius); 449 | border-bottom-right-radius: var(--border-radius); 450 | border-bottom-left-radius: var(--border-radius); 451 | } 452 | 453 | #write .md-mathblock-panel .md-rawblock-control:first-of-type { 454 | border-top-left-radius: var(--border-radius); 455 | } 456 | 457 | .md-mathjax-midline { 458 | background-color: var(--bg-color); 459 | color: var(--text-color); 460 | } 461 | 462 | .md-inline-math script { 463 | color: var(--code-color); 464 | } 465 | 466 | .CodeMirror-lines { 467 | padding-left: 4px; 468 | } 469 | 470 | .code-tooltip { 471 | box-shadow: none; 472 | border-radius: var(--border-radius); 473 | } 474 | 475 | #write .code-tooltip { 476 | bottom: initial; 477 | top: 100%; 478 | left: initial; 479 | right: -1px; 480 | background: var(--bg-color-dark); 481 | border: 1px solid var(--text-accent-color); 482 | border-top-left-radius: 0; 483 | border-top-right-radius: 0; 484 | border-top: 0; 485 | font-family: var(--monospace); 486 | } 487 | 488 | #write .md-mathblock-panel .code-tooltip { 489 | right: 0; 490 | border: none; 491 | } 492 | 493 | /* TODO */ 494 | #write .md-task-list-item>input { 495 | -webkit-appearance: initial; 496 | display: inline-block; 497 | text-align: center; 498 | vertical-align: middle; 499 | position: absolute; 500 | border: 1px solid var(--text-color); 501 | margin-top: 0.3rem; 502 | margin-left: -1.45rem; 503 | height: 0.95rem; 504 | width: 0.95rem; 505 | transition: all 0.35s; 506 | } 507 | 508 | #write .md-task-list-item>input:focus { 509 | outline: none; 510 | box-shadow: none; 511 | } 512 | 513 | #write .md-task-list-item>input[checked] { 514 | background: var(--todo-bg-color); 515 | border: 1px solid var(--todo-bg-color); 516 | text-decoration: line-through; 517 | } 518 | 519 | #write .md-task-list-item>input[checked]::before { 520 | display: flex; 521 | align-items: center; 522 | justify-content: center; 523 | content: '✓'; 524 | position: absolute; 525 | margin-top: 0.05rem; 526 | top: 0; 527 | left: 0; 528 | height: 100%; 529 | width: 100%; 530 | color: var(--todo-tick-color); 531 | font-size: 0.75em; 532 | font-weight: 600; 533 | } 534 | 535 | #write .md-task-list-item>input[checked]::after { 536 | text-decoration: line-through; 537 | } 538 | 539 | /* TODO */ 540 | .md-image>.md-meta { 541 | border-radius: var(--border-radius); 542 | padding: 2px 0px 0px 4px; 543 | font-size: 0.9em; 544 | color: inherit; 545 | } 546 | 547 | .md-toc { 548 | margin-top: 20px; 549 | padding-bottom: 20px; 550 | } 551 | 552 | /* Source mode */ 553 | .CodeMirror.cm-s-typora-default *, 554 | .cm-s-typora-default * { 555 | background: inherit; 556 | color: var(--body-text-color); 557 | font-family: var(--monospace); 558 | font-size: var(--font-size) !important; 559 | font-style: normal; 560 | font-weight: medium; 561 | } 562 | 563 | .CodeMirror.cm-s-typora-default div.CodeMirror-cursor { 564 | border-left: 2.75px solid var(--code-cursor-color); 565 | } 566 | 567 | .CodeMirror div.CodeMirror-cursor { 568 | border-left: 2.75px solid var(--code-cursor-color); 569 | } 570 | 571 | 572 | .sidebar-tabs { 573 | border-bottom: none; 574 | } 575 | 576 | .outline-expander { 577 | width: 1.5rem; 578 | vertical-align: initial; 579 | } 580 | 581 | .outline-expander:before, 582 | .outline-expander:hover:before, 583 | .outline-item-open>.outline-item>.outline-expander:before, 584 | .outline-item-open>.outline-item>.outline-expander:hover:before { 585 | content: "\f125"; 586 | transition: transform 125ms ease-in-out; 587 | } 588 | 589 | .outline-item-open>.outline-item>.outline-expander:hover:before, 590 | .outline-item-open>.outline-item>.outline-expander:before { 591 | transform: rotate(90deg); 592 | } 593 | 594 | .outline-label:hover { 595 | text-decoration: none; 596 | } 597 | 598 | #toc-dropmenu { 599 | background: var(--bg-color-dark); 600 | } 601 | 602 | #toc-dropmenu .outline-title { 603 | font-size: 1rem; 604 | text-transform: uppercase; 605 | } 606 | 607 | .dropdown-menu .divider { 608 | display: none; 609 | } 610 | 611 | .context-menu { 612 | border: none !important; 613 | backdrop-filter: saturate(180%) blur(20px) brightness(1.1); 614 | background-color: var(--bg-color-dark); 615 | box-shadow: 0 25.6px 57.6px 0 rgba(0, 0, 0, .22), 0 4.8px 14.4px 0 rgba(0, 0, 0, .18) !important; 616 | } 617 | 618 | .file-node-background { 619 | height: 31px; 620 | } 621 | 622 | .file-node-content:hover .file-node-icon, 623 | .file-node-content:hover .file-node-open-state { 624 | visibility: visible; 625 | } 626 | 627 | .file-node-icon { 628 | margin-right: 8px; 629 | } 630 | 631 | .file-library-node:not(.file-node-root):focus>.file-node-content { 632 | outline: none; 633 | } 634 | 635 | /* New file animation */ 636 | .blink-area { 637 | animation: none; 638 | } 639 | 640 | .file-list-item-summary { 641 | font-size: var(--font-size); 642 | font-family: var(--font-family); 643 | } 644 | 645 | #md-searchpanel input { 646 | border-radius: var(--border-radius); 647 | box-shadow: none; 648 | } 649 | 650 | #md-searchpanel input:focus { 651 | box-shadow: none; 652 | border-color: var(--meta-content-color); 653 | } 654 | 655 | #md-searchpanel .search-type-selection { 656 | width: auto; 657 | } 658 | 659 | #md-searchpanel .btn:not(.close-btn):hover { 660 | box-shadow: none; 661 | } 662 | 663 | .mac-seamless-mode #typora-sidebar { 664 | color: var(--sidebar-text-color); 665 | background-color: var(--side-bar-bg-color); 666 | } 667 | 668 | #md-notification .btn { 669 | border: 0; 670 | } 671 | 672 | /* CODE HIGHLIGHT */ 673 | pre.CodeMirror-line { 674 | color: var(--cm-line) !important 675 | } 676 | 677 | .cm-variable { 678 | color: var(--cm-variable) !important 679 | } 680 | 681 | .cm-keyword { 682 | color: var(--cm-keyword) !important 683 | } 684 | 685 | .cm-tag { 686 | color: var(--cm-tag) !important 687 | } 688 | 689 | .cm-variable-3 { 690 | color: var(--cm-variable-3) !important 691 | } 692 | 693 | .cm-bracket { 694 | color: var(--cm-bracket); 695 | } 696 | 697 | .cm-error { 698 | color: var(--cm-error) !important; 699 | } 700 | 701 | .cm-attribute { 702 | color: var(--cm-attribute) !important 703 | } 704 | 705 | .cm-def { 706 | color: var(--cm-def) !important 707 | } 708 | 709 | .cm-comment { 710 | color: var(--cm-comment) !important 711 | } 712 | 713 | .cm-string { 714 | color: var(--cm-string) !important; 715 | font-variant-ligatures: common-ligatures !important; 716 | } 717 | 718 | .cm-tag:not() { 719 | font-weight: 700; 720 | } 721 | 722 | .cm-operator { 723 | color: var(--cm-operator) !important 724 | } 725 | 726 | .cm-number { 727 | color: var(--cm-number) !important 728 | } 729 | 730 | .cm-meta { 731 | color: var(--cm-meta) !important; 732 | font-weight: 700 !important; 733 | } 734 | 735 | .cm-atom { 736 | color: var(--cm-atom) !important 737 | } 738 | 739 | .cm-builtin { 740 | color: var(--cm-builtin) !important 741 | } 742 | 743 | .cm-property { 744 | color: var(--cm-property) !important 745 | } 746 | 747 | .cm-variable-2 { 748 | color: var(--cm-variable-2) !important 749 | } 750 | 751 | 752 | /* CODE HIGHLIGHT */ 753 | 754 | .file-tree-node.active>.file-node-background { 755 | color: var(--active-file-text-color); 756 | background-color: var(--active-file-bg-color); 757 | border-left: 0px solid var(--active-file-border-color) !important; 758 | border-color: var(--active-file-border-color) !important; 759 | } 760 | 761 | .CodeMirror-gutters { 762 | border-right: 1px solid var(--cm-gutter); 763 | background: inherit; 764 | white-space: nowrap; 765 | } 766 | 767 | .ty-footer, 768 | .sidebar-footer, 769 | footer { 770 | backdrop-filter: saturate(120%) blur(20px) brightness(0.85); 771 | border: none !important; 772 | background: none; 773 | background-color: var(--footer-bg-color); 774 | } 775 | 776 | .code-tooltip { 777 | border-radius: 4px; 778 | background-color: var(--dark-trait); 779 | box-shadow: 0 25.6px 57.6px 0 rgba(0, 0, 0, .52), 0 4.8px 14.4px 0 rgba(0, 0, 0, .28) !important; 780 | } 781 | 782 | #sidebar-files-menu { 783 | color: var(--side-bar-text-color); 784 | border-radius: 4px; 785 | border: none !important; 786 | box-shadow: 0 25.6px 57.6px 0 #f7f6f3, 0 4.8px 14.4px 0 rgba(0, 0, 0, .28); 787 | } 788 | 789 | .code-tooltip.md-tooltip-hide.md-hover-tip { 790 | box-shadow: 0 25.6px 57.6px 0 rgba(0, 0, 0, .52), 0 4.8px 14.4px 0 rgba(0, 0, 0, .28); 791 | } 792 | 793 | #typora-sidebar { 794 | border: none !important; 795 | } 796 | 797 | #footer-word-count-info, 798 | #spell-check-panel { 799 | border: none !important; 800 | backdrop-filter: saturate(120%) blur(20px) brightness(0.85) !important; 801 | box-shadow: 0 25.6px 57.6px 0 rgba(0, 0, 0, .32), 0 4.8px 14.4px 0 rgba(0, 0, 0, .28) !important; 802 | } 803 | 804 | /* Windows/Linux unibody mode */ 805 | .megamenu-content, 806 | .megamenu-opened header { 807 | color: var(--body-text-color); 808 | background: var(--bg-color-dark); 809 | } 810 | 811 | #recent-file-panel-action-btn { 812 | background: inherit; 813 | border: 1px var(--light-trait-300) solid; 814 | } 815 | 816 | .megamenu-menu-panel table td:nth-child(1) { 817 | color: var(--text-color); 818 | background: var(--bg-color-dark); 819 | } 820 | 821 | .megamenu-menu-panel table td:nth-child(2) { 822 | color: var(--text-color); 823 | background: var(--bg-color-dark); 824 | } 825 | 826 | .megamenu-menu-panel tbody tr:hover td:nth-child(1) { 827 | color: var(--text-color); 828 | background: var(--active-file-bg-color); 829 | } 830 | 831 | .megamenu-menu-panel tbody tr:hover td:nth-child(2) { 832 | color: var(--text-color); 833 | background: var(--active-file-bg-color); 834 | } 835 | 836 | .megamenu-menu-panel input[type='text'] { 837 | background: inherit; 838 | border: 1px var(--text-color-secondary) solid; 839 | } 840 | 841 | #recent-file-panel-action-btn { 842 | background: inherit; 843 | } 844 | 845 | .megamenu-menu, 846 | .megamenu-content { 847 | background: var(--side-bar-bg-color); 848 | color: var(--text-color); 849 | } 850 | 851 | #top-titlebar, 852 | #top-titlebar * { 853 | background: inherit; 854 | color: var(--text-color); 855 | } 856 | 857 | .megamenu-menu-header #megamenu-menu-header-title:before { 858 | color: var(--text-color); 859 | } 860 | 861 | megamenu-back-btn { 862 | color: var(--text-color); 863 | border-color: var(--text-color); 864 | } 865 | 866 | .megamenu-menu-header #megamenu-menu-header-title, 867 | .megamenu-menu-header:hover, 868 | .megamenu-menu-header:focus { 869 | color: inherit; 870 | } 871 | 872 | .megamenu-menu-panel table tr:nth-child(2n + 1) { 873 | background-color: var(--side-bar-bg-color); 874 | } -------------------------------------------------------------------------------- /themes/notion/template.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {title} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 |
    24 | {{ if use_slice_mode }} 25 | 26 | {{ for slice_content in slices }} 27 |
    28 |
    {slice_header}
    29 |
    {slice_content}
    30 |
    31 | {{ endfor }} 32 | {{ else }} 33 | 34 | { content } 35 | {{ endif }} 36 |
    37 | 38 | 39 | -------------------------------------------------------------------------------- /themes/xhs/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "body_min_width": 0, 3 | "body_max_width": 1500, 4 | "slice_header": "
    超级个体搞钱故事
    ", 5 | "use_slice_mode": true 6 | } 7 | -------------------------------------------------------------------------------- /themes/xhs/dark.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Text editor */ 3 | --bg-color: #2D2D2C; 4 | --slice-bg-color: rgb(32, 29, 41); 5 | --text-color: #ffffff; 6 | --body-text-color: #ffffff; 7 | --text-cursor-color: var(--text-color); 8 | 9 | --secondary-text-color: #73726e; 10 | --text-accent-color: #393c3f; 11 | --bg-color-dark: #3f4447; 12 | 13 | --some-add-thing: #123123; 14 | --some-add-thing: #123123; 15 | --some-add-thing: #123123; 16 | --some-add-thing: #123123; 17 | --some-add-thing: #123123; 18 | --some-add-thing: #123123; 19 | 20 | --text-highlight-color: #fff; 21 | --text-highlight-bg: #4f4029; 22 | 23 | --code-fence-text-color: #ecebeb; 24 | --code-fence-bg-color: #3f4447; 25 | 26 | --blockquote-bg-color: #3c4144; 27 | --blockquote-accent-color: #DC75FF; 28 | --blockquote-text-color: #CBCBCB; 29 | 30 | --todo-bg-color: #2eaadc; 31 | --todo-tick-color: #fff; 32 | 33 | --search-select-bg-color: #2e443a; 34 | --search-select-text-color: #eaedec; 35 | 36 | --search-hit-text-color: #eceded; 37 | --search-hit-bg-color: #5e3436; 38 | 39 | --url-text-color: var(--text-color); 40 | --url-underline-color: var(--text-color); 41 | 42 | --footnote-text-color: #b1b3b4; 43 | --footnote-bg-color: #393c3f; 44 | 45 | --table-primary-color: var(--bg-color); 46 | --table-secondary-color: var(--bg-color); 47 | 48 | --select-text-bg-color: #2e5767; 49 | 50 | --code-color: #DC75FF; 51 | --code-cursor-color: var(--text-color); 52 | 53 | --kbd-text-color: #acaeaf; 54 | --kbd-bg-color: var(--bg-color-dark); 55 | 56 | /* Menu system */ 57 | --side-bar-bg-color: var(--bg-color-dark); 58 | --side-bar-text-color: var(--text-color); 59 | --item-hover-bg-color: #4b5053; 60 | --window-border: 1px solid var(--bg-color); 61 | 62 | --active-file-bg-color: #4b5053; 63 | --active-file-border-color: var(--active-file-bg-color); 64 | --active-file-text-color: var(--text-color); 65 | 66 | --footer-bg-color: var(--side-bar-bg-color); 67 | 68 | --control-text-color: #afb1b2; 69 | 70 | /* General */ 71 | --font-size: 18px; 72 | /* --font-family: "LXGW WenKai Mono", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 73 | --monospace: 'LXGW WenKai Mono', 'SF Mono Medium', 'Fira Code', 'Cousine', 'Consolas', monospace; */ 74 | --font-family: BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 75 | --monospace: BlinkMacSystemFont, 'SF Mono Medium', 'Fira Code', 'Cousine', 'Consolas', monospace; 76 | --heading-char-color: var(--light-trait-400); 77 | --color-popover-bg-color: var(--text-color); 78 | --rawblock-edit-panel-bd: var(--bg-color-dark); 79 | --meta-content-color: var(--body-text-color); 80 | --primary-btn-border-color: var(--body-text-color); 81 | --border-radius: 4px; 82 | 83 | 84 | /* Code variables */ 85 | --cm-line: #b8babb; 86 | --cm-variable: var(--text-color); 87 | --cm-keyword: #d1949e; 88 | --cm-tag: #d1949e; 89 | --cm-bracket: #d1949e; 90 | --cm-error: #ff5a5a; 91 | --cm-attribute: #d1949e; 92 | --cm-def: #eceded; 93 | --cm-comment: #998066; 94 | --cm-string: #bde052; 95 | --cm-operator: #f5b83d; 96 | --cm-number: #d1949e; 97 | --cm-meta: var(--text-color); 98 | --cm-atom: #845dc4; 99 | --cm-builtin: #bde052; 100 | --cm-property: var(--text-color); 101 | --cm-variable-2: var(--text-color); 102 | --cm-variable-3: #bde052; 103 | --cm-gutter: #f1f3f450; 104 | } 105 | 106 | .markdown-body { 107 | display: flex; 108 | flex-direction: row; 109 | flex-wrap: wrap; 110 | } 111 | 112 | .slice-div { 113 | width: 416px; 114 | height: 568px; 115 | background-color: var(--slice-bg-color); 116 | /* background-image: url("/static/xhs/slice_bg.png"); */ 117 | background-size: 100% 100%; 118 | margin: 0 7px 26px 7px; 119 | padding: 20px 20px; 120 | position: relative; 121 | text-shadow: 0 0 2px black; 122 | } 123 | 124 | .slice-div:before { 125 | content: ""; 126 | position: absolute; 127 | top: 0; 128 | left: 0; 129 | z-index: 0; 130 | width: 100%; 131 | height: 100%; 132 | background-image: linear-gradient(to right bottom, rgb(88, 28, 135) 0%, rgba(88, 28, 135, 0) 40%), linear-gradient(to left bottom, rgb(30, 58, 138) 0%, rgba(30, 58, 138, 0) 40%); 133 | opacity: 0.3; 134 | } 135 | 136 | .slice-div:first-child .slice-content { 137 | font-size: 22px; 138 | } 139 | 140 | .slice-div:first-child::after { 141 | content: "右滑查看详情 >>>"; 142 | position: absolute; 143 | z-index: 2; 144 | color: var(--text-color); 145 | font-size: 20px; 146 | right: 30px; 147 | bottom: 20px; 148 | } 149 | 150 | .slice-div:first-child ul { 151 | margin-top: 30px; 152 | font-weight: bold; 153 | } 154 | 155 | .slice-background { 156 | width: 100%; 157 | height: 100%; 158 | position: absolute; 159 | top: 0; 160 | left: 0; 161 | z-index: 1; 162 | } 163 | 164 | .slice-header { 165 | display: flex; 166 | flex-direction: row; 167 | justify-content: space-between; 168 | align-items: flex-end; 169 | position: relative; 170 | z-index: 2; 171 | } 172 | .slice-content { 173 | position: relative; 174 | z-index: 2; 175 | } 176 | 177 | .slice-header-title { 178 | display: flex; 179 | width: 360px; 180 | height: 40px; 181 | flex-flow: column; 182 | justify-content: space-between; 183 | } 184 | 185 | .slice-header-title > span { 186 | line-height: 1.2; 187 | font-size: 22px; 188 | } 189 | 190 | .slice-header-decorate { 191 | height: 6px; 192 | background: linear-gradient(to right, #D574D9, #AB3FAC); 193 | } 194 | 195 | html { 196 | font-size: var(--font-size); 197 | } 198 | 199 | body { 200 | font-family: var(--font-family); 201 | -webkit-font-smoothing: antialiased; 202 | color: var(--body-text-color); 203 | background-color: var(--bg-color); 204 | line-height: 1.6; 205 | } 206 | 207 | pre { 208 | padding: 16px; 209 | overflow: auto; 210 | font-size: 85%; 211 | line-height: 1.45; 212 | background-color: var(--code-fence-bg-color); 213 | border-radius: 6px; 214 | margin-top: 0; 215 | margin-bottom: 16px; 216 | font-family: var(--monospace); 217 | word-wrap: normal; 218 | } 219 | 220 | code { 221 | color: var(--code-color); 222 | font-family: var(--monospace); 223 | } 224 | 225 | 226 | pre code { 227 | background-color: transparent; 228 | } 229 | 230 | 231 | a { 232 | color: var(--text-color); 233 | } 234 | 235 | h1, 236 | h2, 237 | h3, 238 | h4, 239 | h5, 240 | h6 { 241 | position: relative; 242 | margin-top: 1.5rem; 243 | margin-bottom: 1rem; 244 | font-weight: 700; 245 | line-height: 1.4; 246 | cursor: text; 247 | } 248 | 249 | cursor { 250 | color: var(--text-cursor-color) 251 | } 252 | 253 | h1:hover a.anchor, 254 | h2:hover a.anchor, 255 | h3:hover a.anchor, 256 | h4:hover a.anchor, 257 | h5:hover a.anchor, 258 | h6:hover a.anchor { 259 | text-decoration: none; 260 | } 261 | 262 | h1 code, 263 | h2 code, 264 | h3 code, 265 | h4 code, 266 | h5 code, 267 | h6 code { 268 | color: inherit; 269 | font-size: inherit; 270 | } 271 | 272 | h1, h2, h3, h4 { 273 | font-size: 1.2em; 274 | color: var(--code-color); 275 | } 276 | 277 | h1::before, 278 | h2::before, 279 | h3::before, 280 | h4::before { 281 | content: "#"; 282 | padding-right: 8px; 283 | } 284 | 285 | h5 { 286 | font-size: 1em; 287 | color: var(--light-trait-400); 288 | } 289 | 290 | h6 { 291 | font-size: 1em; 292 | color: var(--light-trait-400); 293 | } 294 | 295 | p, 296 | blockquote, 297 | ul, 298 | ol, 299 | dl, 300 | table { 301 | margin: 0.8em 0; 302 | } 303 | 304 | li>ol, 305 | li>ul { 306 | margin: 0 0; 307 | } 308 | 309 | ul>li { 310 | margin-top: 10px; 311 | } 312 | 313 | hr { 314 | background-color: var(--light-trait-100); 315 | height: 1.5px; 316 | border: none 317 | } 318 | 319 | a { 320 | color: var(--url-text-color); 321 | text-decoration: none; 322 | transition: all .1s ease-in; 323 | } 324 | 325 | a:hover { 326 | text-decoration: none; 327 | opacity: 1; 328 | } 329 | 330 | li p.first { 331 | display: inline-block; 332 | } 333 | 334 | ul { 335 | padding-left: 16px; 336 | margin-top: 14px; 337 | } 338 | 339 | ol { 340 | padding-left: 30px; 341 | margin-top: 14px; 342 | } 343 | 344 | ul::marker, ol::marker { 345 | color: var(--code-color); 346 | } 347 | 348 | ul:last-child, 349 | ol:last-child { 350 | margin-bottom: 0; 351 | } 352 | 353 | mark { 354 | border-radius: 4px; 355 | color: var(--text-highlight-color); 356 | font-weight: inherit; 357 | background-color: var(--text-highlight-bg); 358 | padding-left: 4px; 359 | padding-right: 4px; 360 | padding-top: 2px; 361 | padding-bottom: 2px; 362 | margin-left: 2px; 363 | margin-right: 2px; 364 | } 365 | 366 | blockquote { 367 | color: var(--blockquote-text-color); 368 | margin: 30px 0 0 0; 369 | /* border-left: 12px solid var(--blockquote-accent-color); */ 370 | padding: 10px; 371 | line-height: 1.2; 372 | background-color: rgba(255, 255, 255, 0.2); 373 | border-radius: 5px; 374 | border: 1px solid rgba(255, 255, 255, 0.1); 375 | box-shadow: 0 0 4px black; 376 | } 377 | 378 | blockquote p { 379 | margin: 5px 0; 380 | } 381 | 382 | blockquote::before { 383 | display: block; 384 | content: "🤔"; 385 | color: var(--code-color); 386 | } 387 | 388 | blockquote blockquote { 389 | padding-right: 0; 390 | } 391 | 392 | /* Alternating color rows in table*/ 393 | table tr:nth-child(2n) { 394 | background-color: var(--table-primary-color); 395 | } 396 | 397 | table tr:nth-child(2n + 1) { 398 | background-color: var(--table-secondary-color); 399 | } 400 | 401 | /* Alternating color rows in table*/ 402 | 403 | table { 404 | padding: 0; 405 | word-break: initial; 406 | } 407 | 408 | table tr { 409 | border-top: 1px solid var(--text-accent-color); 410 | margin: 0; 411 | padding: 0; 412 | } 413 | 414 | table tr th { 415 | font-weight: bold; 416 | border: 1px solid var(--text-accent-color); 417 | border-bottom: 0; 418 | margin: 0; 419 | padding: 6px 13px; 420 | } 421 | 422 | table tr td { 423 | border: 1px solid var(--text-accent-color); 424 | margin: 0; 425 | padding: 6px 13px; 426 | } 427 | 428 | table tr th:first-child, 429 | table tr td:first-child { 430 | margin-top: 0; 431 | } 432 | 433 | table tr th:last-child, 434 | table tr td:last-child { 435 | margin-bottom: 0; 436 | } 437 | -------------------------------------------------------------------------------- /themes/xhs/light.css: -------------------------------------------------------------------------------- 1 | :root { 2 | /* Text editor */ 3 | --bg-color: #f9fafb; 4 | --slice-bg-color: #f9fafb; 5 | --text-color: #111827; 6 | --body-text-color: #111827; 7 | --text-cursor-color: var(--text-color); 8 | 9 | --secondary-text-color: #73726e; 10 | --text-accent-color: #393c3f; 11 | --bg-color-dark: #3f4447; 12 | 13 | --some-add-thing: #123123; 14 | --some-add-thing: #123123; 15 | --some-add-thing: #123123; 16 | --some-add-thing: #123123; 17 | --some-add-thing: #123123; 18 | --some-add-thing: #123123; 19 | 20 | --text-highlight-color: #fff; 21 | --text-highlight-bg: #4f4029; 22 | 23 | --code-fence-text-color: #ecebeb; 24 | --code-fence-bg-color: #3f4447; 25 | 26 | --blockquote-bg-color: #3c4144; 27 | --blockquote-accent-color: #b45309; 28 | --blockquote-text-color: #CBCBCB; 29 | 30 | --todo-bg-color: #2eaadc; 31 | --todo-tick-color: #fff; 32 | 33 | --search-select-bg-color: #2e443a; 34 | --search-select-text-color: #eaedec; 35 | 36 | --search-hit-text-color: #eceded; 37 | --search-hit-bg-color: #5e3436; 38 | 39 | --url-text-color: var(--text-color); 40 | --url-underline-color: var(--text-color); 41 | 42 | --footnote-text-color: #b1b3b4; 43 | --footnote-bg-color: #393c3f; 44 | 45 | --table-primary-color: var(--bg-color); 46 | --table-secondary-color: var(--bg-color); 47 | 48 | --select-text-bg-color: #2e5767; 49 | 50 | --code-color: #b45309; 51 | --code-cursor-color: var(--text-color); 52 | 53 | --kbd-text-color: #acaeaf; 54 | --kbd-bg-color: var(--bg-color-dark); 55 | 56 | /* Menu system */ 57 | --side-bar-bg-color: var(--bg-color-dark); 58 | --side-bar-text-color: var(--text-color); 59 | --item-hover-bg-color: #4b5053; 60 | --window-border: 1px solid var(--bg-color); 61 | 62 | --active-file-bg-color: #4b5053; 63 | --active-file-border-color: var(--active-file-bg-color); 64 | --active-file-text-color: var(--text-color); 65 | 66 | --footer-bg-color: var(--side-bar-bg-color); 67 | 68 | --control-text-color: #afb1b2; 69 | 70 | /* General */ 71 | --font-size: 18px; 72 | /* --font-family: "LXGW WenKai Mono", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 73 | --monospace: 'LXGW WenKai Mono', 'SF Mono Medium', 'Fira Code', 'Cousine', 'Consolas', monospace; */ 74 | --font-family: BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol"; 75 | --monospace: BlinkMacSystemFont, 'SF Mono Medium', 'Fira Code', 'Cousine', 'Consolas', monospace; 76 | --heading-char-color: var(--light-trait-400); 77 | --color-popover-bg-color: var(--text-color); 78 | --rawblock-edit-panel-bd: var(--bg-color-dark); 79 | --meta-content-color: var(--body-text-color); 80 | --primary-btn-border-color: var(--body-text-color); 81 | --border-radius: 4px; 82 | 83 | 84 | /* Code variables */ 85 | --cm-line: #b8babb; 86 | --cm-variable: var(--text-color); 87 | --cm-keyword: #d1949e; 88 | --cm-tag: #d1949e; 89 | --cm-bracket: #d1949e; 90 | --cm-error: #ff5a5a; 91 | --cm-attribute: #d1949e; 92 | --cm-def: #eceded; 93 | --cm-comment: #998066; 94 | --cm-string: #bde052; 95 | --cm-operator: #f5b83d; 96 | --cm-number: #d1949e; 97 | --cm-meta: var(--text-color); 98 | --cm-atom: #845dc4; 99 | --cm-builtin: #bde052; 100 | --cm-property: var(--text-color); 101 | --cm-variable-2: var(--text-color); 102 | --cm-variable-3: #bde052; 103 | --cm-gutter: #f1f3f450; 104 | } 105 | 106 | .markdown-body { 107 | display: flex; 108 | flex-direction: row; 109 | flex-wrap: wrap; 110 | } 111 | 112 | .slice-div { 113 | width: 396px; 114 | height: 548px; 115 | background-color: var(--slice-bg-color); 116 | background-image: linear-gradient(to right bottom, #fde68a 0%, #fecaca 100%); 117 | background-size: 100% 100%; 118 | margin: 0 7px 26px 7px; 119 | padding: 30px; 120 | position: relative; 121 | } 122 | 123 | .slice-div:before { 124 | content: ""; 125 | position: absolute; 126 | top: 10px; 127 | bottom: 10px; 128 | left: 10px; 129 | right: 10px; 130 | z-index: 0; 131 | border-radius: 4px; 132 | background-color: rgba(255, 255, 255, 0.9); 133 | box-shadow: 0 0 5px rgba(0, 0, 0, 0.3); 134 | 135 | /* background-image: linear-gradient(to right bottom, #fffbeb 0%, #fef2f2 100%); */ 136 | } 137 | 138 | .slice-div:first-child .slice-content { 139 | font-size: 22px; 140 | } 141 | 142 | .slice-div:first-child::after { 143 | content: "右滑查看详情 >>>"; 144 | position: absolute; 145 | z-index: 2; 146 | color: var(--text-color); 147 | font-size: 20px; 148 | right: 30px; 149 | bottom: 20px; 150 | } 151 | 152 | .slice-div:first-child ul { 153 | margin-top: 30px; 154 | font-weight: bold; 155 | } 156 | 157 | .slice-background { 158 | width: 100%; 159 | height: 100%; 160 | position: absolute; 161 | top: 0; 162 | left: 0; 163 | z-index: 1; 164 | } 165 | 166 | .slice-header { 167 | display: flex; 168 | flex-direction: row; 169 | justify-content: space-between; 170 | align-items: flex-end; 171 | position: relative; 172 | z-index: 2; 173 | } 174 | .slice-content { 175 | position: relative; 176 | z-index: 2; 177 | } 178 | 179 | .slice-header-title { 180 | margin-left: 10px; 181 | display: flex; 182 | flex-grow: 1; 183 | height: 40px; 184 | flex-flow: column; 185 | justify-content: space-between; 186 | } 187 | 188 | .slice-header-title > span { 189 | line-height: 1.2; 190 | font-size: 22px; 191 | } 192 | 193 | .slice-header-decorate { 194 | height: 6px; 195 | background: linear-gradient(to right, #f59e0b, #d97706); 196 | } 197 | 198 | html { 199 | font-size: var(--font-size); 200 | } 201 | 202 | body { 203 | font-family: var(--font-family); 204 | -webkit-font-smoothing: antialiased; 205 | color: var(--body-text-color); 206 | background-color: var(--bg-color); 207 | line-height: 1.6; 208 | } 209 | 210 | pre { 211 | padding: 16px; 212 | overflow: auto; 213 | font-size: 85%; 214 | line-height: 1.45; 215 | background-color: var(--code-fence-bg-color); 216 | border-radius: 6px; 217 | margin-top: 0; 218 | margin-bottom: 16px; 219 | font-family: var(--monospace); 220 | word-wrap: normal; 221 | } 222 | 223 | code { 224 | color: var(--code-color); 225 | font-family: var(--monospace); 226 | font-weight: 600; 227 | } 228 | 229 | 230 | pre code { 231 | background-color: transparent; 232 | } 233 | 234 | 235 | a { 236 | color: var(--text-color); 237 | } 238 | 239 | h1, 240 | h2, 241 | h3, 242 | h4, 243 | h5, 244 | h6 { 245 | position: relative; 246 | margin-top: 1.5rem; 247 | margin-bottom: 1rem; 248 | font-weight: 700; 249 | line-height: 1.4; 250 | cursor: text; 251 | } 252 | 253 | cursor { 254 | color: var(--text-cursor-color) 255 | } 256 | 257 | h1:hover a.anchor, 258 | h2:hover a.anchor, 259 | h3:hover a.anchor, 260 | h4:hover a.anchor, 261 | h5:hover a.anchor, 262 | h6:hover a.anchor { 263 | text-decoration: none; 264 | } 265 | 266 | h1 code, 267 | h2 code, 268 | h3 code, 269 | h4 code, 270 | h5 code, 271 | h6 code { 272 | color: inherit; 273 | font-size: inherit; 274 | } 275 | 276 | h1, h2, h3, h4 { 277 | font-size: 1.2em; 278 | color: var(--code-color); 279 | } 280 | 281 | h1::before, 282 | h2::before, 283 | h3::before, 284 | h4::before { 285 | content: "#"; 286 | padding-right: 8px; 287 | } 288 | 289 | h5 { 290 | font-size: 1em; 291 | color: var(--light-trait-400); 292 | } 293 | 294 | h6 { 295 | font-size: 1em; 296 | color: var(--light-trait-400); 297 | } 298 | 299 | p, 300 | blockquote, 301 | ul, 302 | ol, 303 | dl, 304 | table { 305 | margin: 0.8em 0; 306 | } 307 | 308 | li>ol, 309 | li>ul { 310 | margin: 0 0; 311 | } 312 | 313 | ul>li { 314 | margin-top: 10px; 315 | } 316 | 317 | hr { 318 | background-color: var(--light-trait-100); 319 | height: 1.5px; 320 | border: none 321 | } 322 | 323 | a { 324 | color: var(--url-text-color); 325 | text-decoration: none; 326 | transition: all .1s ease-in; 327 | } 328 | 329 | a:hover { 330 | text-decoration: none; 331 | opacity: 1; 332 | } 333 | 334 | li p.first { 335 | display: inline-block; 336 | } 337 | 338 | ul { 339 | padding-left: 16px; 340 | margin-top: 14px; 341 | } 342 | 343 | ol { 344 | padding-left: 30px; 345 | margin-top: 14px; 346 | } 347 | 348 | ul::marker, ol::marker { 349 | color: var(--code-color); 350 | } 351 | 352 | ul:last-child, 353 | ol:last-child { 354 | margin-bottom: 0; 355 | } 356 | 357 | mark { 358 | border-radius: 4px; 359 | color: var(--text-highlight-color); 360 | font-weight: inherit; 361 | background-color: var(--text-highlight-bg); 362 | padding-left: 4px; 363 | padding-right: 4px; 364 | padding-top: 2px; 365 | padding-bottom: 2px; 366 | margin-left: 2px; 367 | margin-right: 2px; 368 | } 369 | 370 | blockquote { 371 | color: var(--blockquote-text-color); 372 | margin: 30px 0 0 0; 373 | /* border-left: 12px solid var(--blockquote-accent-color); */ 374 | padding: 10px; 375 | line-height: 1.2; 376 | background-color: rgba(255, 255, 255, 0.2); 377 | border-radius: 5px; 378 | border: 1px solid rgba(255, 255, 255, 0.1); 379 | box-shadow: 0 0 4px black; 380 | } 381 | 382 | blockquote p { 383 | margin: 5px 0; 384 | } 385 | 386 | blockquote::before { 387 | display: block; 388 | content: "🤔"; 389 | color: var(--code-color); 390 | } 391 | 392 | blockquote blockquote { 393 | padding-right: 0; 394 | } 395 | 396 | /* Alternating color rows in table*/ 397 | table tr:nth-child(2n) { 398 | background-color: var(--table-primary-color); 399 | } 400 | 401 | table tr:nth-child(2n + 1) { 402 | background-color: var(--table-secondary-color); 403 | } 404 | 405 | /* Alternating color rows in table*/ 406 | 407 | table { 408 | padding: 0; 409 | word-break: initial; 410 | } 411 | 412 | table tr { 413 | border-top: 1px solid var(--text-accent-color); 414 | margin: 0; 415 | padding: 0; 416 | } 417 | 418 | table tr th { 419 | font-weight: bold; 420 | border: 1px solid var(--text-accent-color); 421 | border-bottom: 0; 422 | margin: 0; 423 | padding: 6px 13px; 424 | } 425 | 426 | table tr td { 427 | border: 1px solid var(--text-accent-color); 428 | margin: 0; 429 | padding: 6px 13px; 430 | } 431 | 432 | table tr th:first-child, 433 | table tr td:first-child { 434 | margin-top: 0; 435 | } 436 | 437 | table tr th:last-child, 438 | table tr td:last-child { 439 | margin-bottom: 0; 440 | } 441 | -------------------------------------------------------------------------------- /themes/xhs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/xhs/logo.png -------------------------------------------------------------------------------- /themes/xhs/slice_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hardhackerlabs/medup/792400ed68099ce2b6c9e000e9aaa3cf6cc9d983/themes/xhs/slice_bg.png -------------------------------------------------------------------------------- /themes/xhs/template.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | {title} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 20 | 21 | 22 | 23 |
    24 | {{ if use_slice_mode }} 25 | 26 | {{ for slice_content in slices }} 27 |
    28 |
    {slice_header}
    29 |
    {slice_content}
    30 |
    31 | {{ endfor }} 32 | {{ else }} 33 | 34 | { content } 35 | {{ endif }} 36 |
    37 | 38 | 39 | 40 | 41 | 42 | 93 | 94 | --------------------------------------------------------------------------------