From 06f9178ec4ecfa8df0bbb4574df5592c10826741 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Thu, 20 Feb 2025 20:08:59 +0800 Subject: [PATCH] text: Add TextView with Markdown and Simple HTML support. (#639) --- .github/workflows/ci.yml | 5 +- Cargo.lock | 1146 ++++++++++++++++++++++++++++- README.md | 1 + crates/story/Cargo.toml | 1 + crates/story/examples/html.html | 102 +++ crates/story/examples/html.rs | 87 +++ crates/story/examples/markdown.md | 143 ++++ crates/story/examples/markdown.rs | 63 ++ crates/story/src/lib.rs | 5 + crates/ui/Cargo.toml | 6 + crates/ui/src/lib.rs | 1 + crates/ui/src/text/element.rs | 583 +++++++++++++++ crates/ui/src/text/html.rs | 726 ++++++++++++++++++ crates/ui/src/text/markdown.rs | 395 ++++++++++ crates/ui/src/text/mod.rs | 47 ++ crates/ui/src/text/utils.rs | 61 ++ 16 files changed, 3343 insertions(+), 29 deletions(-) create mode 100644 crates/story/examples/html.html create mode 100644 crates/story/examples/html.rs create mode 100644 crates/story/examples/markdown.md create mode 100644 crates/story/examples/markdown.rs create mode 100644 crates/ui/src/text/element.rs create mode 100644 crates/ui/src/text/html.rs create mode 100644 crates/ui/src/text/markdown.rs create mode 100644 crates/ui/src/text/mod.rs create mode 100644 crates/ui/src/text/utils.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2365c7a1..1325731e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: - name: Machete uses: bnjbvr/cargo-machete@main - name: Setup | Cache Cargo - uses: actions/cache@v3.0.11 + uses: actions/cache@v4 with: path: | ~/.cargo/bin/ @@ -35,4 +35,5 @@ jobs: cargo clippy -- --deny warnings typos - name: Build test - run: cargo build + run: | + cargo test --all diff --git a/Cargo.lock b/Cargo.lock index 58ec6e59..aaedcf53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + [[package]] name = "adler2" version = "2.0.0" @@ -20,6 +29,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.15", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -34,6 +54,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -242,7 +271,7 @@ dependencies = [ "polling 2.8.0", "rustix 0.37.27", "slab", - "socket2", + "socket2 0.4.10", "waker-fn", ] @@ -453,6 +482,21 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + [[package]] name = "base62" version = "2.0.2" @@ -465,6 +509,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "781dd20c3aff0bd194fe7d2a977dd92f21c173891f3a03b677359e5fa457e5d5" +dependencies = [ + "simd-abstraction", +] + [[package]] name = "bindgen" version = "0.70.1" @@ -474,7 +527,7 @@ dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.11.0", + "itertools 0.13.0", "log", "prettyplease", "proc-macro2", @@ -524,6 +577,18 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blade-graphics" version = "0.6.0" @@ -645,6 +710,28 @@ version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytemuck" version = "1.19.0" @@ -821,7 +908,7 @@ version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f3778aa0dfc6500b1f4ed1f4f9d61b5292b59f52dd6988ab7abbb8707fc7380" dependencies = [ - "ahash", + "ahash 0.8.11", "arc-swap", "charts-rs-derive", "fontdue", @@ -952,7 +1039,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "indexmap 2.7.1", "rustc-hash 2.1.0", @@ -1003,12 +1090,41 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "const-str" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" +dependencies = [ + "const-str-proc-macro", +] + +[[package]] +name = "const-str-proc-macro" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.1" @@ -1231,6 +1347,28 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "cssparser" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa 1.0.11", + "phf 0.11.3", + "smallvec", +] + +[[package]] +name = "cssparser-color" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" +dependencies = [ + "cssparser 0.33.0", +] + [[package]] name = "cssparser-macros" version = "0.6.1" @@ -1285,6 +1423,34 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + +[[package]] +name = "data-encoding" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" + +[[package]] +name = "data-url" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30bfce702bcfa94e906ef82421f2c0e61c076ad76030c16ee5d2e9a32fe193" +dependencies = [ + "matches", +] + [[package]] name = "data-url" version = "0.3.1" @@ -1306,7 +1472,7 @@ version = "0.99.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -1316,7 +1482,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "proc-macro2", "quote", @@ -1835,6 +2001,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2110,6 +2282,12 @@ dependencies = [ "weezl", ] +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + [[package]] name = "gio" version = "0.18.4" @@ -2201,7 +2379,7 @@ version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15f1ce686646e7f1e19bf7d5533fe443a45dbfb990e00629110797578b42fb19" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.3", "bstr", "log", "regex-automata", @@ -2275,7 +2453,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "anyhow", "as-raw-xcb-connection", @@ -2366,8 +2544,12 @@ dependencies = [ "anyhow", "chrono", "gpui", + "html5ever 0.27.0", "image", "itertools 0.13.0", + "markdown", + "markup5ever_rcdom", + "minify-html", "once_cell", "paste", "regex", @@ -2387,7 +2569,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "proc-macro2", "quote", @@ -2452,6 +2634,25 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "h2" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.7.1", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "half" version = "2.4.1" @@ -2467,6 +2668,19 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.11", + "bumpalo", +] [[package]] name = "hashbrown" @@ -2474,7 +2688,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -2557,12 +2771,26 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever", + "markup5ever 0.11.0", "proc-macro2", "quote", "syn 1.0.109", ] +[[package]] +name = "html5ever" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "http" version = "1.1.0" @@ -2574,10 +2802,33 @@ dependencies = [ "itoa 1.0.11", ] +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "anyhow", "bytes", @@ -2590,6 +2841,69 @@ dependencies = [ "url", ] +[[package]] +name = "httparse" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2d708df4e7140240a16cd6ab0ab65c972d7433ab77819ea693fde9c43811e2a" + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "itoa 1.0.11", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "socket2 0.5.8", + "tokio", + "tower-service", + "tracing", +] + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -2761,6 +3075,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "is-docker" version = "0.2.0" @@ -2780,6 +3100,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.11.0" @@ -2913,8 +3242,8 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8" dependencies = [ - "cssparser", - "html5ever", + "cssparser 0.27.2", + "html5ever 0.26.0", "indexmap 1.9.3", "matches", "selectors", @@ -2994,6 +3323,45 @@ version = "0.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64804cc6a5042d4f05379909ba25b503ec04e2c082151d62122d5dcaa274b961" +[[package]] +name = "lightningcss" +version = "1.0.0-alpha.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a75fcbcdbcc84fc1ae7c60c31f99337560b620757a9bfc1c9f84df3cff8ac24" +dependencies = [ + "ahash 0.8.11", + "bitflags 2.6.0", + "const-str", + "cssparser 0.33.0", + "cssparser-color", + "dashmap", + "data-encoding", + "getrandom 0.2.15", + "indexmap 2.7.1", + "itertools 0.10.5", + "lazy_static", + "lightningcss-derive", + "parcel_selectors", + "parcel_sourcemap", + "paste", + "pathdiff", + "rayon", + "serde", + "smallvec", +] + +[[package]] +name = "lightningcss-derive" +version = "1.0.0-alpha.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84c12744d1279367caed41739ef094c325d53fb0ffcd4f9b84a368796f870252" +dependencies = [ + "convert_case 0.6.0", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -3102,6 +3470,15 @@ dependencies = [ "libc", ] +[[package]] +name = "markdown" +version = "1.0.0-alpha.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790c11786cb51d02938e3eb38276575e1658b1dd8555875f5788a40670a33934" +dependencies = [ + "unicode-id", +] + [[package]] name = "markup5ever" version = "0.11.0" @@ -3116,6 +3493,32 @@ dependencies = [ "tendril", ] +[[package]] +name = "markup5ever" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +dependencies = [ + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "string_cache", + "string_cache_codegen", + "tendril", +] + +[[package]] +name = "markup5ever_rcdom" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" +dependencies = [ + "html5ever 0.27.0", + "markup5ever 0.12.1", + "tendril", + "xml5ever", +] + [[package]] name = "matches" version = "0.1.10" @@ -3144,7 +3547,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "anyhow", "bindgen", @@ -3194,6 +3597,53 @@ dependencies = [ "paste", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minify-html" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd4517942a8e7425c990b14977f86a63e4996eed7b15cfcca1540126ac5ff25" +dependencies = [ + "aho-corasick 0.7.20", + "lazy_static", + "lightningcss", + "memchr", + "minify-html-common", + "minify-js", + "once_cell", + "rustc-hash 1.1.0", +] + +[[package]] +name = "minify-html-common" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697a6b40dffdc5de10c0cbd709dc2bc2039cea9dab8aaa636eb9a49d6b411780" +dependencies = [ + "aho-corasick 0.7.20", + "itertools 0.12.1", + "lazy_static", + "memchr", + "rustc-hash 1.1.0", + "serde", + "serde_json", +] + +[[package]] +name = "minify-js" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22d6c512a82abddbbc13b70609cb2beff01be2c7afff534d6e5e1c85e438fc8b" +dependencies = [ + "lazy_static", + "parse-js", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3216,6 +3666,17 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" +[[package]] +name = "mio" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +dependencies = [ + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.52.0", +] + [[package]] name = "naga" version = "23.1.0" @@ -3454,7 +3915,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" dependencies = [ - "proc-macro-crate 1.3.1", + "proc-macro-crate 3.2.0", "proc-macro2", "quote", "syn 2.0.96", @@ -3687,6 +4148,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -3736,6 +4206,12 @@ dependencies = [ "pathdiff", ] +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + [[package]] name = "option-ext" version = "0.2.0" @@ -3752,6 +4228,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "outref" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f222829ae9293e33a9f5e9f440c6760a3d450a64affe1846486b140db81c1f4" + [[package]] name = "pango" version = "0.18.3" @@ -3777,6 +4259,36 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parcel_selectors" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dccbc6fb560df303a44e511618256029410efbc87779018f751ef12c488271fe" +dependencies = [ + "bitflags 2.6.0", + "cssparser 0.33.0", + "log", + "phf 0.11.3", + "phf_codegen 0.11.3", + "precomputed-hash", + "rustc-hash 2.1.0", + "smallvec", +] + +[[package]] +name = "parcel_sourcemap" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "485b74d7218068b2b7c0e3ff12fbc61ae11d57cb5d8224f525bd304c6be05bbb" +dependencies = [ + "base64-simd", + "data-url 0.1.1", + "rkyv", + "serde", + "serde_json", + "vlq", +] + [[package]] name = "parking" version = "2.2.1" @@ -3806,6 +4318,19 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "parse-js" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ec3b11d443640ec35165ee8f6f0559f1c6f41878d70330fe9187012b5935f02" +dependencies = [ + "aho-corasick 0.7.20", + "bumpalo", + "hashbrown 0.13.2", + "lazy_static", + "memchr", +] + [[package]] name = "paste" version = "1.0.15" @@ -3859,7 +4384,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" dependencies = [ - "phf_macros", + "phf_macros 0.8.0", "phf_shared 0.8.0", "proc-macro-hack", ] @@ -3873,6 +4398,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf_codegen" version = "0.8.0" @@ -3893,6 +4428,16 @@ dependencies = [ "phf_shared 0.10.0", ] +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + [[package]] name = "phf_generator" version = "0.8.0" @@ -3913,6 +4458,16 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand 0.8.5", +] + [[package]] name = "phf_macros" version = "0.8.0" @@ -3927,6 +4482,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "phf_shared" version = "0.8.0" @@ -3945,6 +4513,15 @@ dependencies = [ "siphasher 0.3.11", ] +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher 1.0.1", +] + [[package]] name = "pico-args" version = "0.5.0" @@ -4184,6 +4761,26 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "qoi" version = "0.4.1" @@ -4208,6 +4805,58 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e96808277ec6f97351a2380e6c25114bc9e67037775464979f3037c92d05ef" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.1.0", + "rustls", + "socket2 0.5.8", + "thiserror 2.0.11", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2fe5ef3495d7d2e377ff17b1a8ce2ee2ec2a18cde8b6ad6619d65d0701c135d" +dependencies = [ + "bytes", + "getrandom 0.2.15", + "rand 0.8.5", + "ring", + "rustc-hash 2.1.0", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.11", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46f3055866785f6b92bc6164b76be02ca8f2eb4b002c0354b28cf4c119e5944" +dependencies = [ + "cfg_aliases 0.2.1", + "libc", + "once_cell", + "socket2 0.5.8", + "tracing", + "windows-sys 0.59.0", +] + [[package]] name = "quote" version = "1.0.37" @@ -4217,6 +4866,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.7.3" @@ -4424,7 +5079,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "derive_refineable", ] @@ -4435,7 +5090,7 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.3", "memchr", "regex-automata", "regex-syntax", @@ -4447,7 +5102,7 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ - "aho-corasick", + "aho-corasick 1.1.3", "memchr", "regex-syntax", ] @@ -4458,6 +5113,78 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.12.8" +source = "git+https://github.com/zed-industries/reqwest.git?rev=fd110f6998da16bbca97b6dddda9be7827c50e29#fd110f6998da16bbca97b6dddda9be7827c50e29" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-socks", + "tokio-util", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "windows-registry", +] + +[[package]] +name = "reqwest_client" +version = "0.1.0" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" +dependencies = [ + "anyhow", + "bytes", + "futures", + "http_client", + "log", + "reqwest", + "serde", + "smol 2.0.2", + "tokio", +] + [[package]] name = "resvg" version = "0.44.0" @@ -4481,6 +5208,49 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "ring" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.15", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "roxmltree" version = "0.20.0" @@ -4576,6 +5346,12 @@ dependencies = [ "triomphe", ] +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + [[package]] name = "rustc-hash" version = "1.1.0" @@ -4624,6 +5400,61 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" +dependencies = [ + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" +dependencies = [ + "web-time", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.18" @@ -4680,6 +5511,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "schemars" version = "0.8.21" @@ -4723,6 +5563,29 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "security-framework" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271720403f46ca04f7ba6f55d438f8bd878d6b8ca0a1046e8228c4145bcbb316" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "selectors" version = "0.22.0" @@ -4730,7 +5593,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df320f1889ac4ba6bc0cdc9c9af7af4bd64bb927bccdf32d81140dc1f9be12fe" dependencies = [ "bitflags 1.3.2", - "cssparser", + "cssparser 0.27.2", "derive_more", "fxhash", "log", @@ -4752,7 +5615,7 @@ checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" [[package]] name = "semantic_version" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "anyhow", "serde", @@ -4837,6 +5700,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.11", + "ryu", + "serde", +] + [[package]] name = "serde_yml" version = "0.0.11" @@ -4907,6 +5782,15 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-abstraction" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadb29c57caadc51ff8346233b5cec1d240b68ce55cf1afc764818791876987" +dependencies = [ + "outref", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -4922,6 +5806,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simplecss" version = "0.2.1" @@ -5042,6 +5932,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "soup3" version = "0.5.0" @@ -5111,6 +6011,7 @@ dependencies = [ "rand 0.8.5", "raw-window-handle", "regex", + "reqwest_client", "rust-embed", "serde", "serde_json", @@ -5192,7 +6093,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "arrayvec", "log", @@ -5326,6 +6227,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + [[package]] name = "sys-locale" version = "0.3.1" @@ -5335,6 +6245,27 @@ dependencies = [ "libc", ] +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "system-deps" version = "6.2.2" @@ -5378,6 +6309,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "target-lexicon" version = "0.12.16" @@ -5555,6 +6492,56 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +[[package]] +name = "tokio" +version = "1.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2 0.5.8", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-socks" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" +dependencies = [ + "either", + "futures-util", + "thiserror 1.0.66", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fcaa8d55a2bdd6b83ace262b016eca0d79ee02818c5c1bcdf0305114081078" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.7.8" @@ -5625,6 +6612,12 @@ dependencies = [ "winnow 0.6.20", ] +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.40" @@ -5667,6 +6660,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "ttf-parser" version = "0.21.1" @@ -5741,6 +6740,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "260bc6647b3893a9a90668360803a15f96b85a5257b1c3a0c3daf6ae2496de42" +[[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.13" @@ -5804,6 +6809,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.2" @@ -5823,7 +6834,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7447e703d7223b067607655e625e0dbca80822880248937da65966194c4864e6" dependencies = [ "base64", - "data-url", + "data-url 0.3.1", "flate2", "fontdb 0.22.0", "imagesize", @@ -5852,7 +6863,7 @@ checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/huacnlee/zed.git?branch=webview#7c1a1305091a5b2463ca9779dcf2b97a0ce52f22" +source = "git+https://github.com/huacnlee/zed.git?branch=webview#ca79918115468f492adb689f9e8030f0037821a5" dependencies = [ "anyhow", "async-fs 2.1.2", @@ -5945,6 +6956,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vlq" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" + [[package]] name = "vswhom" version = "0.1.0" @@ -5981,6 +6998,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.9.0+wasi-snapshot-preview1" @@ -6019,6 +7045,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.95" @@ -6048,6 +7086,19 @@ version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wayland-backend" version = "0.3.7" @@ -6143,6 +7194,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.1" @@ -6314,6 +7375,17 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -6607,7 +7679,7 @@ dependencies = [ "dunce", "gdkx11", "gtk", - "html5ever", + "html5ever 0.26.0", "http", "javascriptcore-rs", "jni", @@ -6636,6 +7708,15 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.21.0" @@ -6707,7 +7788,7 @@ name = "xim" version = "0.4.0" source = "git+https://github.com/XDeme1/xim-rs?rev=d50d461764c2213655cd9cf65a0ea94c70d3c4fd#d50d461764c2213655cd9cf65a0ea94c70d3c4fd" dependencies = [ - "ahash", + "ahash 0.8.11", "hashbrown 0.14.5", "log", "x11rb", @@ -6748,6 +7829,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" +[[package]] +name = "xml5ever" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" +dependencies = [ + "log", + "mac", + "markup5ever 0.12.1", +] + [[package]] name = "xmlwriter" version = "0.1.0" diff --git a/README.md b/README.md index fe806c21..c4fbb60c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ UI components for building fantastic desktop application by using [GPUI](https:/ - Sidebar - Breadcrumb - Badge +- TextView (Markdown, Simple HTML) to native rendering. ## Showcase diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml index 1c1d6070..a5be1abe 100644 --- a/crates/story/Cargo.toml +++ b/crates/story/Cargo.toml @@ -11,6 +11,7 @@ chrono = "0.4" fake = { version = "2.10.0", features = ["dummy"] } gpui.workspace = true gpui-component.workspace = true +reqwest_client = { git = "https://github.com/huacnlee/zed.git", branch = "webview" } rand = "0.8" regex = "1" rust-embed.workspace = true diff --git a/crates/story/examples/html.html b/crates/story/examples/html.html new file mode 100644 index 00000000..c04033fb --- /dev/null +++ b/crates/story/examples/html.html @@ -0,0 +1,102 @@ +
+

A simple HTML document

+
+ Here is a test in div. +

+ This is a paragraph inside a div element, have + @Mention Tag + Link with: Bold italic, bold, italic, and + code text. +

+
+

This is second paragraph.

+
+ A text after div. +

+ 这是一个中文演示段落,用于展示更多的 + Markdown GFM + 内容。これは日本語のデモ段落です。の多言語サポートを示すためのテキストが含まれています。 +

+
+ +
+

List

+ Example for Bulleted and Numbered List: + +

Numbered List

+ Text before the Numbered List. +
    +
  1. + Numbered item 1 +
      +
    1. Sub item 1
    2. +
    3. Sub item 2
    4. +
    +
  2. +
  3. Numbered item 2
  4. +
  5. Numbered item 3
  6. +
+ Text after the Numbered List. +

Bulleted List

+ Text before the Bulleted List. +
    +
  • + Bullet 1 +
      +
    1. Sub Numbered 1
    2. +
    3. Sub Numbered 2
    4. +
    +
  • +
  • Bullet 2
  • +
+ Text after the Bulleted List. +
+ Text before the section. +
+

Table

+ Text before the table. + + + + + + + + + + + + + + + + + +
Head 1Head 2
Cell 1Cell 2
Cell 3Cell 4
+ Text after the table. +
+ Text after the section. +
+

Images

+ Rust + Text before the image. + Rust + Text after the image. + Rust +
+
diff --git a/crates/story/examples/html.rs b/crates/story/examples/html.rs new file mode 100644 index 00000000..6a45ffff --- /dev/null +++ b/crates/story/examples/html.rs @@ -0,0 +1,87 @@ +use gpui::*; +use gpui_component::{input::TextInput, text::TextView, ActiveTheme as _}; +use story::Assets; + +pub struct Example { + text_input: Entity, + text_view: Entity, + _subscribe: Subscription, +} + +const EXAMPLE: &str = include_str!("./html.html"); + +impl Example { + pub fn new(window: &mut Window, cx: &mut Context) -> Self { + let text_input = cx.new(|cx| { + TextInput::new(window, cx) + .multi_line() + .rows(50) + .placeholder("Input your HTML here...") + }); + let text_view = cx.new(|cx| TextView::html(EXAMPLE, cx)); + + let _subscribe = cx.subscribe( + &text_input, + |this, _, _: &gpui_component::input::InputEvent, cx| { + let new_text = this.text_input.read(cx).text(); + this.text_view.update(cx, |view, cx| { + view.set_text(new_text, cx); + }); + }, + ); + + _ = text_input.update(cx, |input, cx| { + input.set_text(EXAMPLE, window, cx); + }); + + Self { + text_input, + text_view, + _subscribe, + } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_row() + .h_full() + .child( + div() + .id("source") + .h_full() + .w_1_2() + .border_r_1() + .border_color(cx.theme().border) + .flex_1() + .child(self.text_input.clone()), + ) + .child( + div() + .id("preview") + .h_full() + .w_1_2() + .p_5() + .flex_1() + .overflow_y_scroll() + .child(self.text_view.clone()), + ) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("HTML Example", Example::view, cx); + }); +} diff --git a/crates/story/examples/markdown.md b/crates/story/examples/markdown.md new file mode 100644 index 00000000..2522e787 --- /dev/null +++ b/crates/story/examples/markdown.md @@ -0,0 +1,143 @@ +# Hello, **World**! + +This is first paragraph, there have **BOLD**, _italic_, and ~strikethrough~, `code` text. + +> Blockquote: More complex nested inline style like **bold: _italic_**. +> This is second paragraph, it includes a block quote. + +This is an additional demonstration paragraph in English demonstrating more content for [Markdown GFM](https://github.github.com/gfm/). It includes various stylistic elements and plain text. + +--- + +这是一个中文演示段落,用于展示更多的 [Markdown GFM](https://github.github.com/gfm/) 内容。您可以在此尝试使用使用**粗体**、*斜体*和`代码`等样式。これは日本語のデモ段落です。Markdown の多言語サポートを示すためのテキストが含まれています。例えば、、**ボールド**、_イタリック_、および`コード`のスタイルなどを試すことができます。 + +## Heading for [Links](https://www.google.com) + +Here is a link to [Google](https://www.google.com), and another to [Rust](https://www.rust-lang.org). + +### Images + +![Rust](https://www.rust-lang.org/logos/rust-logo-blk.svg) + +### HTML + +#### Paragraph and Text + +
+ Here is a test in div. +

This is a paragraph inside a div element, have link, bold, italic, and code text.

+
+

This is second paragraph.

+
+ A text after div. +
+ +#### List + +
    +
  1. Numbered item 1
  2. +
  3. Numbered item 2
  4. +
+ +
    +
  • Bullet 1
  • +
  • Bullet 2
  • +
+ +#### Table + + + + + + + + + + + + + + + + + + +
Head 1Head 2
Cell 1Cell 2
Cell 3Cell 4
+ +#### Image + +Rust +Rust +Rust + +### Table + +| Header 1 | Header 2 | Header 3 | Header 4 | +| -------- | -------- | ------------------------------------ | -------- | +| Cell 0 | Cell 1 | This is a long cell with line break. | Cell 3 | +| Row 2 | Row 2 | Row 2
[Link](https://github.com) | Row 2 | +| Row 3 | **Bold** | Row 3 | Row 3 | + +#### Lists + +##### Bulleted List + +- Bullet 1 +- Bullet 2 + - Bullet 2.1 + - Bullet 2.1.1 + - Bullet 2.1.1.1 + - Bullet 2.1.2 + - Bullet 2.2 +- Bullet 3 + +##### Numbered List + +1. Numbered item 1 + 1. Numbered item 1.1 + 1. Numbered item 1.1.1 + 1. Numbered item 1.2 +2. Numbered item 2 +3. Numbered item 3 + +##### To-Do List + +- [x] Task 1 +- [ ] Task 2 +- [ ] Task 3 + +#### Heading for Code + +```rust +fn main() { + println!("Hello, World!"); +} +``` + +## Unsupported + +### HTML + +
+Click to expand +
+

This is a paragraph inside a details element.

+

This is second paragraph.

+
+
+ +### Math + +This is an inline math $x^2 + y^2 = z^2$. + +This is a block math: + +$$ +\begin{aligned} +x^2 + y^2 &= z^2 \\ +x^3 + y^3 &= z^3 +\end{aligned} +$$ + +This is final paragraph, it includes a code block and a list of items. diff --git a/crates/story/examples/markdown.rs b/crates/story/examples/markdown.rs new file mode 100644 index 00000000..e0838084 --- /dev/null +++ b/crates/story/examples/markdown.rs @@ -0,0 +1,63 @@ +use gpui::*; +use gpui_component::{text::TextView, ActiveTheme as _}; +use story::Assets; + +pub struct Example { + text_view: Entity, +} + +const EXAMPLE: &str = include_str!("./markdown.md"); + +impl Example { + pub fn new(_: &mut Window, cx: &mut Context) -> Self { + let text_view = cx.new(|cx| TextView::markdown(EXAMPLE, cx)); + + Self { text_view } + } + + fn view(window: &mut Window, cx: &mut App) -> Entity { + cx.new(|cx| Self::new(window, cx)) + } +} + +impl Render for Example { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .flex() + .flex_row() + .h_full() + .child( + div() + .id("source") + .h_full() + .w_1_2() + .border_r_1() + .border_color(cx.theme().border) + .flex_1() + .p_5() + .overflow_y_scroll() + .child(EXAMPLE), + ) + .child( + div() + .id("preview") + .h_full() + .w_1_2() + .p_5() + .flex_1() + .overflow_y_scroll() + .child(self.text_view.clone()), + ) + } +} + +fn main() { + let app = Application::new().with_assets(Assets); + + app.run(move |cx| { + story::init(cx); + cx.activate(true); + + story::create_new_window("Markdown Example", Example::view, cx); + }); +} diff --git a/crates/story/src/lib.rs b/crates/story/src/lib.rs index 6946315a..c13f562f 100644 --- a/crates/story/src/lib.rs +++ b/crates/story/src/lib.rs @@ -201,6 +201,11 @@ pub fn init(cx: &mut App) { dropdown_story::init(cx); popup_story::init(cx); + let http_client = std::sync::Arc::new( + reqwest_client::ReqwestClient::user_agent("gpui-component/story").unwrap(), + ); + cx.set_http_client(http_client); + register_panel(cx, PANEL_NAME, |_, _, info, window, cx| { let story_state = match info { PanelInfo::Panel(value) => StoryState::from_value(value.clone()), diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 86418445..4f2ad7e0 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -36,6 +36,12 @@ usvg = { version = "0.44.0", default-features = false, features = [ ] } uuid = "1.10" wry = "0.48.0" +# Markdown Parser +markdown = "1.0.0-alpha.22" +# HTML Parser +html5ever = "0.27" +markup5ever_rcdom = "0.3.0" +minify-html = "0.15.0" # Calendar chrono = "0.4.38" diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 5ad14510..66f8d918 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -45,6 +45,7 @@ pub mod switch; pub mod tab; pub mod table; pub mod tag; +pub mod text; pub mod theme; pub mod tooltip; pub mod webview; diff --git a/crates/ui/src/text/element.rs b/crates/ui/src/text/element.rs new file mode 100644 index 00000000..ed6520aa --- /dev/null +++ b/crates/ui/src/text/element.rs @@ -0,0 +1,583 @@ +use std::ops::Range; + +use gpui::{ + div, img, prelude::FluentBuilder as _, px, relative, rems, App, DefiniteLength, ElementId, + FontStyle, FontWeight, Half, HighlightStyle, InteractiveElement as _, InteractiveText, + IntoElement, Length, ObjectFit, ParentElement, RenderOnce, SharedString, SharedUri, Styled, + StyledImage as _, StyledText, Window, +}; + +use crate::{h_flex, v_flex, ActiveTheme as _, Icon, IconName}; + +use super::utils::list_item_prefix; + +#[allow(unused)] +#[derive(Debug, Default, Clone, PartialEq)] +pub struct LinkMark { + pub url: SharedString, + pub title: Option, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct InlineTextStyle { + pub bold: bool, + pub italic: bool, + pub strikethrough: bool, + pub code: bool, + pub link: Option, +} + +#[derive(Debug, Default, Copy, Clone, PartialEq)] +pub struct Span { + pub start: usize, + pub end: usize, +} + +impl From for ElementId { + fn from(value: Span) -> Self { + ElementId::Name(format!("md-{}:{}", value.start, value.end).into()) + } +} + +#[allow(unused)] +#[derive(Debug, Default, Clone)] +pub struct ImageNode { + pub url: SharedUri, + pub title: Option, + pub alt: Option, + pub width: Option, + pub height: Option, +} + +impl PartialEq for ImageNode { + fn eq(&self, other: &Self) -> bool { + self.url == other.url && self.title == other.title && self.alt == other.alt + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct TextNode { + /// The text content. + pub text: String, + /// The text styles, each tuple contains the range of the text and the style. + pub marks: Vec<(Range, InlineTextStyle)>, +} + +#[derive(Debug, Clone, PartialEq, IntoElement)] +pub enum Paragraph { + Texts { + span: Option, + children: Vec, + }, + Image { + span: Option, + image: ImageNode, + }, +} + +impl Default for Paragraph { + fn default() -> Self { + Self::Texts { + span: None, + children: vec![], + } + } +} + +impl From for Paragraph { + fn from(value: String) -> Self { + Self::Texts { + span: None, + children: vec![TextNode { + text: value.clone(), + marks: vec![], + }], + } + } +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Table { + pub children: Vec, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct TableRow { + pub children: Vec, +} + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct TableCell { + pub children: Paragraph, + pub width: Option, +} + +impl Paragraph { + pub fn clear(&mut self) { + match self { + Self::Texts { children, .. } => children.clear(), + Self::Image { .. } => *self = Self::default(), + } + } + + pub fn is_image(&self) -> bool { + matches!(self, Self::Image { .. }) + } + + pub fn set_span(&mut self, span: Span) { + match self { + Self::Texts { span: s, .. } => *s = Some(span), + Self::Image { span: s, .. } => *s = Some(span), + } + } + + pub fn push_str(&mut self, text: &str) { + if let Self::Texts { children, .. } = self { + children.push(TextNode { + text: text.to_string(), + marks: vec![(0..text.len(), InlineTextStyle::default())], + }); + } + } + + pub fn push(&mut self, text: TextNode) { + if let Self::Texts { children, .. } = self { + children.push(text); + } + } + + pub fn set_image(&mut self, image: ImageNode) { + *self = Self::Image { span: None, image }; + } + + pub fn is_empty(&self) -> bool { + match self { + Self::Texts { .. } => self.text_len() == 0, + Self::Image { .. } => false, + } + } + + /// Return length of children text. + pub fn text_len(&self) -> usize { + match self { + Self::Texts { children, .. } => { + let mut len = 0; + for text_node in children.iter() { + len = text_node.text.len().max(len); + } + len + } + Self::Image { .. } => 1, + } + } +} + +#[allow(unused)] +#[derive(Debug, Clone, IntoElement, PartialEq)] +pub enum Node { + Root { + children: Vec, + }, + Paragraph(Paragraph), + Heading { + level: u8, + children: Paragraph, + }, + Blockquote(Paragraph), + List { + /// Only contains ListItem, others will be ignored + children: Vec, + ordered: bool, + }, + ListItem { + children: Vec, + spread: bool, + /// Whether the list item is checked, if None, it's not a checkbox + checked: Option, + }, + CodeBlock { + code: SharedString, + lang: Option, + }, + Table(Table), + //
+ Break, + Divider, + Ignore, + Unknown, +} + +impl Node { + fn is_ignore(&self) -> bool { + matches!(self, Self::Ignore) + } + + fn is_list_item(&self) -> bool { + matches!(self, Self::ListItem { .. }) + } + + /// Combine all children, omitting the empt parent nodes. + pub(super) fn compact(&self) -> Node { + match self { + Self::Root { children } => { + let children = children + .iter() + .map(|c| c.compact()) + .filter(|c| !c.is_ignore()) + .collect::>(); + if children.len() == 1 { + children.first().unwrap().compact() + } else { + self.clone() + } + } + _ => self.clone(), + } + } +} + +impl RenderOnce for Paragraph { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + match self { + Self::Texts { span, children } => { + let mut text = String::new(); + let mut highlights: Vec<(Range, HighlightStyle)> = vec![]; + let mut links: Vec<(Range, LinkMark)> = vec![]; + let mut offset = 0; + + for text_node in children.into_iter() { + let text_len = text_node.text.len(); + text.push_str(&text_node.text); + + let mut node_highlights = vec![]; + for (range, style) in text_node.marks { + let inner_range = (offset + range.start)..(offset + range.end); + + let mut highlight = HighlightStyle::default(); + if style.bold { + highlight.font_weight = Some(FontWeight::BOLD); + } + if style.italic { + highlight.font_style = Some(FontStyle::Italic); + } + if style.strikethrough { + highlight.strikethrough = Some(gpui::StrikethroughStyle { + thickness: gpui::px(1.), + ..Default::default() + }); + } + if style.code { + highlight.background_color = Some(cx.theme().accent); + } + + if let Some(link_mark) = style.link { + highlight.color = Some(cx.theme().link); + highlight.underline = Some(gpui::UnderlineStyle { + thickness: gpui::px(1.), + ..Default::default() + }); + + links.push((inner_range.clone(), link_mark)); + } + + node_highlights.push((inner_range, highlight)); + } + + highlights = gpui::combine_highlights(highlights, node_highlights).collect(); + + offset += text_len; + } + + let text_style = window.text_style(); + let element_id: ElementId = span.unwrap_or_default().into(); + let styled_text = StyledText::new(text).with_highlights(&text_style, highlights); + let link_ranges = links + .iter() + .map(|(range, _)| range.clone()) + .collect::>(); + + InteractiveText::new(element_id, styled_text) + .on_click(link_ranges, { + let links = links.clone(); + move |ix, _, cx| { + if let Some((_, link)) = &links.get(ix) { + cx.open_url(&link.url); + } + } + }) + .into_any_element() + } + Self::Image { image, .. } => img(image.url) + .object_fit(ObjectFit::Contain) + .max_w(relative(1.)) + .when_some(image.width, |this, width| this.w(width)) + .into_any_element(), + } + } +} + +#[derive(Default)] +struct ListState { + todo: bool, + ordered: bool, + depth: usize, +} + +impl Node { + fn render_list_item( + item: Node, + ix: usize, + state: ListState, + window: &mut Window, + cx: &mut App, + ) -> impl IntoElement { + match item { + Node::ListItem { + children, + spread, + checked, + } => v_flex() + .when(spread, |this| this.child(div())) + .children({ + let mut items = Vec::with_capacity(children.len()); + for child in children.into_iter() { + match &child { + Node::Paragraph(_) => { + items.push( + h_flex() + .items_center() + .when(!state.todo && checked.is_none(), |this| { + this.child(list_item_prefix( + ix, + state.ordered, + state.depth, + )) + }) + .when_some(checked, |this, checked| { + this.child( + div() + .flex() + .mr_1p5() + .size(rems(0.875)) + .items_center() + .justify_center() + .rounded(cx.theme().radius.half()) + .bg(cx.theme().primary) + .text_color(cx.theme().primary_foreground) + .when(checked, |this| { + this.child( + Icon::new(IconName::Check) + .size_2() + .text_xs(), + ) + }), + ) + }) + .child(child.render_node( + Some(ListState { + depth: state.depth + 1, + ordered: state.ordered, + todo: checked.is_some(), + }), + window, + cx, + )), + ); + } + Node::List { .. } => { + items.push(div().ml(rems(1.)).child(child.render_node( + Some(ListState { + depth: state.depth + 1, + ordered: state.ordered, + todo: checked.is_some(), + }), + window, + cx, + ))) + } + _ => {} + } + } + items + }) + .into_any_element(), + _ => div().into_any_element(), + } + } + + fn render_table(item: &Node, _: &mut Window, cx: &mut App) -> impl IntoElement { + const DEFAULT_LENGTH: usize = 5; + const MAX_LENGTH: usize = 150; + let col_lens = match item { + Node::Table(table) => { + let mut col_lens = vec![]; + for row in table.children.iter() { + for (ix, cell) in row.children.iter().enumerate() { + if col_lens.len() <= ix { + col_lens.push(DEFAULT_LENGTH); + } + + let len = cell.children.text_len(); + if len > col_lens[ix] { + col_lens[ix] = len; + } + } + } + col_lens + } + _ => vec![], + }; + + match item { + Node::Table(table) => div() + .id("table") + .mb(rems(1.)) + .w_full() + .border_1() + .border_color(cx.theme().border) + .rounded(cx.theme().radius) + .children({ + let mut rows = Vec::with_capacity(table.children.len()); + for (row_ix, row) in table.children.iter().enumerate() { + rows.push( + div() + .id("row") + .w_full() + .when(row_ix < table.children.len() - 1, |this| this.border_b_1()) + .border_color(cx.theme().border) + .flex() + .flex_row() + .children({ + let mut cells = Vec::with_capacity(row.children.len()); + for (ix, cell) in row.children.iter().enumerate() { + let len = col_lens + .get(ix) + .copied() + .unwrap_or(MAX_LENGTH) + .min(MAX_LENGTH); + + cells.push( + div() + .id("cell") + .w(Length::Definite(relative(len as f32))) + .px_2() + .py_1() + .truncate() + .child(cell.children.clone()), + ) + } + cells + }), + ) + } + rows + }) + .into_any_element(), + _ => div().into_any_element(), + } + } + + fn render_node( + self, + list_state: Option, + window: &mut Window, + cx: &mut App, + ) -> impl IntoElement { + let in_list = list_state.is_some(); + let mb = if in_list { rems(0.0) } else { rems(1.) }; + + match self { + Node::Root { children } => div().children(children).into_any_element(), + Node::Paragraph(paragraph) => div().mb(mb).child(paragraph).into_any_element(), + Node::Heading { level, children } => { + let (text_size, font_weight) = match level { + 1 => (rems(2.), FontWeight::BOLD), + 2 => (rems(1.5), FontWeight::SEMIBOLD), + 3 => (rems(1.25), FontWeight::SEMIBOLD), + 4 => (rems(1.125), FontWeight::SEMIBOLD), + 5 => (rems(1.), FontWeight::SEMIBOLD), + 6 => (rems(1.), FontWeight::MEDIUM), + _ => (rems(1.), FontWeight::NORMAL), + }; + + h_flex() + .mb(rems(0.5)) + .whitespace_normal() + .text_size(text_size) + .font_weight(font_weight) + .child(children) + .into_any_element() + } + Node::Blockquote(children) => div() + .w_full() + .mb(mb) + .text_color(cx.theme().muted_foreground) + .border_l_3() + .border_color(cx.theme().secondary_active) + .px_4() + .py_1() + .child(children) + .into_any_element(), + Node::List { children, ordered } => v_flex() + .mb(mb) + .children({ + let mut items = Vec::with_capacity(children.len()); + let list_state = list_state.unwrap_or_default(); + let mut ix = 0; + for item in children.into_iter() { + let is_item = item.is_list_item(); + + items.push(Self::render_list_item( + item, + ix, + ListState { + ordered, + todo: list_state.todo, + depth: list_state.depth, + }, + window, + cx, + )); + + if is_item { + ix += 1; + } + } + items + }) + .into_any_element(), + Node::CodeBlock { code, .. } => div() + .mb(mb) + .rounded(cx.theme().radius) + .bg(cx.theme().secondary) + .p_3() + .text_size(rems(0.875)) + .relative() + .child(code) + .into_any_element(), + Node::Table { .. } => Self::render_table(&self, window, cx).into_any_element(), + Node::Divider => div() + .bg(cx.theme().border) + .h(px(2.)) + .mb(mb) + .into_any_element(), + Node::Break => div().into_any_element(), + Node::Ignore => div().into_any_element(), + _ => { + if cfg!(debug_assertions) { + eprintln!("Unknown implementation: {:?}", self); + } + + div().into_any_element() + } + } + } +} + +/// Ref: +/// https://ui.shadcn.com/docs/components/typography +impl RenderOnce for Node { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { + self.render_node(None, window, cx) + } +} diff --git a/crates/ui/src/text/html.rs b/crates/ui/src/text/html.rs new file mode 100644 index 00000000..9278b915 --- /dev/null +++ b/crates/ui/src/text/html.rs @@ -0,0 +1,726 @@ +extern crate markup5ever_rcdom as rcdom; + +use std::cell::RefCell; +use std::collections::HashMap; +use std::ops::Range; +use std::rc::Rc; + +use gpui::{ + div, px, relative, Context, DefiniteLength, IntoElement, ParentElement as _, Render, + SharedString, +}; +use html5ever::tendril::TendrilSink; +use html5ever::{local_name, parse_document, LocalName, ParseOpts}; +use markup5ever_rcdom::{Node, NodeData, RcDom}; + +use super::element::{ + self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Table, TableRow, TextNode, +}; + +const BLOCK_ELEMENTS: [&str; 33] = [ + "html", + "body", + "head", + "address", + "article", + "aside", + "blockquote", + "details", + "summary", + "dialog", + "div", + "dl", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "header", + "hr", + "main", + "nav", + "ol", + "p", + "pre", + "section", + "table", + "ul", +]; + +pub(super) fn parse_html(source: &str) -> Result { + let opts = ParseOpts { + ..Default::default() + }; + + let bytes = minify_html::minify(source.as_bytes(), &minify_html::Cfg::default()); + let mut cursor = std::io::Cursor::new(bytes); + // Ref + // https://github.com/servo/html5ever/blob/main/rcdom/examples/print-rcdom.rs + let dom = parse_document(RcDom::default(), opts) + .from_utf8() + .read_from(&mut cursor)?; + + let mut paragraph = Paragraph::default(); + // NOTE: The outer paragraph is not used. + let node: element::Node = parse_node(&dom.document, &mut paragraph); + let node = node.compact(); + + Ok(node) +} + +pub struct HtmlView { + text: SharedString, + parsed: bool, + node: Option, +} + +impl HtmlView { + pub fn new(raw: impl Into) -> Self { + Self { + text: raw.into(), + parsed: false, + node: None, + } + } + + pub fn set_text(&mut self, raw: impl Into, cx: &mut Context) { + self.text = raw.into(); + self.parsed = false; + self.node = None; + cx.notify(); + } + + fn parse_if_needed(&mut self) { + if !self.parsed { + self.node = parse_html(&self.text).ok(); + self.parsed = true; + } + } +} + +impl Render for HtmlView { + fn render(&mut self, _: &mut gpui::Window, _: &mut Context<'_, Self>) -> impl IntoElement { + self.parse_if_needed(); + + if let Some(node) = &self.node { + div().child(node.clone()) + } else { + div() + } + } +} + +fn attr_value(attrs: &RefCell>, name: LocalName) -> Option { + attrs.borrow().iter().find_map(|attr| { + if attr.name.local == name { + Some(attr.value.to_string()) + } else { + None + } + }) +} + +/// Get style properties to HashMap +/// TODO: Use cssparser to parse style attribute. +fn style_attrs(attrs: &RefCell>) -> HashMap { + let mut styles = HashMap::new(); + let Some(css_text) = attr_value(attrs, local_name!("style")) else { + return styles; + }; + + for decl in css_text.split(';') { + for rule in decl.split(':') { + let mut parts = rule.splitn(2, ':'); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + styles.insert( + key.trim().to_lowercase().to_string(), + value.trim().to_string(), + ); + } + } + } + + styles +} + +/// Parse length value from style attribute. +/// +/// When is percentage, it will be converted to relative length. +/// Else, it will be converted to pixels. +fn value_to_length(value: &str) -> Option { + if value.ends_with("px") { + value + .trim_end_matches("px") + .parse() + .ok() + .map(|v| px(v).into()) + } else if value.ends_with("%") { + value + .trim_end_matches("%") + .parse::() + .ok() + .map(|v| relative(v / 100.)) + } else { + value + .trim_end_matches("px") + .parse() + .ok() + .map(|v| px(v).into()) + } +} + +/// Get width, height from attributes or parse them from style attribute. +fn attr_width_height( + attrs: &RefCell>, +) -> (Option, Option) { + let mut width = None; + let mut height = None; + + if let Some(value) = attr_value(attrs, local_name!("width")) { + width = value_to_length(&value); + } + + if let Some(value) = attr_value(attrs, local_name!("height")) { + height = value_to_length(&value); + } + + if width.is_none() || height.is_none() { + let styles = style_attrs(attrs); + if width.is_none() { + width = styles.get("width").and_then(|v| value_to_length(&v)); + } + if height.is_none() { + height = styles.get("height").and_then(|v| value_to_length(&v)); + } + } + + (width, height) +} + +fn parse_table_row(table: &mut Table, node: &Rc) { + let mut row = TableRow::default(); + let mut count = 0; + for child in node.children.borrow().iter() { + match child.data { + NodeData::Element { + ref name, + ref attrs, + .. + } if name.local == local_name!("td") || name.local == local_name!("th") => { + if child.children.borrow().is_empty() { + continue; + } + + count += 1; + parse_table_cell(&mut row, child, attrs); + } + _ => {} + } + } + + if count > 0 { + table.children.push(row); + } +} + +fn parse_table_cell( + row: &mut element::TableRow, + node: &Rc, + attrs: &RefCell>, +) { + let mut paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + parse_paragraph(&mut paragraph, child); + } + let width = attr_width_height(attrs).0; + let table_cell = element::TableCell { + children: paragraph, + width, + }; + row.children.push(table_cell); +} + +/// Trim text but leave at least one space. +/// +/// - Before: " \r\n Hello world \t " +/// - After: " Hello world " +fn trim_text(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + + for (i, c) in text.chars().enumerate() { + if c.is_whitespace() { + if i > 0 && out.ends_with(' ') { + continue; + } + } + out.push(c); + } + + out +} + +fn parse_paragraph( + paragraph: &mut Paragraph, + node: &Rc, +) -> (String, Vec<(Range, InlineTextStyle)>) { + let mut text = String::new(); + let mut marks = vec![]; + + /// Append new_text and new_marks to text and marks. + fn merge_child_text( + text: &mut String, + marks: &mut Vec<(Range, InlineTextStyle)>, + new_text: &str, + new_marks: &[(Range, InlineTextStyle)], + ) { + let offset = text.len(); + text.push_str(new_text); + for (range, style) in new_marks { + marks.push((range.start + offset..new_text.len() + offset, style.clone())); + } + } + + match &node.data { + NodeData::Text { ref contents } => { + let part = trim_text(&contents.borrow()); + text.push_str(&part); + paragraph.push_str(&text); + } + NodeData::Element { name, attrs, .. } => match name.local { + local_name!("em") | local_name!("i") => { + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + marks.push(( + 0..text.len(), + InlineTextStyle { + italic: true, + ..Default::default() + }, + )); + paragraph.push(element::TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + local_name!("strong") | local_name!("b") => { + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + + marks.push(( + 0..text.len(), + InlineTextStyle { + bold: true, + ..Default::default() + }, + )); + paragraph.push(TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + local_name!("del") | local_name!("s") => { + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + marks.push(( + 0..text.len(), + InlineTextStyle { + strikethrough: true, + ..Default::default() + }, + )); + paragraph.push(TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + local_name!("code") => { + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + marks.push(( + 0..text.len(), + InlineTextStyle { + code: true, + ..Default::default() + }, + )); + paragraph.push(TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + local_name!("a") => { + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + + marks.push(( + 0..text.len(), + InlineTextStyle { + link: Some(LinkMark { + url: attr_value(&attrs, local_name!("href")).unwrap().into(), + title: attr_value(&attrs, local_name!("title")).map(Into::into), + }), + ..Default::default() + }, + )); + paragraph.push(TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + local_name!("img") => { + let Some(src) = attr_value(attrs, local_name!("src")) else { + if cfg!(debug_assertions) { + eprintln!("[html] Image node missing src attribute"); + } + return (text, marks); + }; + + let alt = attr_value(attrs, local_name!("alt")); + let title = attr_value(attrs, local_name!("title")); + let (width, height) = attr_width_height(attrs); + + paragraph.set_image(ImageNode { + url: src.into(), + alt: alt.map(Into::into), + width, + height, + title: title.map(Into::into), + }); + } + _ => { + // All unknown tags to as text + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + paragraph.push(element::TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + }, + _ => { + let mut child_paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + let (child_text, child_marks) = parse_paragraph(&mut child_paragraph, &child); + merge_child_text(&mut text, &mut marks, &child_text, &child_marks); + } + paragraph.push(element::TextNode { + text: text.clone(), + marks: marks.clone(), + }); + } + } + + (text, marks) +} + +fn parse_node(node: &Rc, paragraph: &mut Paragraph) -> element::Node { + match node.data { + NodeData::Text { ref contents } => { + let text = contents.borrow().trim_start().to_string(); + if text.len() > 0 { + paragraph.push_str(&text); + } + + element::Node::Ignore + } + NodeData::Element { + ref name, + ref attrs, + .. + } => match name.local { + local_name!("br") => element::Node::Break, + local_name!("h1") + | local_name!("h2") + | local_name!("h3") + | local_name!("h4") + | local_name!("h5") + | local_name!("h6") => { + let mut children = vec![]; + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + let level = name + .local + .chars() + .last() + .unwrap_or('6') + .to_digit(10) + .unwrap_or(6) as u8; + + let mut paragraph = Paragraph::default(); + for child in node.children.borrow().iter() { + parse_paragraph(&mut paragraph, child); + } + + let heading = element::Node::Heading { + level, + children: paragraph, + }; + if children.len() > 0 { + children.push(heading); + + element::Node::Root { children } + } else { + heading + } + } + local_name!("img") => { + let mut children = vec![]; + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + let Some(src) = attr_value(attrs, local_name!("src")) else { + if cfg!(debug_assertions) { + eprintln!("[html] Image node missing src attribute"); + } + return element::Node::Ignore; + }; + + let alt = attr_value(&attrs, local_name!("alt")); + let title = attr_value(&attrs, local_name!("title")); + let (width, height) = attr_width_height(&attrs); + + let image = Paragraph::Image { + span: None, + image: ImageNode { + url: src.into(), + title: title.map(Into::into), + alt: alt.map(Into::into), + width, + height, + }, + }; + + if children.len() > 0 { + children.push(element::Node::Paragraph(image)); + element::Node::Root { children } + } else { + element::Node::Paragraph(image) + } + } + local_name!("ul") | local_name!("ol") => { + let mut children = vec![]; + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + let ordered = name.local == local_name!("ol"); + + let mut list_children = vec![]; + for child in node.children.borrow().iter() { + let mut child_paragraph = Paragraph::default(); + list_children.push(parse_node(child, &mut child_paragraph)); + } + + let list = element::Node::List { + children: list_children, + ordered, + }; + if children.len() > 0 { + children.push(list); + element::Node::Root { children } + } else { + list + } + } + local_name!("li") => { + let mut children = vec![]; + for child in node.children.borrow().iter() { + let mut child_paragraph = Paragraph::default(); + children.push(parse_node(child, &mut child_paragraph)); + if child_paragraph.text_len() > 0 { + children.push(element::Node::Paragraph(child_paragraph.clone())); + child_paragraph.clear(); + } + } + + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + element::Node::ListItem { + children, + spread: false, + checked: None, + } + } + local_name!("table") => { + let mut children = vec![]; + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + let mut table = Table::default(); + for child in node.children.borrow().iter() { + match child.data { + NodeData::Element { ref name, .. } + if name.local == local_name!("tbody") + || name.local == local_name!("thead") => + { + for sub_child in child.children.borrow().iter() { + parse_table_row(&mut table, &sub_child); + } + } + _ => { + parse_table_row(&mut table, &child); + } + } + } + + let table = element::Node::Table(table); + if children.len() > 0 { + children.push(table); + element::Node::Root { children } + } else { + table + } + } + _ => { + if BLOCK_ELEMENTS.contains(&name.local.trim()) { + let mut children: Vec = vec![]; + + // Case: + // + // Hello

Inner text of block element

World + + // Insert before text as a node -- The "Hello" + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + // Inner of the block element -- The "Inner text of block element" + for child in node.children.borrow().iter() { + children.push(parse_node(child, paragraph)); + } + + // if !paragraph.is_empty() { + // children.push(element::Node::Paragraph(paragraph.clone())); + // paragraph.clear(); + // } + + if children.is_empty() { + element::Node::Ignore + } else { + element::Node::Root { children } + } + } else { + // Others to as Inline + parse_paragraph(paragraph, node); + + if paragraph.is_image() { + let image = paragraph.clone(); + paragraph.clear(); + element::Node::Paragraph(image) + } else { + element::Node::Ignore + } + } + } + }, + NodeData::Document => { + let mut children = vec![]; + for child in node.children.borrow().iter() { + children.push(parse_node(child, paragraph)); + } + + if !paragraph.is_empty() { + children.push(element::Node::Paragraph(paragraph.clone())); + paragraph.clear(); + } + + element::Node::Root { children } + } + NodeData::Doctype { .. } => element::Node::Ignore, + NodeData::Comment { .. } => element::Node::Ignore, + NodeData::ProcessingInstruction { .. } => element::Node::Ignore, + } +} + +#[cfg(test)] +mod tests { + use gpui::{px, relative}; + + use crate::text::element::{Node, Paragraph}; + + use super::trim_text; + + #[test] + fn test_trim_text() { + assert_eq!(trim_text(" \n\tHello world \t\r "), " Hello world ",); + } + + #[test] + fn test_value_to_length() { + assert_eq!(super::value_to_length("100px"), Some(px(100.).into())); + assert_eq!(super::value_to_length("100%"), Some(relative(1.))); + assert_eq!(super::value_to_length("56%"), Some(relative(0.56))); + assert_eq!(super::value_to_length("240"), Some(px(240.).into())); + } + + #[test] + fn test_image() { + let html = r#"Example"#; + let node = super::parse_html(html).unwrap(); + assert_eq!( + node, + Node::Paragraph(Paragraph::Image { + span: None, + image: super::ImageNode { + url: "https://example.com/image.png".to_string().into(), + alt: Some("Example".to_string().into()), + width: Some(px(100.).into()), + height: Some(px(200.).into()), + title: Some("Example Image".to_string().into()) + } + }) + ); + + let html = r#"Example"#; + let node = super::parse_html(html).unwrap(); + assert_eq!( + node, + Node::Paragraph(Paragraph::Image { + span: None, + image: super::ImageNode { + url: "https://example.com/image.png".to_string().into(), + alt: Some("Example".to_string().into()), + width: Some(relative(0.8)), + height: None, + title: Some("Example Image".to_string().into()) + } + }) + ); + } +} diff --git a/crates/ui/src/text/markdown.rs b/crates/ui/src/text/markdown.rs new file mode 100644 index 00000000..6a83044b --- /dev/null +++ b/crates/ui/src/text/markdown.rs @@ -0,0 +1,395 @@ +use gpui::{ + div, prelude::FluentBuilder as _, Context, IntoElement, ParentElement, Render, SharedString, + Styled, Window, +}; +use markdown::{ + mdast::{self, Node}, + ParseOptions, +}; + +use crate::v_flex; + +use super::{ + element::{self, ImageNode, InlineTextStyle, LinkMark, Paragraph, Span, Table, TableRow}, + html::parse_html, +}; + +/// Markdown GFM renderer +/// +/// This is design goal is to be able to most common Markdown (GFM) features +/// to let us to display rich text in our application. +/// +/// The goal: +/// +/// - For used to help message. +/// - For used to display like about page. +/// - Some general style customization (Like base text size, line-height...). +/// +/// Not in goal: +/// +/// - As a markdown editor. +/// - Add custom markdown syntax. +/// - Complex styles cumstomization. +pub(super) struct MarkdownView { + text: SharedString, + parsed: bool, + root: Option>, +} + +impl MarkdownView { + pub(super) fn new(raw: impl Into) -> Self { + Self { + text: raw.into(), + parsed: false, + root: None, + } + } + + /// Set the source of the markdown view. + pub(crate) fn set_text(&mut self, raw: impl Into, cx: &mut Context) { + self.text = raw.into(); + self.parsed = false; + cx.notify(); + } + + fn parse_if_needed(&mut self) { + if self.parsed { + return; + } + + self.root = Some(markdown::to_mdast(&self.text, &ParseOptions::gfm()).map(|n| n.into())); + self.parsed = true; + } +} + +impl Render for MarkdownView { + fn render(&mut self, _: &mut Window, _: &mut gpui::Context<'_, Self>) -> impl IntoElement { + self.parse_if_needed(); + + let Some(root) = self.root.clone() else { + return div(); + }; + + div().map(|this| match root { + Ok(node) => this.child(node), + Err(err) => this.child( + v_flex() + .gap_1() + .child("Error parsing markdown") + .child(err.to_string()), + ), + }) + } +} + +fn parse_table_row(table: &mut Table, node: &mdast::TableRow) { + let mut row = TableRow::default(); + node.children.iter().for_each(|c| { + match c { + Node::TableCell(cell) => { + parse_table_cell(&mut row, cell); + } + _ => {} + }; + }); + table.children.push(row); +} + +fn parse_table_cell(row: &mut element::TableRow, node: &mdast::TableCell) { + let mut paragraph = Paragraph::default(); + node.children.iter().for_each(|c| { + parse_paragraph(&mut paragraph, c); + }); + let table_cell = element::TableCell { + children: paragraph, + ..Default::default() + }; + row.children.push(table_cell); +} + +fn parse_paragraph(paragraph: &mut Paragraph, node: &mdast::Node) -> String { + let span = node.position().map(|pos| Span { + start: pos.start.offset, + end: pos.end.offset, + }); + if let Some(span) = span { + paragraph.set_span(span); + } + + let mut text = String::new(); + + match node { + Node::Paragraph(val) => { + val.children.iter().for_each(|c| { + text.push_str(&parse_paragraph(paragraph, c)); + }); + } + Node::Text(val) => { + text = val.value.clone(); + paragraph.push_str(&val.value) + } + Node::Emphasis(val) => { + let mut child_paragraph = Paragraph::default(); + for child in val.children.iter() { + text.push_str(&parse_paragraph(&mut child_paragraph, &child)); + } + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![( + 0..text.len(), + InlineTextStyle { + italic: true, + ..Default::default() + }, + )], + }); + } + Node::Strong(val) => { + let mut child_paragraph = Paragraph::default(); + for child in val.children.iter() { + text.push_str(&parse_paragraph(&mut child_paragraph, &child)); + } + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![( + 0..text.len(), + InlineTextStyle { + bold: true, + ..Default::default() + }, + )], + }); + } + Node::Delete(val) => { + let mut child_paragraph = Paragraph::default(); + for child in val.children.iter() { + text.push_str(&parse_paragraph(&mut child_paragraph, &child)); + } + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![( + 0..text.len(), + InlineTextStyle { + strikethrough: true, + ..Default::default() + }, + )], + }); + } + Node::InlineCode(val) => { + text = val.value.clone(); + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![( + 0..text.len(), + InlineTextStyle { + code: true, + ..Default::default() + }, + )], + }); + } + Node::Link(val) => { + let mut child_paragraph = Paragraph::default(); + for child in val.children.iter() { + text.push_str(&parse_paragraph(&mut child_paragraph, &child)); + } + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![( + 0..text.len(), + InlineTextStyle { + link: Some(LinkMark { + url: val.url.clone().into(), + title: val.title.clone().map(|s| s.into()), + }), + ..Default::default() + }, + )], + }); + } + Node::Image(raw) => { + paragraph.set_image(ImageNode { + url: raw.url.clone().into(), + title: raw.title.clone().map(|t| t.into()), + alt: Some(raw.alt.clone().into()), + ..Default::default() + }); + } + Node::InlineMath(raw) => { + text = raw.value.clone(); + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![( + 0..text.len(), + InlineTextStyle { + code: true, + ..Default::default() + }, + )], + }); + } + Node::MdxTextExpression(raw) => { + text = raw.value.clone(); + paragraph.push(element::TextNode { + text: text.clone(), + marks: vec![(0..text.len(), InlineTextStyle::default())], + }); + } + Node::Html(val) => match parse_html(&val.value) { + Ok(el) => { + if el == element::Node::Break { + text.push_str("\n"); + } else { + if cfg!(debug_assertions) { + eprintln!("[markdown] unsupported inline html tag: {:#?}", el); + } + } + } + Err(err) => { + if cfg!(debug_assertions) { + eprintln!("[markdown] error parsing html: {:#?}", err); + } + + text.push_str(&val.value); + } + }, + _ => { + if cfg!(debug_assertions) { + eprintln!("[markdown] unsupported inline node: {:#?}", node); + } + } + } + + text +} + +impl From for element::Node { + fn from(value: Node) -> Self { + match value { + Node::Root(val) => { + let children = val.children.into_iter().map(|c| c.into()).collect(); + element::Node::Root { children } + } + Node::Paragraph(val) => { + let mut paragraph = Paragraph::default(); + val.children.iter().for_each(|c| { + parse_paragraph(&mut paragraph, c); + }); + + element::Node::Paragraph(paragraph) + } + Node::Blockquote(val) => { + let mut paragraph = Paragraph::default(); + val.children.iter().for_each(|c| { + parse_paragraph(&mut paragraph, c); + }); + + element::Node::Blockquote(paragraph) + } + Node::List(list) => { + let children = list.children.into_iter().map(|c| c.into()).collect(); + element::Node::List { + ordered: list.ordered, + children, + } + } + Node::ListItem(val) => { + let children = val.children.into_iter().map(|c| c.into()).collect(); + element::Node::ListItem { + children, + spread: val.spread, + checked: val.checked, + } + } + Node::Break(_) => element::Node::Break, + Node::Code(raw) => element::Node::CodeBlock { + code: raw.value.into(), + lang: raw.lang.map(|s| s.into()), + }, + Node::Heading(val) => { + let mut paragraph = Paragraph::default(); + val.children.iter().for_each(|c| { + parse_paragraph(&mut paragraph, c); + }); + + element::Node::Heading { + level: val.depth, + children: paragraph, + } + } + Node::Math(val) => element::Node::CodeBlock { + code: val.value.into(), + lang: Some("math".into()), + }, + Node::Html(val) => match parse_html(&val.value) { + Ok(el) => el, + Err(err) => { + if cfg!(debug_assertions) { + eprintln!("[markdown] error parsing html: {:#?}", err); + } + + element::Node::Paragraph(val.value.into()) + } + }, + Node::MdxFlowExpression(val) => element::Node::CodeBlock { + code: val.value.into(), + lang: Some("mdx".into()), + }, + Node::Yaml(val) => element::Node::CodeBlock { + code: val.value.into(), + lang: Some("yaml".into()), + }, + Node::Toml(val) => element::Node::CodeBlock { + code: val.value.into(), + lang: Some("toml".into()), + }, + Node::MdxJsxTextElement(val) => { + println!("MdxJsxTextElement: {:#?}", val); + let mut paragraph = Paragraph::default(); + val.children.iter().for_each(|c| { + parse_paragraph(&mut paragraph, c); + }); + element::Node::Paragraph(paragraph) + } + Node::MdxJsxFlowElement(val) => { + println!("MdxJsxFlowElement: {:#?}", val); + let mut paragraph = Paragraph::default(); + val.children.iter().for_each(|c| { + parse_paragraph(&mut paragraph, c); + }); + element::Node::Paragraph(paragraph) + } + Node::ThematicBreak(_) => element::Node::Divider, + Node::Table(val) => { + let mut table = Table::default(); + val.children.iter().for_each(|c| { + if let Node::TableRow(row) = c { + parse_table_row(&mut table, row); + } + }); + + element::Node::Table(table) + } + _ => { + if cfg!(debug_assertions) { + eprintln!("[markdown] unsupported node: {:#?}", value); + } + element::Node::Unknown + } + } + } +} + +#[cfg(test)] +mod tests { + use super::MarkdownView; + + #[test] + fn test_parse() { + let source = include_str!("../../../story/examples/markdown.md"); + let mut renderer = MarkdownView::new(source); + renderer.parse_if_needed(); + // println!("{:#?}", renderer.root); + } +} diff --git a/crates/ui/src/text/mod.rs b/crates/ui/src/text/mod.rs new file mode 100644 index 00000000..18b7396f --- /dev/null +++ b/crates/ui/src/text/mod.rs @@ -0,0 +1,47 @@ +use gpui::{App, AppContext, Entity, IntoElement, Render, SharedString}; +use html::HtmlView; +use markdown::MarkdownView; + +mod element; +mod html; +mod markdown; +mod utils; + +#[allow(private_interfaces)] +pub enum TextView { + Markdown(Entity), + Html(Entity), +} + +impl TextView { + /// Create a new markdown text view. + pub fn markdown(raw: impl Into, cx: &mut App) -> Self { + Self::Markdown(cx.new(|_| MarkdownView::new(raw))) + } + + /// Create a new html text view. + pub fn html(raw: impl Into, cx: &mut App) -> Self { + Self::Html(cx.new(|_| HtmlView::new(raw))) + } + + /// Set the source text of the text view. + pub fn set_text(&mut self, raw: impl Into, cx: &mut App) { + match self { + Self::Markdown(view) => view.update(cx, |this, cx| this.set_text(raw, cx)), + Self::Html(view) => view.update(cx, |this, cx| this.set_text(raw, cx)), + } + } +} + +impl Render for TextView { + fn render( + &mut self, + _: &mut gpui::Window, + _: &mut gpui::Context<'_, Self>, + ) -> impl IntoElement { + match self { + Self::Markdown(view) => view.clone().into_any_element(), + Self::Html(view) => view.clone().into_any_element(), + } + } +} diff --git a/crates/ui/src/text/utils.rs b/crates/ui/src/text/utils.rs new file mode 100644 index 00000000..5c1c99ff --- /dev/null +++ b/crates/ui/src/text/utils.rs @@ -0,0 +1,61 @@ +const NUMBERED_PREFIXES_1: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; +const NUMBERED_PREFIXES_2: &str = "abcdefghijklmnopqrstuvwxyz"; + +const BULLETS: [&str; 5] = ["▪", "•", "◦", "‣", "⁃"]; + +/// Returns the prefix for a list item. +pub fn list_item_prefix(ix: usize, ordered: bool, depth: usize) -> String { + if ordered { + if depth == 0 { + return format!("{}. ", ix + 1); + } + + if depth == 1 { + return format!( + "{}. ", + NUMBERED_PREFIXES_1 + .chars() + .nth(ix % NUMBERED_PREFIXES_1.len()) + .unwrap() + ); + } else { + return format!( + "{}. ", + NUMBERED_PREFIXES_2 + .chars() + .nth(ix % NUMBERED_PREFIXES_2.len()) + .unwrap() + ); + } + } else { + let depth = depth.min(BULLETS.len() - 1); + let bullet = BULLETS[depth]; + return format!("{} ", bullet); + } +} + +#[cfg(test)] +mod tests { + use crate::text::utils::list_item_prefix; + + #[test] + fn test_list_item_prefix() { + assert_eq!(list_item_prefix(0, true, 0), "1. "); + assert_eq!(list_item_prefix(1, true, 0), "2. "); + assert_eq!(list_item_prefix(2, true, 0), "3. "); + assert_eq!(list_item_prefix(10, true, 0), "11. "); + assert_eq!(list_item_prefix(0, true, 1), "A. "); + assert_eq!(list_item_prefix(1, true, 1), "B. "); + assert_eq!(list_item_prefix(2, true, 1), "C. "); + assert_eq!(list_item_prefix(0, true, 2), "a. "); + assert_eq!(list_item_prefix(1, true, 2), "b. "); + assert_eq!(list_item_prefix(6, true, 2), "g. "); + assert_eq!(list_item_prefix(0, true, 1), "A. "); + assert_eq!(list_item_prefix(0, true, 2), "a. "); + assert_eq!(list_item_prefix(0, false, 0), "▪ "); + assert_eq!(list_item_prefix(0, false, 1), "• "); + assert_eq!(list_item_prefix(0, false, 2), "◦ "); + assert_eq!(list_item_prefix(0, false, 3), "‣ "); + assert_eq!(list_item_prefix(0, false, 4), "⁃ "); + } +}