From 0a3b71340ea1ed8aa7e64b2dee032c929eca17ae Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Sat, 8 Mar 2025 23:37:50 +0100 Subject: [PATCH] switch to syntect --- Cargo.lock | 686 +++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 4 +- src/lib.rs | 746 +++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 643 +----------------------------------------- test/simple.md | 19 ++ 5 files changed, 1461 insertions(+), 637 deletions(-) create mode 100644 Cargo.lock create mode 100644 src/lib.rs create mode 100644 test/simple.md diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..73b6b9b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,686 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" + +[[package]] +name = "anstyle-parse" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +dependencies = [ + "anstyle", + "once_cell", + "windows-sys", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" + +[[package]] +name = "colorchoice" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "flate2" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11faaf5a5236997af9848be0bef4db95824b1d534ebc64d0f0c6cf3e67bd38dc" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "libc" +version = "0.2.170" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "markdown" +version = "1.0.0-alpha.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9047e0a37a596d4e15411a1ffbdabe71c328908cb90a721cb9bf8dcf3434e6d2" +dependencies = [ + "serde", + "unicode-id", +] + +[[package]] +name = "mdsvexrs" +version = "0.1.0" +dependencies = [ + "clap", + "itertools", + "markdown", + "regex", + "serde", + "serde_json", + "serde_yaml", + "syntect", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5" +dependencies = [ + "adler2", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "once_cell" +version = "1.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" + +[[package]] +name = "onig" +version = "6.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c4b31c8722ad9171c6d77d3557db078cab2bd50afcc9d09c8b315c59df8ca4f" +dependencies = [ + "bitflags", + "libc", + "once_cell", + "onig_sys", +] + +[[package]] +name = "onig_sys" +version = "69.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b829e3d7e9cc74c7e315ee8edb185bf4190da5acde74afd7fc59c35b1f086e7" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plist" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d3a6e5838b60e0e8fa7a43f22ade549a37d61f8bdbe636d0d7816191de969c2" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.218" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syntect" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874dcfa363995604333cf947ae9f751ca3af4522c60886774c4963943b4746b1" +dependencies = [ + "bincode", + "bitflags", + "flate2", + "fnv", + "once_cell", + "onig", + "plist", + "regex-syntax", + "serde", + "serde_derive", + "serde_json", + "thiserror", + "walkdir", + "yaml-rust", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.3.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" + +[[package]] +name = "time-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "unicode-id" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10103c57044730945224467c09f71a4db0071c123a0648cc3e818913bde6b561" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml index 61510ec..27b2d13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "markdown" +name = "mdsvexrs" version = "0.1.0" edition = "2021" @@ -11,4 +11,4 @@ serde_yaml = "0.9.34" serde = { version = "1.0.215", features = ["derive"] } regex = "1.11.1" clap = { version = "4.5.21", features = ["derive"] } -syntext = "5.0" \ No newline at end of file +syntect = "5.0" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3120cbe --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,746 @@ +use std::{ + sync::LazyLock, + time::{Duration, Instant}, +}; + +use itertools::Itertools; +use markdown::{ + mdast::{ + Blockquote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, + FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, InlineMath, Link, + LinkReference, List, ListItem, Math, MdxFlowExpression, MdxJsxFlowElement, + MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph, Root, Strong, Table, + TableCell, TableRow, Text, ThematicBreak, Toml, Yaml, + }, + unist::Position, + Constructs, +}; +use serde::Serialize; +use serde_json::Value; +use syntect::{ + easy::HighlightLines, + highlighting::ThemeSet, + html::{append_highlighted_html_for_styled_line, IncludeBackground}, + parsing::SyntaxSet, + util::LinesWithEndings, +}; + +#[derive(Debug)] +struct ToHtmlResult { + html: String, + svelte: bool, +} + +impl ToHtmlResult { + fn new(html: String, svelte: bool) -> Self { + Self { html, svelte } + } + + fn empty() -> Self { + Self::new("".to_string(), false) + } +} + +trait ToHtml { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult; + + fn visit(&self, _ctx: &mut Context) {} +} + +impl ToHtml for Vec +where + T: ToHtml, +{ + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + merge(&self.iter().map(|t| t.to_html(ctx)).collect::>()) + } + + fn visit(&self, ctx: &mut Context) { + for t in self { + t.visit(ctx); + } + } +} + +impl ToHtml for Root { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + self.children.to_html(ctx) + } + + fn visit(&self, ctx: &mut Context) { + self.children.visit(ctx); + } +} + +impl ToHtml for Blockquote { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new( + format!("
{}
", children.html), + children.svelte, + ) + } +} + +impl ToHtml for FootnoteDefinition { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } + + fn visit(&self, _ctx: &mut Context) { + todo!() + } +} + +impl ToHtml for MdxJsxFlowElement { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for List { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + // todo!() + let litype = match self.ordered { + true => "ol", + false => "ul", + }; + let children = self.children.to_html(ctx); + ToHtmlResult::new( + format!("<{}>{}", litype, children.html, litype), + children.svelte, + ) + } +} + +impl ToHtml for MdxjsEsm { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for Toml { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for Yaml { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::empty() + } + + fn visit(&self, ctx: &mut Context) { + let value = serde_yaml::from_str(&self.value).unwrap(); + ctx.yaml = Some(value); + } +} + +impl ToHtml for Break { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::new("
".to_string(), false) + } +} + +static LANG_HINT_REGEX: LazyLock = + LazyLock::new(|| regex::Regex::new(r"\{:(?[\w.]+)\}$").unwrap()); + +impl ToHtml for InlineCode { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let value = &self.value; + // if value ends with {lang} then it's a language hint + let output = if let Some(caps) = LANG_HINT_REGEX.captures(value) { + let lang = &caps["lang"]; + let code = &value[..value.len() - lang.len() - 3]; + ctx.highlight(HighlightRequest { + lang: lang.to_string(), + inline: true, + code: code.to_string(), + meta: None, + }) + } else if let Some(lang) = &ctx.default_lang { + ctx.highlight(HighlightRequest { + lang: lang.clone(), + inline: true, + code: value.clone(), + meta: None, + }) + } else { + format!("{}", html_encode(&self.value)) + }; + ToHtmlResult::new(output, false) + } +} + +impl ToHtml for InlineMath { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Delete { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Emphasis { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for MdxTextExpression { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for FootnoteReference { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Html { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let value = self.value.clone(); + if value.starts_with(" ToHtmlResult { + let alt = &self.alt; + let title = self + .title + .as_ref() + .map(|t| format!(" title=\"{}\"", t)) + .unwrap_or_default(); + let url = &self.url; + ToHtmlResult::new( + format!("\"{}\"{}", url, alt, title), + false, + ) + } +} + +impl ToHtml for ImageReference { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for MdxJsxTextElement { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +impl ToHtml for Link { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + let title = self + .title + .as_ref() + .map(|t| format!(" title=\"{}\"", t)) + .unwrap_or_default(); + ToHtmlResult::new( + format!("{}", self.url, title, children.html), + children.svelte, + ) + } +} + +impl ToHtml for LinkReference { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Strong { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new( + format!("{}", children.html), + children.svelte, + ) + } +} + +impl ToHtml for Text { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::new(html_encode(&self.value), false) + } +} + +impl ToHtml for Code { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let value = &self.value; + let lang = self.lang.as_ref().or(ctx.default_lang.as_ref()); + let highlighted = if let Some(lang) = lang { + ctx.highlight(HighlightRequest { + code: value.clone(), + inline: false, + lang: lang.clone(), + meta: self.meta.clone(), + }) + } else { + format!("
{}
", html_encode(&self.value)) + }; + ToHtmlResult::new(highlighted, false) + } +} + +impl ToHtml for Math { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for MdxFlowExpression { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + unimplemented!() + } +} + +fn slug(str: &str) -> String { + str.to_lowercase().replace(" ", "-") +} + +impl ToHtml for Heading { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + let text = self + .children + .iter() + .filter_map(|c| match c { + Node::Text(t) => Some(t.value.clone()), + _ => None, + }) + .join(""); + let mut slug = slug(&text); + if ctx.titles.iter().any(|t| t.id == slug) { + let mut i = 1; + while ctx.titles.iter().any(|t| t.id == format!("{}-{}", slug, i)) { + i += 1; + } + slug = format!("{}-{}", slug, i); + } + ctx.titles.push(Title { + level: self.depth, + text: text.clone(), + id: slug.clone(), + pos: self.position.clone(), + }); + ToHtmlResult::new( + format!( + "\n{}\n", + self.depth, slug, children.html, self.depth + ), + children.svelte, + ) + } +} + +impl ToHtml for Table { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}
", children.html), children.svelte) + } +} + +impl ToHtml for ThematicBreak { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + ToHtmlResult::new("\n
\n".to_string(), false) + } +} + +impl ToHtml for TableRow { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for TableCell { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("{}", children.html), children.svelte) + } +} + +impl ToHtml for ListItem { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("
  • {}
  • ", children.html), children.svelte) + } +} + +impl ToHtml for Definition { + fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { + todo!() + } +} + +impl ToHtml for Paragraph { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + let children = self.children.to_html(ctx); + ToHtmlResult::new(format!("

    {}

    ", children.html), children.svelte) + } +} + +macro_rules! node_impl { + ($($node:ident($name:ident)),+) => { + impl ToHtml for Node { + fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { + match self { + $(markdown::mdast::Node::$node($name) => $name.to_html(ctx)),+ + } + } + fn visit(&self, ctx: &mut Context) { + match self { + $(markdown::mdast::Node::$node($name) => $name.visit(ctx)),+ + } + } + } + } +} + +node_impl!( + Root(root), + Blockquote(blockquote), + FootnoteDefinition(footnote_definition), + MdxJsxFlowElement(mdx_jsx_flow_element), + List(list), + MdxjsEsm(mdxjs_esm), + Toml(toml), + Yaml(yaml), + Break(rbreak), + InlineCode(inline_code), + InlineMath(inline_math), + Delete(delete), + Emphasis(emphasis), + MdxTextExpression(mdx_text_expression), + FootnoteReference(footnote_reference), + Html(html), + Image(image), + ImageReference(image_reference), + MdxJsxTextElement(mdx_jsx_text_element), + Link(link), + LinkReference(link_reference), + Strong(strong), + Text(text), + Code(code), + Math(math), + MdxFlowExpression(mdx_flow_expression), + Heading(heading), + Table(table), + ThematicBreak(thematic_break), + TableRow(table_row), + TableCell(table_cell), + ListItem(list_item), + Definition(definition), + Paragraph(paragraph) +); + +fn svelte_html_encode(string: String) -> String { + String::from("{@html `") + &string.replace("\\", "\\\\").replace("`", "\\`") + "`}" +} + +fn merge(results: &[ToHtmlResult]) -> ToHtmlResult { + let chunked = results.iter().chunk_by(|r| r.svelte); + let mut html = chunked.into_iter().map(|(svelte, results)| { + let html = results.map(|r| r.html.clone()).join(""); + ToHtmlResult::new(html, svelte) + }); + let Some(first) = html.next() else { + return ToHtmlResult::new("".to_string(), false); + }; + if let Some(second) = html.next() { + ToHtmlResult::new( + [first, second] + .into_iter() + .chain(html) + .map(|r| { + if r.html.is_empty() { + return "".to_string(); + } + if r.svelte { + r.html + } else { + svelte_html_encode(r.html) + } + }) + .join(""), + true, + ) + } else { + first + } +} + +// from markdown-rs +pub fn html_encode(value: &str) -> String { + let encode_html = true; // originally a param + // It’ll grow a bit bigger for each dangerous character. + let mut result = String::with_capacity(value.len()); + let bytes = value.as_bytes(); + let mut index = 0; + let mut start = 0; + + while index < bytes.len() { + let byte = bytes[index]; + if matches!(byte, b'\0') || (encode_html && matches!(byte, b'&' | b'"' | b'<' | b'>')) { + result.push_str(&value[start..index]); + result.push_str(match byte { + b'\0' => "�", + b'&' => "&", + b'"' => """, + b'<' => "<", + // `b'>'` + _ => ">", + }); + + start = index + 1; + } + + index += 1; + } + + result.push_str(&value[start..]); + + result +} + +fn finish(res: ToHtmlResult) -> String { + if res.svelte { + res.html + } else { + svelte_html_encode(res.html) + } +} + +#[derive(Serialize)] +pub struct Title { + pub level: u8, + pub text: String, + pub id: String, + pub pos: Option, +} + +pub struct Context { + pub yaml: Option>, + pub default_lang: Option, + pub script: Option, + pub options: MdsvexrsOptions, + pub titles: Vec, + + syntax_set: SyntaxSet, + theme_set: ThemeSet, + + pub(crate) highlight_times: Duration, + pub(crate) parse_time: Duration, + pub(crate) visit_time: Duration, + pub(crate) convert_time: Duration, +} + +#[derive(Serialize)] +struct HighlightRequest { + code: String, + inline: bool, + lang: String, + meta: Option<String>, +} + +pub struct MdsvexrsOptions { + pub layout: String, + pub path: String, +} + +impl Context { + pub fn new(options: MdsvexrsOptions) -> Self { + + let syntax_set = SyntaxSet::load_defaults_newlines(); + let theme_set = ThemeSet::load_defaults(); + + Context { + // child, + // bufread, + syntax_set, + theme_set, + yaml: None, + titles: Vec::new(), + default_lang: None, + script: None, + options, + highlight_times: Duration::ZERO, + parse_time: Duration::ZERO, + visit_time: Duration::ZERO, + convert_time: Duration::ZERO, + } + } + + fn highlight(&mut self, code: HighlightRequest) -> String { + let theme = &self.theme_set.themes["InspiredGitHub"]; + let start = Instant::now(); + + let syntax = self.syntax_set.find_syntax_by_token(&code.lang); + let syntax = match syntax { + Some(t) => t, + None => { + println!("No syntax found for {}", code.lang); + return format!("<pre><code>{}</code></pre>", html_encode(&code.code)); + } + }; + let mut highlighter = HighlightLines::new(syntax, theme); + + let mut string = String::new(); + + match code.inline { + true => { + let regions = highlighter + .highlight_line(&code.code, &self.syntax_set) + .unwrap(); + string += "<code>"; + append_highlighted_html_for_styled_line( + ®ions[..], + IncludeBackground::No, + &mut string, + ) + .unwrap(); + string += "</code>"; + } + false => { + string += "<pre>\n<code>"; + for line in LinesWithEndings::from(&code.code) { + let regions = highlighter.highlight_line(line, &self.syntax_set).unwrap(); + append_highlighted_html_for_styled_line( + ®ions[..], + IncludeBackground::No, + &mut string, + ) + .unwrap(); + } + string += "</code></pre>\n"; + } + }; + + self.highlight_times += start.elapsed(); + string + } + + fn resolve_layout(&self) -> &str { + &self.options.layout + } + + pub fn convert(&mut self, input: &str) -> String { + let start = Instant::now(); + let ast = markdown::to_mdast(input, &DEFAULT_MD_OPTIONS).unwrap(); + self.parse_time = start.elapsed(); + let start = Instant::now(); + ast.visit(self); + self.visit_time = start.elapsed(); + + if let Some(yaml) = &self.yaml { + if let Some(val) = yaml.get("defaultLang") { + self.default_lang = Some(val.as_str().unwrap().to_string()); + } + } + + let start = Instant::now(); + let res = ast.to_html(self); + let html = finish(res); + self.convert_time = start.elapsed(); + + if let Some(yaml) = &mut self.yaml { + yaml.insert( + "titles".to_string(), + serde_json::to_value(&self.titles).unwrap(), + ); + } + + let value = self + .script + .clone() + .unwrap_or_else(|| String::from("<script></script>")); + + let script = { + let end = value.find('>').expect( + "Unclosed script tag (found <script but not >). May be a bug with Markdown parser.", + ) + 1; + let mut script = value[..end].to_string(); + let layout = self.resolve_layout(); + script += format!("import MDXLayout from \"{}\";", layout).as_str(); + script += &value[end..]; + script + }; + + let frontmatter = + (|| serde_json::to_string(self.yaml.as_ref()?).ok())().unwrap_or("{}".to_string()); + + format!( + "<script context=\"module\">export const metadata = {frontmatter}</script> +{script} +<MDXLayout {{...metadata}}> +{html} +</MDXLayout>" + ) + } + + pub fn print_timings(&self) { + println!("Parse: {:?}", self.parse_time); + println!("Visit: {:?}", self.visit_time); + println!("Convert: {:?}", self.convert_time); + println!("Highlight: {:?}", self.highlight_times); + } +} + +pub(crate) const DEFAULT_MD_OPTIONS: markdown::ParseOptions = markdown::ParseOptions { + constructs: Constructs { + attention: true, + autolink: true, + block_quote: true, + character_escape: true, + character_reference: true, + code_indented: true, + code_fenced: true, + code_text: true, + definition: true, + frontmatter: true, + gfm_autolink_literal: true, + gfm_footnote_definition: true, + gfm_label_start_footnote: true, + gfm_strikethrough: true, + gfm_table: true, + gfm_task_list_item: true, + hard_break_escape: true, + hard_break_trailing: true, + heading_atx: true, + heading_setext: true, + html_flow: true, + html_text: true, + label_start_image: true, + label_start_link: true, + label_end: true, + list_item: true, + math_flow: true, + math_text: true, + mdx_esm: false, + mdx_expression_flow: false, + mdx_expression_text: false, + mdx_jsx_flow: false, + mdx_jsx_text: false, + thematic_break: true, + }, + math_text_single_dollar: true, + gfm_strikethrough_single_tilde: true, + mdx_expression_parse: None, + mdx_esm_parse: None, +}; diff --git a/src/main.rs b/src/main.rs index 6c15efe..1f3818f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,548 +1,7 @@ -use std::{io::{stdin, BufRead, BufReader, Read, Write}, process::{Child, ChildStdout, Command, Stdio}, sync::LazyLock, time::{Duration, Instant}}; +use std::io::{stdin, Read}; -use itertools::Itertools; -use markdown::{mdast::{Blockquote, Break, Code, Definition, Delete, Emphasis, FootnoteDefinition, FootnoteReference, Heading, Html, Image, ImageReference, InlineCode, InlineMath, Link, LinkReference, List, ListItem, Math, MdxFlowExpression, MdxJsxFlowElement, MdxJsxTextElement, MdxTextExpression, MdxjsEsm, Node, Paragraph, Root, Strong, Table, TableCell, TableRow, Text, ThematicBreak, Toml, Yaml}, unist::Position, Constructs}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; use clap::Parser; - -#[derive(Debug)] -struct ToHtmlResult { - html: String, - svelte: bool -} - -impl ToHtmlResult { - fn new(html: String, svelte: bool) -> Self { - Self { html, svelte } - } - - fn empty() -> Self { - Self::new("".to_string(), false) - } -} - -trait ToHtml { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult; - - fn visit(&self, _ctx: &mut Context) {} -} - -impl<T> ToHtml for Vec<T> where T: ToHtml { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - merge(&self.iter().map(|t| t.to_html(ctx)).collect::<Vec<_>>()) - } - - fn visit(&self, ctx: &mut Context) { - for t in self { - t.visit(ctx); - } - } -} - -impl ToHtml for Root { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - self.children.to_html(ctx) - } - - fn visit(&self, ctx: &mut Context) { - self.children.visit(ctx); - } -} - -impl ToHtml for Blockquote { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<blockquote>{}</blockquote>", children.html), children.svelte) - } -} - -impl ToHtml for FootnoteDefinition { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } - - fn visit(&self, _ctx: &mut Context) { - todo!() - } -} - -impl ToHtml for MdxJsxFlowElement { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - unimplemented!() - } -} - -impl ToHtml for List { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - // todo!() - let litype = match self.ordered { - true => "ol", - false => "ul" - }; - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<{}>{}</{}>", litype, children.html, litype), children.svelte) - } -} - -impl ToHtml for MdxjsEsm { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - unimplemented!() - } -} - -impl ToHtml for Toml { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - unimplemented!() - } -} - -impl ToHtml for Yaml { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - ToHtmlResult::empty() - } - - fn visit(&self, ctx: &mut Context) { - let value = serde_yaml::from_str(&self.value).unwrap(); - ctx.yaml = Some(value); - } -} - -impl ToHtml for Break { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - ToHtmlResult::new("<br>".to_string(), false) - } -} - -static LANG_HINT_REGEX: LazyLock<regex::Regex> = LazyLock::new(|| regex::Regex::new(r"\{:(?<lang>[\w.]+)\}$").unwrap()); - -impl ToHtml for InlineCode { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let value = &self.value; - // if value ends with {lang} then it's a language hint - let output = if let Some(caps) = LANG_HINT_REGEX.captures(value) { - let lang = &caps["lang"]; - let code = &value[..value.len() - lang.len() - 3]; - ctx.highlight(HighlightRequest { - lang: lang.to_string(), - inline: true, - code: code.to_string(), - meta: None - }) - } else if let Some(lang) = &ctx.default_lang { - ctx.highlight(HighlightRequest { - lang: lang.clone(), - inline: true, - code: value.clone(), - meta: None - }) - } else { - format!("<code>{}</code>", html_encode(&self.value)) - }; - ToHtmlResult::new(output, false) - } -} - -impl ToHtml for InlineMath { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for Delete { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for Emphasis { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<em>{}</em>", children.html), children.svelte) - } -} - -impl ToHtml for MdxTextExpression { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - unimplemented!() - } -} - -impl ToHtml for FootnoteReference { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for Html { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let value = self.value.clone(); - if value.starts_with("<script") { - ctx.script = Some(value); - - return ToHtmlResult::empty(); - } - ToHtmlResult::new(value, true) - } -} - -impl ToHtml for Image { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - let alt = &self.alt; - let title = self.title.as_ref().map(|t| format!(" title=\"{}\"", t)).unwrap_or_default(); - let url = &self.url; - ToHtmlResult::new(format!("<img src=\"{}\" alt=\"{}\"{}>", url, alt, title), false) - } -} - -impl ToHtml for ImageReference { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for MdxJsxTextElement { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - unimplemented!() - } -} - -impl ToHtml for Link { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - let title = self.title.as_ref().map(|t| format!(" title=\"{}\"", t)).unwrap_or_default(); - ToHtmlResult::new(format!("<a href=\"{}\"{}>{}</a>", self.url, title, children.html), children.svelte) - } -} - -impl ToHtml for LinkReference { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for Strong { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<strong>{}</strong>", children.html), children.svelte) - } -} - -impl ToHtml for Text { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - ToHtmlResult::new(html_encode(&self.value), false) - } -} - -impl ToHtml for Code { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let value = &self.value; - let lang = self.lang.as_ref().or(ctx.default_lang.as_ref()); - let highlighted = if let Some(lang) = lang { - ctx.highlight(HighlightRequest { - code: value.clone(), - inline: false, - lang: lang.clone(), - meta: self.meta.clone() - }) - } else { - format!("<pre><code>{}</code></pre>", html_encode(&self.value)) - }; - ToHtmlResult::new(highlighted, false) - } -} - -impl ToHtml for Math { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for MdxFlowExpression { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - unimplemented!() - } -} - -fn slug(str: &str) -> String { - str.to_lowercase().replace(" ", "-") -} - -impl ToHtml for Heading { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - let text = self.children.iter().filter_map(|c| { - match c { - Node::Text(t) => Some(t.value.clone()), - _ => None - } - }).join(""); - let mut slug = slug(&text); - if ctx.titles.iter().any(|t| t.id == slug) { - let mut i = 1; - while ctx.titles.iter().any(|t| t.id == format!("{}-{}", slug, i)) { - i += 1; - } - slug = format!("{}-{}", slug, i); - } - ctx.titles.push(Title { - level: self.depth, - text: text.clone(), - id: slug.clone(), - pos: self.position.clone() - }); - ToHtmlResult::new(format!("\n<h{} id=\"{}\">{}</h{}>\n", self.depth, slug, children.html, self.depth), children.svelte) - } -} - -impl ToHtml for Table { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<table>{}</table>", children.html), children.svelte) - } -} - -impl ToHtml for ThematicBreak { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - ToHtmlResult::new("\n<hr>\n".to_string(), false) - } -} - -impl ToHtml for TableRow { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<tr>{}</tr>", children.html), children.svelte) - } -} - -impl ToHtml for TableCell { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<td>{}</td>", children.html), children.svelte) - } -} - -impl ToHtml for ListItem { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<li>{}</li>", children.html), children.svelte) - } -} - -impl ToHtml for Definition { - fn to_html(&self, _ctx: &mut Context) -> ToHtmlResult { - todo!() - } -} - -impl ToHtml for Paragraph { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - let children = self.children.to_html(ctx); - ToHtmlResult::new(format!("<p>{}</p>", children.html), children.svelte) - } -} - -macro_rules! node_impl { - ($($node:ident($name:ident)),+) => { - impl ToHtml for Node { - fn to_html(&self, ctx: &mut Context) -> ToHtmlResult { - match self { - $(markdown::mdast::Node::$node($name) => $name.to_html(ctx)),+ - } - } - fn visit(&self, ctx: &mut Context) { - match self { - $(markdown::mdast::Node::$node($name) => $name.visit(ctx)),+ - } - } - } - } -} - -node_impl!( - Root(root), - Blockquote(blockquote), - FootnoteDefinition(footnote_definition), - MdxJsxFlowElement(mdx_jsx_flow_element), - List(list), - MdxjsEsm(mdxjs_esm), - Toml(toml), - Yaml(yaml), - Break(rbreak), - InlineCode(inline_code), - InlineMath(inline_math), - Delete(delete), - Emphasis(emphasis), - MdxTextExpression(mdx_text_expression), - FootnoteReference(footnote_reference), - Html(html), - Image(image), - ImageReference(image_reference), - MdxJsxTextElement(mdx_jsx_text_element), - Link(link), - LinkReference(link_reference), - Strong(strong), - Text(text), - Code(code), - Math(math), - MdxFlowExpression(mdx_flow_expression), - Heading(heading), - Table(table), - ThematicBreak(thematic_break), - TableRow(table_row), - TableCell(table_cell), - ListItem(list_item), - Definition(definition), - Paragraph(paragraph) -); - -fn svelte_html_encode(string: String) -> String { - String::from("{@html `") + &string.replace("\\", "\\\\").replace("`", "\\`") + "`}" -} - -fn merge(results: &[ToHtmlResult]) -> ToHtmlResult { - let chunked = results.iter().chunk_by(|r| r.svelte); - let mut html = chunked.into_iter().map(|(svelte, results)| { - let html = results.map(|r| r.html.clone()).join(""); - ToHtmlResult::new(html, svelte) - }); - let Some(first) = html.next() else { - return ToHtmlResult::new("".to_string(), false); - }; - if let Some(second) = html.next() { - ToHtmlResult::new( - [first, second].into_iter().chain(html).map(|r| { - if r.html.is_empty() { - return "".to_string(); - } - if r.svelte { - r.html - } else { - svelte_html_encode(r.html) - } - }).join(""), - true - ) - } else { - first - } -} - -// from markdown-rs -pub fn html_encode(value: &str) -> String { - let encode_html = true; // originally a param - // It’ll grow a bit bigger for each dangerous character. - let mut result = String::with_capacity(value.len()); - let bytes = value.as_bytes(); - let mut index = 0; - let mut start = 0; - - while index < bytes.len() { - let byte = bytes[index]; - if matches!(byte, b'\0') || (encode_html && matches!(byte, b'&' | b'"' | b'<' | b'>')) { - result.push_str(&value[start..index]); - result.push_str(match byte { - b'\0' => "�", - b'&' => "&", - b'"' => """, - b'<' => "<", - // `b'>'` - _ => ">", - }); - - start = index + 1; - } - - index += 1; - } - - result.push_str(&value[start..]); - - result -} - -fn finish(res: ToHtmlResult) -> String { - if res.svelte { - res.html - } else { - svelte_html_encode(res.html) - } -} - -#[derive(Serialize)] -struct Title { - level: u8, - text: String, - id: String, - pos: Option<Position> -} - -struct Context { - child: Child, - bufread: BufReader<ChildStdout>, - yaml: Option<serde_json::Map<String, Value>>, - default_lang: Option<String>, - script: Option<String>, - args: Args, - titles: Vec<Title>, - - highlight_times: Duration, - js_times: Duration, - js_sum: f64 -} - -#[derive(Serialize)] -struct HighlightRequest { - code: String, - inline: bool, - lang: String, - meta: Option<String> -} - -#[derive(Deserialize)] -struct HighlightResponse { - html: String, - - elapsed: f64, - sum: f64 -} - -impl Context { - fn new(args: Args) -> Self { - let mut child = Command::new("node") - .arg(".") - .current_dir("highlighter") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn().unwrap(); - let stdout = child.stdout.take().unwrap(); - let bufread = BufReader::new(stdout); - - Context { child, bufread, yaml: None, titles: Vec::new(), default_lang: None, script: None, args, highlight_times: Duration::ZERO, js_times: Duration::ZERO, js_sum: 0. } - } - - fn highlight(&mut self, code: HighlightRequest) -> String { - let start = Instant::now(); - - let stdin = self.child.stdin.as_mut().unwrap(); - let data = serde_json::to_string(&code).unwrap() + "\n"; - stdin.write_all(data.as_bytes()).unwrap(); - - let mut buf = String::new(); - let _line = self.bufread.read_line(&mut buf).unwrap(); - let res: HighlightResponse = serde_json::from_str(&buf).unwrap(); - - self.highlight_times += start.elapsed(); - self.js_times += Duration::from_nanos((res.elapsed * 1_000_000.) as u64); - self.js_sum = res.sum; - res.html - } - - fn resolve_layout(&self) -> &str { - &self.args.layout - // Path::new(&self.args.layout). - } -} +use mdsvexrs::Context; #[derive(Parser)] struct Args { @@ -554,104 +13,18 @@ struct Args { timings: bool } -/// Converts markdown to svelte code, MDSvex alternative. Expects trusted code! fn main() { - let options = markdown::ParseOptions { - constructs: Constructs { - attention: true, - autolink: true, - block_quote: true, - character_escape: true, - character_reference: true, - code_indented: true, - code_fenced: true, - code_text: true, - definition: true, - frontmatter: true, - gfm_autolink_literal: true, - gfm_footnote_definition: true, - gfm_label_start_footnote: true, - gfm_strikethrough: true, - gfm_table: true, - gfm_task_list_item: true, - hard_break_escape: true, - hard_break_trailing: true, - heading_atx: true, - heading_setext: true, - html_flow: true, - html_text: true, - label_start_image: true, - label_start_link: true, - label_end: true, - list_item: true, - math_flow: true, - math_text: true, - mdx_esm: false, - mdx_expression_flow: false, - mdx_expression_text: false, - mdx_jsx_flow: false, - mdx_jsx_text: false, - thematic_break: true, - }, - math_text_single_dollar: true, - gfm_strikethrough_single_tilde: true, - ..Default::default() - }; let args = Args::parse(); - let mut ctx = Context::new(args); + let mut ctx = Context::new(mdsvexrs::MdsvexrsOptions { layout: args.layout, path: args.path }); + let mut input = String::new(); - - - let start = Instant::now(); stdin().read_to_string(&mut input).unwrap(); - let stdin_read = start.elapsed(); - - let start = Instant::now(); - let ast = markdown::to_mdast(&input, &options).unwrap(); - let ast_parse = start.elapsed(); - - let start = Instant::now(); - ast.visit(&mut ctx); - let ast_visit = start.elapsed(); + let output = ctx.convert(&input); - if let Some(yaml) = &ctx.yaml { - if let Some(val) = yaml.get("defaultLang") { - ctx.default_lang = Some(val.as_str().unwrap().to_string()); - } - } - - let start = Instant::now(); - let res = ast.to_html(&mut ctx); - let html = finish(res); - let html_convert = start.elapsed(); - - if ctx.args.timings { - dbg!(stdin_read, ast_parse, ast_visit, html_convert, ctx.highlight_times, ctx.js_times, ctx.js_sum); + if args.timings { + ctx.print_timings(); return; } - if let Some(yaml) = &mut ctx.yaml { - yaml.insert("titles".to_string(), serde_json::to_value(&ctx.titles).unwrap()); - } - - let value = ctx.script.clone().unwrap_or_else(|| String::from("<script></script>")); - - let script = { - let end = value.find('>').expect("Unclosed script tag (found <script but not >). May be a bug with Markdown parser.") + 1; - let mut script = value[..end].to_string(); - let layout = ctx.resolve_layout(); - script += format!("import MDXLayout from \"{}\";", layout).as_str(); - script += &value[end..]; - script - }; - - let frontmatter = (|| { - serde_json::to_string(&ctx.yaml?).ok() - })().unwrap_or("{}".to_string()); - println!("<script context=\"module\">export const metadata = {}</script>", frontmatter); - - println!("{script}"); - println!("<MDXLayout {{...metadata}}>"); - println!("{}", html); - println!("</MDXLayout>"); + print!("{output}"); } \ No newline at end of file diff --git a/test/simple.md b/test/simple.md new file mode 100644 index 0000000..2b841c2 --- /dev/null +++ b/test/simple.md @@ -0,0 +1,19 @@ +--- +yaml: hello +defaultLang: js +--- +<script lang="ts"> + console.log(metadata.titles); +</script> + +# Title + +Hello there + +## Sub + +Another content with `Inline::code(){:rs}`. And a `function defaultLang()`. + +```rs +fn test() {} +``` \ No newline at end of file