diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index ef621d3..2ff10d1 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -45,5 +45,5 @@ jobs: features: fullstack debug-symbols: false base-path: ${{ github.event.repository.name }} - dx-cli-version: 0.7.7 + dx-cli-version: 0.7.9 toolchain: nightly diff --git a/.github/workflows/web.yml b/.github/workflows/web.yml index db90197..640ba34 100644 --- a/.github/workflows/web.yml +++ b/.github/workflows/web.yml @@ -25,4 +25,4 @@ jobs: features: fullstack debug-symbols: false base-path: ${{ github.event.repository.name }} - dx-cli-version: 0.7.7 + dx-cli-version: 0.7.9 diff --git a/Cargo.lock b/Cargo.lock index 7ad286f..c88b377 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1644,6 +1644,15 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "betlang" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a5811a1a59386e19785588e5bb2384b7d543b4870acedd1c7a1cb177b13b7d" +dependencies = [ + "fearless_simd", +] + [[package]] name = "bit_field" version = "0.10.3" @@ -2314,9 +2323,9 @@ dependencies = [ [[package]] name = "dioxus" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daed3da3e7678d7267bc8523c607dbd19c970f4154cef30b9a58acf35e0a44d5" +checksum = "7c01ecf7ddbae18a419ad3d83c486101a85ffc5740ea09cdd0f09a30dc12170d" dependencies = [ "dioxus-asset-resolver", "dioxus-cli-config", @@ -2348,9 +2357,9 @@ dependencies = [ [[package]] name = "dioxus-asset-resolver" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28cf8859bac5946200df9dc0de8bdc3df60bec007e465dbd3684dbd05fc3ac7" +checksum = "69387edbbc60c7cb93ad96d8cc7a22b49a76e21643380b89b1c49a78d347ff60" dependencies = [ "dioxus-cli-config", "http", @@ -2380,20 +2389,21 @@ dependencies = [ [[package]] name = "dioxus-cli-config" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ef4c22f0a239158f77966d7e7d39b637a10cb374cbd85f881704a336fd3f5c8" +checksum = "c000f584ddf608e2b272b3074bf11512a474eeeb2eb85a1915f276ce5c4a8615" dependencies = [ "wasm-bindgen", ] [[package]] name = "dioxus-code" -version = "0.1.2" +version = "0.1.3" dependencies = [ "arborium", "arborium-theme", "arborium-tree-sitter", + "betlang", "dioxus", "dioxus-code", "dioxus-code-editor", @@ -2402,7 +2412,7 @@ dependencies = [ [[package]] name = "dioxus-code-basic" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2410,7 +2420,7 @@ dependencies = [ [[package]] name = "dioxus-code-docsite" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2420,7 +2430,7 @@ dependencies = [ [[package]] name = "dioxus-code-editor" -version = "0.1.3" +version = "0.1.4" dependencies = [ "dioxus", "dioxus-code", @@ -2430,7 +2440,7 @@ dependencies = [ [[package]] name = "dioxus-code-editor-example" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2439,7 +2449,7 @@ dependencies = [ [[package]] name = "dioxus-code-live-input" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2447,10 +2457,11 @@ dependencies = [ [[package]] name = "dioxus-code-macro" -version = "0.1.1" +version = "0.1.2" dependencies = [ "arborium", "arborium-theme", + "betlang", "dioxus-code", "macro-string", "proc-macro-crate 3.5.0", @@ -2461,7 +2472,7 @@ dependencies = [ [[package]] name = "dioxus-code-macro-only" -version = "0.1.1" +version = "0.1.2" dependencies = [ "dioxus", "dioxus-code", @@ -2469,9 +2480,9 @@ dependencies = [ [[package]] name = "dioxus-config-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a57abdf7bf60e22732a76588e75274ca4eb5d6399b716c735d187d743060b9" +checksum = "7637091592978fbfdb45a16b26bd99fd97fb1bd7e31c6a963530e00c022af321" dependencies = [ "proc-macro2", "quote", @@ -2479,15 +2490,15 @@ dependencies = [ [[package]] name = "dioxus-config-macros" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b571d361abed46996a489e88ced2a335f0ca608305e238859a1a6cca1d85fb15" +checksum = "54f9ed8fc1a215ad34bb8dbae42a4ea54efbcd26ca9006bbe5cca78e511bf25f" [[package]] name = "dioxus-core" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1ff62d7073a4dd48670093469e2c099ef3c895345998a064554274eb39dc91" +checksum = "45887100ff0cf89abeb8b659808294fda48cd53f3b424e36407dedffcfea830b" dependencies = [ "anyhow", "const_format", @@ -2507,9 +2518,9 @@ dependencies = [ [[package]] name = "dioxus-core-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e22483ccaaf037e600683f25a6114754b9610e85e6fafb11adf45ccc2bd7116" +checksum = "370c63663dff0f24df5dfea643ca239283542c6b228a302f69b32e1d36762b7f" dependencies = [ "convert_case 0.8.0", "dioxus-rsx", @@ -2520,15 +2531,15 @@ dependencies = [ [[package]] name = "dioxus-core-types" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a47a4908cc9680a27b314aa8c3832a517274f896a31b0cce7d7adee57eacbb" +checksum = "36963eab106b169737762f9cd5ee5fd97f585989dcb2d8e30a596e97a6999009" [[package]] name = "dioxus-desktop" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3fc8ad2cb85e7810e35f69eccd31ad1cc715f2718f33b3e6b84ff5c794c6236" +checksum = "662cd78c73ca3f17346adbf2d64757df40dd0ce20536c05123097fd31828d2bd" dependencies = [ "anyhow", "async-trait", @@ -2554,7 +2565,7 @@ dependencies = [ "image", "infer", "jni 0.21.1", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "libc", "muda", "ndk", @@ -2583,9 +2594,9 @@ dependencies = [ [[package]] name = "dioxus-devtools" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58abe580e75d1e6bbab658681b126d4a79df3e9c9fd66d0a0627206d1669f65d" +checksum = "2349cedbdf1b429df1f1bea61fdee0ad3dae7b2548eedfbeca82710122a57da0" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2603,9 +2614,9 @@ dependencies = [ [[package]] name = "dioxus-devtools-types" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5678f9f53962936765d0d767c13200bc6911a943492b8614d666425da62662d2" +checksum = "0ab9b0f7565d1916b70915f59b89ea8054ef0a9d67a364a32bbee68ef5f3818d" dependencies = [ "dioxus-core", "serde", @@ -2614,9 +2625,9 @@ dependencies = [ [[package]] name = "dioxus-document" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28eea4bea227f6eb0852ab67ed97a20474f234e8bb2a6abab17401d443aa746c" +checksum = "37e3a5bec7ffc999ff23446a487eb5cd86111d1574a23533dd3f8b3c69a53a22" dependencies = [ "dioxus-core", "dioxus-core-macro", @@ -2625,7 +2636,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "serde", "serde_json", "tracing", @@ -2633,9 +2644,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75d9db5bb54c6305ed3f1cd71ea245a9935126f7586faf92950b40be76be5ae" +checksum = "37f0558edb88af5ad47275ae36a7f06317163ba482db377c26d7d8590b5cd0f6" dependencies = [ "anyhow", "async-stream", @@ -2698,9 +2709,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack-core" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a1d1f1ff784a78709f95b5021888bcd6bb2a4acf972213acd23c1a8a69701b1" +checksum = "cc634b28b4b1e3eab1e8df4f98510e2d2fa39d686321467f977213155e86ed2b" dependencies = [ "anyhow", "axum-core", @@ -2726,9 +2737,9 @@ dependencies = [ [[package]] name = "dioxus-fullstack-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8927db4a9d939677a921eccf2ed36762f05b2e624787f2ef3c095fabc6e6c1c4" +checksum = "85a8fe7da549859fae00c7f4bf11a2aab734ae7ef6f98f280dce9bea1f3326ec" dependencies = [ "const_format", "convert_case 0.8.0", @@ -2740,9 +2751,9 @@ dependencies = [ [[package]] name = "dioxus-history" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e304644c2131e00c4a58ecd91e8f7f86dba007532732bd1a391506b75a974a9" +checksum = "1a15232302d1933015fcf2d6fe9e286ad36f6e9c205a546089a0f326023bb0d2" dependencies = [ "dioxus-core", "tracing", @@ -2750,9 +2761,9 @@ dependencies = [ [[package]] name = "dioxus-hooks" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50506e568a9786247993a29dd3b06fd84f30605312beea2b5f4e51f4ab543b0c" +checksum = "4534f91cf6305204b948bdec130076ac9ecc7c22faab29475b76870558bf73ea" dependencies = [ "dioxus-core", "dioxus-signals", @@ -2766,9 +2777,9 @@ dependencies = [ [[package]] name = "dioxus-html" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c52bdcc2355437ca8bd9986aa1c43c5439af459a2c16012d949b6a35092ae2" +checksum = "e03d6ad4040b667f2b2eefcb678840e630938c09bf9ec39b04ea4d1d96d90d44" dependencies = [ "async-trait", "bytes", @@ -2783,7 +2794,7 @@ dependencies = [ "futures-util", "generational-box", "keyboard-types", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "rustversion", "serde", "serde_json", @@ -2793,9 +2804,9 @@ dependencies = [ [[package]] name = "dioxus-html-internal-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a48657a6a20a65894abba7e7876937e37e924ecaed63e8b654b364c4ffa133" +checksum = "584e2772127ab00f0d5e1d4d9795f39fecebc828ece0b7a02349d438bc1b1ce7" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -2805,15 +2816,15 @@ dependencies = [ [[package]] name = "dioxus-interpreter-js" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a36364418afcb181e19c67d9dbea766dbc2e6bfc0751365a60727ebe665362" +checksum = "11999d6eb5bb179a9512dad30e5de408aab66f2cb65de9098c9fbe02927e2978" dependencies = [ "dioxus-core", "dioxus-core-types", "dioxus-html", "js-sys", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "rustc-hash 2.1.2", "serde", "sledgehammer_bindgen", @@ -2825,9 +2836,9 @@ dependencies = [ [[package]] name = "dioxus-liveview" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea567ceb197345d2719f0808fc8abf508cb47455bf3ee91c805c49d81c2265d" +checksum = "7344b8f174967c7d2f6ad0103d680ab57daea83ebe3368f7f011c402fd6aaf77" dependencies = [ "axum", "dioxus-cli-config", @@ -2853,9 +2864,9 @@ dependencies = [ [[package]] name = "dioxus-logger" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32720fddff9b73266502b15b2c36b139bd8fbfd5b85b67d29ba06d5b4ed60669" +checksum = "a28ccdfe36d2cb830a2784e40f7e6f7199805a2c6da99bd65b1ca308f11aed28" dependencies = [ "dioxus-cli-config", "tracing", @@ -2881,9 +2892,9 @@ dependencies = [ [[package]] name = "dioxus-router" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74cbe73ab4e815aacaa41726d25b84a2debacade3f35db357495dd1d5ed14949" +checksum = "38e47f62d680429badfcb99bf5dec17ee92b0cb9623f264e36bc003a1359bfdc" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -2902,9 +2913,9 @@ dependencies = [ [[package]] name = "dioxus-router-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b91aa613f584c2dd598436c33d22b94b85b132b449eae8f81e538300980222" +checksum = "6f83fb667d27e256f8c9eca49963fbace66a8722cb64ee15a10ffc97d092357e" dependencies = [ "base16", "digest", @@ -2917,9 +2928,9 @@ dependencies = [ [[package]] name = "dioxus-rsx" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767e37207d120b978643f5d1f68675984dfa68d44ceb4dab3d4337b36695d339" +checksum = "2106afda239a4c7c22ffa1ca19117011225fc1c735c139c0a5b765996aa8bb1d" dependencies = [ "proc-macro2", "proc-macro2-diagnostics", @@ -2942,9 +2953,9 @@ dependencies = [ [[package]] name = "dioxus-server" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba9bd5435d3b92a77f1f6ccb175b009efbb708c02247a786bd5bff4bf1253f3" +checksum = "b5ba2095c16f847d3f680a94cc9b0637d190aace651ecfad0feda180da13634b" dependencies = [ "anyhow", "async-trait", @@ -3000,9 +3011,9 @@ dependencies = [ [[package]] name = "dioxus-signals" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef8e274214375597ad26a2e4407b681de4f876785724171ab605a51b272c51e" +checksum = "3705754f5e043deec9fc7af0d159f18e5b21c02c47d255c7e477f31368f0b6d2" dependencies = [ "dioxus-core", "futures-channel", @@ -3016,9 +3027,9 @@ dependencies = [ [[package]] name = "dioxus-ssr" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3aa8abccb3124dd16ff0e557c5659fbe00f8ae3b6a29c1be01e4e440fb69f7c7" +checksum = "d261c5c9907b84fb1ed52f59f46d68c84a4ae860a65cc5effd0cea740ee428af" dependencies = [ "askama_escape", "dioxus-core", @@ -3028,9 +3039,9 @@ dependencies = [ [[package]] name = "dioxus-stores" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96a6a8e92a6df3b3e875f268a2f20d2aeaf017fc952af86810b4d4e435b9a8c1" +checksum = "64bec7b21c86b1360ec965a07a53a2c96b7caee3465049e1c299a45024e87614" dependencies = [ "dioxus-core", "dioxus-signals", @@ -3040,9 +3051,9 @@ dependencies = [ [[package]] name = "dioxus-stores-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb44211a301fd4ddcb21b9c46807658690c2d5a52a7313393001090764ca1ab" +checksum = "40a5875e9f890f27b1cc3e5b56c1e23601211470315a1fb8627c4ca4f3b2be9a" dependencies = [ "convert_case 0.8.0", "proc-macro2", @@ -3052,9 +3063,9 @@ dependencies = [ [[package]] name = "dioxus-web" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e3fcfea97e9a1755d06a38c05e9d8b19fd09228ede36f2c9cfab69891c628" +checksum = "bc0a0be76b404e8242a597db0fb239d05f8dee4e7856bc1fc7144f7e244822fd" dependencies = [ "dioxus-cli-config", "dioxus-core", @@ -3071,7 +3082,7 @@ dependencies = [ "generational-box", "gloo-timers", "js-sys", - "lazy-js-bundle 0.7.7", + "lazy-js-bundle 0.7.9", "rustc-hash 2.1.2", "send_wrapper", "serde", @@ -3322,6 +3333,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fearless_simd" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76258897e51fd156ee03b6246ea53f3e0eb395d0b327e9961c4fc4c8b2fa151a" + [[package]] name = "field-offset" version = "0.3.6" @@ -3611,9 +3628,9 @@ dependencies = [ [[package]] name = "generational-box" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "155c81d7c7cae205289a26248ede524fdb1b7266b1fdb0a479b57bd3a6db2235" +checksum = "8cd0d825b8d339701ad330dbcd6399519ced4d143484954daf6e3185dace4f77" dependencies = [ "parking_lot", "tracing", @@ -4536,9 +4553,9 @@ checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" [[package]] name = "lazy-js-bundle" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76317b9348f1c069d7e652722c3fa86683ec047ba4c1551bde1daa576ce3807e" +checksum = "ccafada6c9541db44db758619236f2748f6e1bdaa84d04ded858567cd1e89321" [[package]] name = "lazy_static" @@ -4737,9 +4754,9 @@ dependencies = [ [[package]] name = "manganis" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "652dba76564ebccd550649b3db9ed608bb5cabfccbe90113c56783649f324eab" +checksum = "8bfcf56309de35b48b8780ea097ace5c3b773a617b52edc49dfc9a63a7d9dc43" dependencies = [ "const-serialize 0.7.2", "const-serialize 0.8.0-alpha.0", @@ -4753,9 +4770,9 @@ dependencies = [ [[package]] name = "manganis-core" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d13cbb4dc790c31565bf49a418e6f441e66130d66aefe9239636e02ab3ebd4" +checksum = "a24d6be68f594495aea60850a284029d585d7b7839b26096c1b6d758f8518648" dependencies = [ "const-serialize 0.7.2", "const-serialize 0.8.0-alpha.0", @@ -4767,9 +4784,9 @@ dependencies = [ [[package]] name = "manganis-macro" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b234972a38639be3b753809ba1d8060ef08b548d07d07cc0c3030aab0c30c132" +checksum = "5e782a10318d707c0833e31876ded8acf91287eee0010af8392559af614c7226" dependencies = [ "dunce", "macro-string", @@ -6673,9 +6690,9 @@ dependencies = [ [[package]] name = "subsecond" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a046b8921cd8a8b3bbeba6339d4ea8cc687653b30c73a158ed4d730d402199db" +checksum = "9cc79674bd55726e6b123204403389400229a95fe4a3b2c5453dada70b06ca95" dependencies = [ "js-sys", "libc", @@ -6692,9 +6709,9 @@ dependencies = [ [[package]] name = "subsecond-types" -version = "0.7.7" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3c20eb7361e08d071ee249a687cddf16a1ae9d36f9bcd92481f31d6ac17eee" +checksum = "e9798bfed58797aed51c672aa99810aac30a50d3120ecfdcf28c13784e9a8f1c" dependencies = [ "serde", ] diff --git a/Cargo.toml b/Cargo.toml index 9c17529..740faa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ resolver = "3" [workspace.package] -version = "0.1.1" +version = "0.1.2" edition = "2024" license = "MIT" repository = "https://github.com/ealmloff/dioxus-code" @@ -26,7 +26,7 @@ dioxus-code-editor = { version = "0.1.2", path = "code-editor", default-features [package] name = "dioxus-code" -version = "0.1.2" +version = "0.1.3" edition.workspace = true license.workspace = true description = "Syntax-highlighted code blocks for Dioxus." @@ -44,6 +44,53 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["macro"] macro = ["dep:dioxus-code-macro", "dioxus-code-macro/lang-rust"] runtime = ["arborium/lang-rust", "dep:arborium-tree-sitter"] +detection = ["runtime", "dep:betlang", "dioxus-code-macro?/detection"] +detectable-languages = [ + "lang-asm", + "lang-bash", + "lang-batch", + "lang-c", + "lang-c-sharp", + "lang-clojure", + "lang-cmake", + "lang-cobol", + "lang-commonlisp", + "lang-cpp", + "lang-css", + "lang-dart", + "lang-dockerfile", + "lang-elixir", + "lang-erlang", + "lang-go", + "lang-groovy", + "lang-haskell", + "lang-html", + "lang-ini", + "lang-java", + "lang-javascript", + "lang-json", + "lang-julia", + "lang-kotlin", + "lang-lua", + "lang-markdown", + "lang-objc", + "lang-ocaml", + "lang-perl", + "lang-php", + "lang-powershell", + "lang-python", + "lang-r", + "lang-ruby", + "lang-scala", + "lang-sql", + "lang-swift", + "lang-toml", + "lang-typescript", + "lang-vb", + "lang-verilog", + "lang-xml", + "lang-yaml", +] all-languages = [ "runtime", "arborium/all-languages", @@ -258,13 +305,14 @@ lang-zsh = ["runtime", "arborium/lang-zsh", "dioxus-code-macro?/lang-zsh"] arborium = { version = "2.16.0", default-features = false } arborium-theme = "2.16.0" arborium-tree-sitter = { version = "2.16.0", optional = true } -dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } +dioxus = { version = "0.7.9", default-features = false, features = ["lib"] } dioxus-code-macro = { version = "0.1.0", path = "dioxus-code-macro", default-features = false, optional = true } +betlang = { version = "0.1.0", optional = true } [build-dependencies] arborium = { version = "2.16.0", default-features = false } [dev-dependencies] -dioxus = { version = "0.7.0", features = ["ssr"] } +dioxus = { version = "0.7.9", features = ["ssr"] } dioxus-code = { path = ".", features = ["runtime"] } dioxus-code-editor = { workspace = true } diff --git a/code-editor/Cargo.toml b/code-editor/Cargo.toml index 2a3505d..9ba8b99 100644 --- a/code-editor/Cargo.toml +++ b/code-editor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dioxus-code-editor" -version = "0.1.3" +version = "0.1.4" edition.workspace = true license.workspace = true description = "Syntax-highlighted code editor component for Dioxus." @@ -16,7 +16,7 @@ rustdoc-args = ["--cfg", "docsrs"] targets = ["x86_64-unknown-linux-gnu", "wasm32-unknown-unknown"] [dependencies] -dioxus = { version = "0.7.0", default-features = false, features = ["lib"] } +dioxus = { version = "0.7.9", default-features = false, features = ["lib"] } dioxus-code = { workspace = true, features = ["runtime"] } [target.'cfg(target_arch = "wasm32")'.dependencies] @@ -31,3 +31,5 @@ web-sys = { version = "0.3", features = [ [features] all-languages = ["dioxus-code/all-languages"] +detectable-languages = ["dioxus-code/detectable-languages"] +detection = ["dioxus-code/detection"] diff --git a/code-editor/src/lib.rs b/code-editor/src/lib.rs index 7e0e7a2..b6af587 100644 --- a/code-editor/src/lib.rs +++ b/code-editor/src/lib.rs @@ -40,7 +40,9 @@ pub struct CodeEditorProps { /// Tree-sitter grammar used for syntax highlighting. /// /// Pass a [`Language`] variant directly. Use [`Language::from_slug`] to - /// turn a runtime slug into a variant. Defaults to [`Language::Rust`]. + /// turn a runtime slug into a variant. With the `detection` feature, pass + /// `Language::Auto` to detect from source contents. Defaults to + /// [`Language::Rust`]. #[props(default = Language::Rust)] pub language: Language, /// Syntax theme selection shared with [`dioxus-code`]. @@ -120,7 +122,9 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { let edit = edit_tracker.borrow_mut().take_for_render(&props.value); let snapshot = { let mut slot = state.borrow_mut(); - if slot.language != props.language { + if slot.language != props.language + || should_rebuild_buffer(props.language, &slot, &props.value) + { slot.buffer = Buffer::new(props.language, props.value.clone()).ok(); slot.language = props.language; } @@ -202,6 +206,23 @@ pub fn CodeEditor(props: CodeEditorProps) -> Element { } } +#[cfg(feature = "detection")] +fn should_rebuild_buffer(language: Language, slot: &EditorBuffer, value: &str) -> bool { + if language != Language::Auto { + return false; + } + + match slot.buffer.as_ref() { + Some(buffer) => buffer.source() != value, + None => true, + } +} + +#[cfg(not(feature = "detection"))] +fn should_rebuild_buffer(_language: Language, _slot: &EditorBuffer, _value: &str) -> bool { + false +} + fn editor_class(theme: impl Into, line_numbers: bool, extra_class: &str) -> String { let mut class = format!("dxc-editor {}", theme.into().classes()); if !line_numbers { @@ -254,4 +275,24 @@ mod tests { assert_eq!(lines[0], vec![HighlightSegment::new("let x = 1;", None)]); assert!(lines[1].is_empty()); } + + #[cfg(feature = "detection")] + #[test] + fn auto_language_rebuilds_when_source_changes() { + let source = "use std::fmt;\nfn main() { println!(\"hi\"); }"; + let slot = EditorBuffer { + buffer: Buffer::new(Language::Auto, source).ok(), + language: Language::Auto, + }; + + assert!(!should_rebuild_buffer(Language::Rust, &slot, "print('hi')")); + assert!(!should_rebuild_buffer(Language::Auto, &slot, source)); + assert!(should_rebuild_buffer(Language::Auto, &slot, "print('hi')")); + + let empty_slot = EditorBuffer { + buffer: None, + language: Language::Auto, + }; + assert!(should_rebuild_buffer(Language::Auto, &empty_slot, source)); + } } diff --git a/dioxus-code-macro/Cargo.toml b/dioxus-code-macro/Cargo.toml index a492695..bf2d163 100644 --- a/dioxus-code-macro/Cargo.toml +++ b/dioxus-code-macro/Cargo.toml @@ -15,6 +15,7 @@ proc-macro = true [features] default = [] +detection = ["dep:betlang"] all-languages = ["arborium/all-languages"] lang-rust = ["arborium/lang-rust"] lang-ada = ["arborium/lang-ada"] @@ -123,6 +124,7 @@ lang-zsh = ["arborium/lang-zsh"] [dependencies] arborium = { version = "2.16.0", default-features = false } arborium-theme = "2.16.0" +betlang = { version = "0.1.0", optional = true } macro-string = "0.1.4" proc-macro-crate = "3.5.0" proc-macro2 = "1.0.103" diff --git a/dioxus-code-macro/README.md b/dioxus-code-macro/README.md index c3e914b..e0fdc28 100644 --- a/dioxus-code-macro/README.md +++ b/dioxus-code-macro/README.md @@ -50,6 +50,18 @@ let _tree = code!( ); ``` +## Inline source detection + +With the `detection` feature enabled, `code_str!` can infer the language from +inline source contents when no explicit language is provided. The detected +language still needs its matching `lang-*` feature or `all-languages` enabled. + +```rust +use dioxus_code::code_str; + +let _tree = code_str!("fn main() { println!(\"hi\"); }"); +``` + ## License MIT. diff --git a/dioxus-code-macro/src/lib.rs b/dioxus-code-macro/src/lib.rs index 998822f..46b8d64 100644 --- a/dioxus-code-macro/src/lib.rs +++ b/dioxus-code-macro/src/lib.rs @@ -40,9 +40,10 @@ pub fn code(input: TokenStream) -> TokenStream { /// /// Parses a string literal containing source code with [`arborium`] and /// expands to the resulting span tree. Pass the source as a string literal, -/// `concat!(...)`, `include_str!(...)`, or `env!(...)`. The language must be -/// supplied via [`CodeOptions::builder`] with [`CodeOptions::with_language`] -/// since there is no file extension to infer from. +/// `concat!(...)`, `include_str!(...)`, or `env!(...)`. Pass +/// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to name the +/// language explicitly; otherwise, with the macro crate's `detection` feature +/// enabled, the language is inferred from the source contents. /// /// To highlight a file on disk instead, use [`code!`]. /// @@ -106,7 +107,7 @@ fn parse_string_and_options( Ok((value, options)) } -fn try_extract_language(expr: &Expr) -> Option { +fn try_extract_language(expr: &Expr) -> Option { match expr { Expr::Group(group) => try_extract_language(&group.expr), Expr::Paren(paren) => try_extract_language(&paren.expr), @@ -123,7 +124,7 @@ fn try_extract_language(expr: &Expr) -> Option { } } -fn try_parse_language_arg(expr: &Expr) -> Option { +fn try_parse_language_arg(expr: &Expr) -> Option { match expr { Expr::Group(group) => try_parse_language_arg(&group.expr), Expr::Paren(paren) => try_parse_language_arg(&paren.expr), @@ -131,7 +132,7 @@ fn try_parse_language_arg(expr: &Expr) -> Option { try_parse_language_arg(call.args.first().unwrap()) } Expr::Path(path) if is_none_path(path) => None, - Expr::Path(path) => language_slug_from_path(path).map(str::to_string), + Expr::Path(path) => language_spec_from_path(path), _ => None, } } @@ -153,6 +154,12 @@ fn is_none_path(path: &syn::ExprPath) -> bool { .is_some_and(|segment| segment.ident == "None") } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct LanguageSpec { + variant: &'static str, + slug: &'static str, +} + const LANGUAGE_VARIANTS: &[(&str, &str)] = &[ ("Rust", "rust"), ("Ada", "ada"), @@ -259,19 +266,88 @@ const LANGUAGE_VARIANTS: &[(&str, &str)] = &[ ("Zsh", "zsh"), ]; -fn language_slug_from_path(path: &syn::ExprPath) -> Option<&'static str> { +fn language_spec_from_path(path: &syn::ExprPath) -> Option { let variant = path.path.segments.last()?.ident.to_string(); + language_spec_for_variant(&variant) +} + +fn language_spec_for_variant(variant: &str) -> Option { LANGUAGE_VARIANTS .iter() .find(|(name, _)| *name == variant) - .map(|(_, slug)| *slug) + .map(|(variant, slug)| LanguageSpec { variant, slug }) } -fn language_variant_for_slug(slug: &str) -> Option<&'static str> { +fn language_spec_for_slug(slug: &str) -> Option { LANGUAGE_VARIANTS .iter() .find(|(_, s)| *s == slug) - .map(|(name, _)| *name) + .map(|(variant, slug)| LanguageSpec { variant, slug }) +} + +#[cfg(feature = "detection")] +fn detect_source_language(source: &str) -> Option { + betlang::detect(source) + .language() + .and_then(language_spec_for_betlang) +} + +#[cfg(not(feature = "detection"))] +fn detect_source_language(_source: &str) -> Option { + None +} + +#[cfg(feature = "detection")] +fn language_spec_for_betlang(language: betlang::Language) -> Option { + let language = match language { + betlang::Language::Asm => "Asm", + betlang::Language::Batch => "Batch", + betlang::Language::C => "C", + betlang::Language::Clojure => "Clojure", + betlang::Language::CMake => "CMake", + betlang::Language::Cobol => "Cobol", + betlang::Language::Cpp => "Cpp", + betlang::Language::Cs => "CSharp", + betlang::Language::Css => "Css", + betlang::Language::Dart => "Dart", + betlang::Language::Dockerfile => "Dockerfile", + betlang::Language::Elixir => "Elixir", + betlang::Language::Erlang => "Erlang", + betlang::Language::Gemfile | betlang::Language::Gemspec | betlang::Language::Ruby => "Ruby", + betlang::Language::Go => "Go", + betlang::Language::Gradle | betlang::Language::Groovy => "Groovy", + betlang::Language::Haskell => "Haskell", + betlang::Language::Html => "Html", + betlang::Language::Ini => "Ini", + betlang::Language::Java => "Java", + betlang::Language::JavaScript => "JavaScript", + betlang::Language::Json => "Json", + betlang::Language::Julia => "Julia", + betlang::Language::Kotlin => "Kotlin", + betlang::Language::Lisp => "CommonLisp", + betlang::Language::Lua => "Lua", + betlang::Language::Markdown => "Markdown", + betlang::Language::ObjectiveC => "ObjectiveC", + betlang::Language::Ocaml => "OCaml", + betlang::Language::Perl => "Perl", + betlang::Language::Php => "Php", + betlang::Language::Powershell => "PowerShell", + betlang::Language::Python => "Python", + betlang::Language::R => "R", + betlang::Language::Rust => "Rust", + betlang::Language::Scala => "Scala", + betlang::Language::Shell => "Bash", + betlang::Language::Sql => "Sql", + betlang::Language::Swift => "Swift", + betlang::Language::Toml => "Toml", + betlang::Language::TypeScript => "TypeScript", + betlang::Language::Vba => "VisualBasic", + betlang::Language::Verilog => "Verilog", + betlang::Language::Xml => "Xml", + betlang::Language::Yaml => "Yaml", + _ => return None, + }; + language_spec_for_variant(language) } fn expand_code(input: CodeInput) -> syn::Result { @@ -300,18 +376,23 @@ fn expand_shared( let crate_path = dioxus_code_crate_path()?; let options_check = options_check_tokens(&crate_path, options.as_ref()); - let Some(language) = options.as_ref().and_then(try_extract_language).or_else(|| { - origin_path - .as_ref() - .and_then(|path| arborium::detect_language(&path.to_string_lossy()).map(str::to_string)) - }) else { + let Some(language) = options + .as_ref() + .and_then(try_extract_language) + .or_else(|| { + origin_path.as_ref().and_then(|path| { + arborium::detect_language(&path.to_string_lossy()).and_then(language_spec_for_slug) + }) + }) + .or_else(|| detect_source_language(&source)) + else { let message = match origin_path.as_ref() { Some(path) => format!( - "could not detect language for `{}`; pass `CodeOptions::builder().with_language(Language::Rust)`", + "could not detect language for `{}`; pass `CodeOptions::builder().with_language(Language::Rust)` or enable `detection` with the matching `lang-*` feature or `all-languages`", path.display() ), None => String::from( - "could not determine language for `code_str!`; pass `CodeOptions::builder().with_language(Language::Rust)`", + "could not determine language for `code_str!`; pass `CodeOptions::builder().with_language(Language::Rust)` or enable `detection` with the matching `lang-*` feature or `all-languages`", ), }; return Ok(quote! {{ @@ -322,17 +403,10 @@ fn expand_shared( let mut highlighter = arborium::Highlighter::new(); let spans = highlighter - .highlight_spans(&language, &source) + .highlight_spans(language.slug, &source) .map_err(|error| syn::Error::new(Span::call_site(), error.to_string()))?; - let Some(variant) = language_variant_for_slug(&language) else { - let message = format!("language `{language}` has no `Language` variant"); - return Ok(quote! {{ - #options_check - compile_error!(#message); - }}); - }; - let variant_ident = Ident::new(variant, Span::call_site()); + let variant_ident = Ident::new(language.variant, Span::call_site()); let source_expr = match origin_path { Some(path) => { @@ -472,37 +546,59 @@ fn resolve_manifest_path(manifest_dir: &Path, path: &str) -> PathBuf { mod tests { use super::*; - fn language(expr: &str) -> Option { + fn language(expr: &str) -> Option { let expr = syn::parse_str::(expr).unwrap(); try_extract_language(&expr) } + fn slug(expr: &str) -> Option<&'static str> { + language(expr).map(|language| language.slug) + } + #[test] fn extracts_language_variant_options() { assert_eq!( - language("CodeOptions::builder().with_language(Language::Rust)").as_deref(), + slug("CodeOptions::builder().with_language(Language::Rust)"), Some("rust"), ); assert_eq!( - language("CodeOptions::builder().with_language(Some(Language::Rust))").as_deref(), + slug("CodeOptions::builder().with_language(Some(Language::Rust))"), Some("rust"), ); } #[test] fn extracts_none_language_option() { - assert_eq!( - language("CodeOptions::builder().with_language(None)").as_deref(), - None, - ); + assert_eq!(slug("CodeOptions::builder().with_language(None)"), None,); } #[test] fn unknown_method_chains_fall_back_silently() { - assert_eq!(language("CodeOptions::builder()").as_deref(), None); + assert_eq!(slug("CodeOptions::builder()"), None); assert_eq!( - language("CodeOptions::builder().with_themes(Language::Rust)").as_deref(), + slug("CodeOptions::builder().with_themes(Language::Rust)"), None, ); } + + #[cfg(feature = "detection")] + #[test] + fn maps_betlang_languages_directly() { + macro_rules! assert_betlang_mapping { + ($betlang:expr, $variant:literal, $slug:literal) => { + assert_eq!( + language_spec_for_betlang($betlang), + Some(LanguageSpec { + variant: $variant, + slug: $slug, + }) + ); + }; + } + + assert_betlang_mapping!(betlang::Language::Cs, "CSharp", "c-sharp"); + assert_betlang_mapping!(betlang::Language::Lisp, "CommonLisp", "commonlisp"); + assert_betlang_mapping!(betlang::Language::Shell, "Bash", "bash"); + assert_betlang_mapping!(betlang::Language::Vba, "VisualBasic", "vb"); + } } diff --git a/docsite/Cargo.toml b/docsite/Cargo.toml index c63503c..d22a0c7 100644 --- a/docsite/Cargo.toml +++ b/docsite/Cargo.toml @@ -10,9 +10,9 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0", features = ["router"] } -dioxus-code = { workspace = true, features = ["runtime", "lang-toml"] } -dioxus-code-editor = { workspace = true } +dioxus = { version = "0.7.9", features = ["router"] } +dioxus-code = { workspace = true, features = ["detection", "detectable-languages"] } +dioxus-code-editor = { workspace = true, features = ["detection", "detectable-languages"] } dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false, features = ["router"] } [features] diff --git a/docsite/src/main.rs b/docsite/src/main.rs index 6a08fd0..bc9bb0a 100644 --- a/docsite/src/main.rs +++ b/docsite/src/main.rs @@ -544,7 +544,7 @@ fn Hero(theme: CodeTheme, theme_label: String) -> Element { "." } p { class: "hero-lede", - "A drop-in component with two source modes: compile-time macro and runtime highlighting with explicit language selection." + "A drop-in component with compile-time macros, runtime highlighting, and optional source-language detection." } div { class: "hero-terminal-block", div { class: "hero-terminal-bar", @@ -663,7 +663,7 @@ fn FeatureRowReceipt() -> Element { span { class: "receipt-aside-num", "02" } div { h3 { class: "receipt-aside-title", "SourceCode" } - p { class: "receipt-aside-text", "Pull it in when input is dynamic and pass the language your source uses." } + p { class: "receipt-aside-text", "Pull it in when input is dynamic; pass a language or let detection pick one from source." } } } div { class: "receipt-aside-row", @@ -690,6 +690,8 @@ fn Playground( let theme_pair = theme_pairs[active_idx()]; let theme = theme_pair.code_theme(scheme); let value = use_memo(move || Some(active_idx())); + let detected_language = use_memo(move || detected_language(&source())); + let language_label = use_memo(move || language_label(detected_language())); rsx! { section { id: "playground", class: "section", @@ -701,9 +703,9 @@ fn Playground( div { class: "playground-grid", Card { class: "card-editor", div { class: "card-bar", - span { "source.rs" } + span { "source" } span { class: "editor-meta", - span { "rust · " {format!("{} chars", source().chars().count())} } + span { "{language_label()} · " {format!("{} chars", source().chars().count())} } span { class: "editor-meta-divider" } Select:: { value: Some(value.into()), @@ -732,9 +734,9 @@ fn Playground( ClientOnly { CodeEditor { value: source(), - language: Language::Rust, + language: detected_language().unwrap_or(Language::Rust), theme, - aria_label: "Rust source editor", + aria_label: "Auto-detected source editor", class: "playground-code-editor", oninput: move |value| source.set(value), } @@ -745,6 +747,14 @@ fn Playground( } } +fn detected_language(source: &str) -> Option { + Language::detect_source(source) +} + +fn language_label(language: Option) -> &'static str { + language.map(Language::slug).unwrap_or("unknown") +} + #[component] fn Docs(scheme: Scheme) -> Element { let theme_pair = ThemePair::new( @@ -820,7 +830,7 @@ fn doc_step_data() -> [DocStepData; 3] { num: "02", eyebrow: "Runtime source", title: "SourceCode for live input", - copy: "Pass any string through SourceCode. Provide a language hint when you already know it — Arborium handles tokenizing.", + copy: "Pass any string through SourceCode. Provide a language hint when you know it, or enable detection for source-first workflows.", code: DOCS_RUNTIME, language: Language::Rust, file_name: "runtime.rs", @@ -900,3 +910,21 @@ fn demo_theme_pairs() -> &'static [ThemePair] { } const APP_CSS: Asset = asset!("/assets/app.css"); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn playground_language_label_shows_detected_language() { + let detected = detected_language("use std::fmt;\nfn main() { println!(\"hi\"); }"); + + assert_eq!(detected, Some(Language::Rust)); + assert_eq!(language_label(detected), "rust"); + } + + #[test] + fn playground_language_label_handles_unknown_language() { + assert_eq!(language_label(detected_language("")), "unknown",); + } +} diff --git a/examples/basic/Cargo.toml b/examples/basic/Cargo.toml index 6891456..39781e7 100644 --- a/examples/basic/Cargo.toml +++ b/examples/basic/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true, features = ["runtime"] } [features] diff --git a/examples/editor/Cargo.toml b/examples/editor/Cargo.toml index b80fd6a..e44e6a1 100644 --- a/examples/editor/Cargo.toml +++ b/examples/editor/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true, features = ["runtime"] } dioxus-code-editor = { workspace = true } diff --git a/examples/live-input/Cargo.toml b/examples/live-input/Cargo.toml index de5dd36..f04eca0 100644 --- a/examples/live-input/Cargo.toml +++ b/examples/live-input/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true, features = ["runtime"] } [features] diff --git a/examples/macro-only/Cargo.toml b/examples/macro-only/Cargo.toml index 5ab787d..31b9fe2 100644 --- a/examples/macro-only/Cargo.toml +++ b/examples/macro-only/Cargo.toml @@ -10,7 +10,7 @@ categories.workspace = true publish = false [dependencies] -dioxus = { version = "0.7.0" } +dioxus = { version = "0.7.9" } dioxus-code = { workspace = true } [features] diff --git a/src/advanced.rs b/src/advanced.rs index afb7157..a1dc30c 100644 --- a/src/advanced.rs +++ b/src/advanced.rs @@ -22,6 +22,9 @@ use std::{borrow::Cow, fmt, ops::Range}; #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum HighlightError { + /// [`Language::Auto`] could not detect a supported language. + #[cfg(feature = "detection")] + LanguageDetectionFailed, /// The tree-sitter parser rejected the selected grammar. GrammarLoad { /// The language whose grammar failed to load. @@ -92,6 +95,8 @@ impl HighlightError { impl fmt::Display for HighlightError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + #[cfg(feature = "detection")] + Self::LanguageDetectionFailed => write!(f, "could not detect language"), Self::GrammarLoad { language, message } => { write!( f, @@ -624,6 +629,7 @@ impl Buffer { /// ``` pub fn new(language: Language, source: impl ToString) -> Result { let source = source.to_string(); + let language = resolve_language(language, &source)?; let (mut parser, incremental) = Self::parser_for(language)?; let mut cursor = arborium_tree_sitter::QueryCursor::new(); let (tree, spans) = Self::parse_source( @@ -733,6 +739,7 @@ impl Buffer { /// assert_eq!(buffer.language(), Language::Rust); /// ``` pub fn set_language(&mut self, language: Language) -> Result<(), HighlightError> { + let language = resolve_language(language, &self.source)?; if self.language == language { return Ok(()); } @@ -853,11 +860,23 @@ fn collect_spans( normalize_spans(raw) } +#[cfg(feature = "runtime")] +fn resolve_language(language: Language, _source: &str) -> Result { + #[cfg(feature = "detection")] + if language == Language::Auto { + return Language::detect_source(_source).ok_or(HighlightError::LanguageDetectionFailed); + } + + Ok(language) +} + #[cfg(feature = "runtime")] fn grammar_for(language: Language) -> (arborium_tree_sitter::LanguageFn, &'static str) { // Rust is bundled with the `runtime` feature; everything else is opt-in via // its `lang-*` cargo feature (or the `all-languages` umbrella). match language { + #[cfg(feature = "detection")] + Language::Auto => unreachable!("auto language must be resolved before loading a grammar"), Language::Rust => ( arborium::lang_rust::language(), arborium::lang_rust::HIGHLIGHTS_QUERY, @@ -1515,6 +1534,61 @@ mod buffer_tests { ); } + #[cfg(all(feature = "detection", feature = "lang-markdown"))] + #[test] + fn auto_detection_uses_source_content_not_path_suffix() { + let source = r#"# Betlang Fixture + +This Markdown file gives the detector a document-shaped source example. + +## Languages + +- Rust +- Python +- JavaScript + +```rust +fn main() { + println!("hello"); +} +``` + +The surrounding prose and headings should make this look like Markdown rather +than the fenced source language. + +main.rs"#; + let buffer = Buffer::new(Language::Auto, source).unwrap(); + + assert_eq!(buffer.language(), Language::Markdown); + } + + #[cfg(all(feature = "detection", feature = "lang-c-sharp"))] + #[test] + fn auto_detection_maps_betlang_csharp_variant() { + let source = r#"using System; +using System.Collections.Generic; +using System.Linq; + +namespace Betlang.Fixtures +{ + public sealed class Program + { + public static void Main(string[] args) + { + var names = new List { "Ada", "Grace", "Linus" }; + foreach (var name in names.Where(value => value.Length > 0)) + { + Console.WriteLine($"Hello, {name}"); + } + } + } +} +"#; + let buffer = Buffer::new(Language::Auto, source).unwrap(); + + assert_eq!(buffer.language(), Language::CSharp); + } + #[test] fn edit_with_explicit_source_edit() { let mut buffer = Buffer::new(Language::Rust, "fn main() { let x = 1; }").unwrap(); diff --git a/src/language.rs b/src/language.rs index 8f9c5fe..a30fd25 100644 --- a/src/language.rs +++ b/src/language.rs @@ -26,6 +26,9 @@ macro_rules! define_languages { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Language { + /// Automatically detect the language from the source text. + #[cfg(feature = "detection")] + Auto, $( $(#[$attr])* #[doc = concat!("Arborium slug `\"", $slug, "\"`.")] @@ -44,6 +47,8 @@ macro_rules! define_languages { /// assert!(Language::ALL.contains(&Language::Rust)); /// ``` pub const ALL: &'static [Language] = &[ + #[cfg(feature = "detection")] + Self::Auto, $( $(#[$attr])* Self::$variant, @@ -53,6 +58,8 @@ macro_rules! define_languages { /// Arborium slug for this language. pub const fn slug(self) -> &'static str { match self { + #[cfg(feature = "detection")] + Self::Auto => "auto", $( $(#[$attr])* Self::$variant => $slug, @@ -78,11 +85,13 @@ macro_rules! define_languages { } impl Language { - /// Best-effort detection from a path, filename, shebang, or file contents. + /// Best-effort detection from a path or filename. /// - /// Wraps [`arborium::detect_language`] and maps the resulting slug into a + /// Wraps [`arborium::detect_language`] and maps the resulting extension into a /// [`Language`] variant, returning `None` when detection fails or the /// detected language's grammar feature is disabled in this build. + /// With the `detection` feature enabled, use `Language::detect_source` to + /// detect from source contents. /// /// Available with the `runtime` feature. #[cfg(feature = "runtime")] @@ -90,6 +99,128 @@ impl Language { pub fn detect(input: &str) -> Option { arborium::detect_language(input).and_then(Self::from_slug) } + + /// Best-effort detection from source contents. + /// + /// Wraps [`betlang::detect`] and maps the detected language enum directly + /// into a [`Language`] variant, returning `None` when detection fails or + /// the detected language's grammar feature is disabled in this build. + /// + /// Available with the `detection` feature. + /// + /// ```rust + /// use dioxus_code::Language; + /// + /// assert_eq!( + /// Language::detect_source("use std::fmt;\nfn main() { println!(\"hi\"); }"), + /// Some(Language::Rust), + /// ); + /// ``` + #[cfg(feature = "detection")] + #[cfg_attr(docsrs, doc(cfg(feature = "detection")))] + pub fn detect_source(source: &str) -> Option { + betlang::detect(source) + .language() + .and_then(Self::from_betlang_language) + } + + #[cfg(feature = "detection")] + fn from_betlang_language(language: betlang::Language) -> Option { + match language { + #[cfg(feature = "lang-asm")] + betlang::Language::Asm => Some(Self::Asm), + #[cfg(feature = "lang-batch")] + betlang::Language::Batch => Some(Self::Batch), + #[cfg(feature = "lang-c")] + betlang::Language::C => Some(Self::C), + #[cfg(feature = "lang-clojure")] + betlang::Language::Clojure => Some(Self::Clojure), + #[cfg(feature = "lang-cmake")] + betlang::Language::CMake => Some(Self::CMake), + #[cfg(feature = "lang-cobol")] + betlang::Language::Cobol => Some(Self::Cobol), + #[cfg(feature = "lang-cpp")] + betlang::Language::Cpp => Some(Self::Cpp), + #[cfg(feature = "lang-c-sharp")] + betlang::Language::Cs => Some(Self::CSharp), + #[cfg(feature = "lang-css")] + betlang::Language::Css => Some(Self::Css), + #[cfg(feature = "lang-dart")] + betlang::Language::Dart => Some(Self::Dart), + #[cfg(feature = "lang-dockerfile")] + betlang::Language::Dockerfile => Some(Self::Dockerfile), + #[cfg(feature = "lang-elixir")] + betlang::Language::Elixir => Some(Self::Elixir), + #[cfg(feature = "lang-erlang")] + betlang::Language::Erlang => Some(Self::Erlang), + #[cfg(feature = "lang-ruby")] + betlang::Language::Gemfile | betlang::Language::Gemspec | betlang::Language::Ruby => { + Some(Self::Ruby) + } + #[cfg(feature = "lang-go")] + betlang::Language::Go => Some(Self::Go), + #[cfg(feature = "lang-groovy")] + betlang::Language::Gradle | betlang::Language::Groovy => Some(Self::Groovy), + #[cfg(feature = "lang-haskell")] + betlang::Language::Haskell => Some(Self::Haskell), + #[cfg(feature = "lang-html")] + betlang::Language::Html => Some(Self::Html), + #[cfg(feature = "lang-ini")] + betlang::Language::Ini => Some(Self::Ini), + #[cfg(feature = "lang-java")] + betlang::Language::Java => Some(Self::Java), + #[cfg(feature = "lang-javascript")] + betlang::Language::JavaScript => Some(Self::JavaScript), + #[cfg(feature = "lang-json")] + betlang::Language::Json => Some(Self::Json), + #[cfg(feature = "lang-julia")] + betlang::Language::Julia => Some(Self::Julia), + #[cfg(feature = "lang-kotlin")] + betlang::Language::Kotlin => Some(Self::Kotlin), + #[cfg(feature = "lang-commonlisp")] + betlang::Language::Lisp => Some(Self::CommonLisp), + #[cfg(feature = "lang-lua")] + betlang::Language::Lua => Some(Self::Lua), + #[cfg(feature = "lang-markdown")] + betlang::Language::Markdown => Some(Self::Markdown), + #[cfg(feature = "lang-objc")] + betlang::Language::ObjectiveC => Some(Self::ObjectiveC), + #[cfg(feature = "lang-ocaml")] + betlang::Language::Ocaml => Some(Self::OCaml), + #[cfg(feature = "lang-perl")] + betlang::Language::Perl => Some(Self::Perl), + #[cfg(feature = "lang-php")] + betlang::Language::Php => Some(Self::Php), + #[cfg(feature = "lang-powershell")] + betlang::Language::Powershell => Some(Self::PowerShell), + #[cfg(feature = "lang-python")] + betlang::Language::Python => Some(Self::Python), + #[cfg(feature = "lang-r")] + betlang::Language::R => Some(Self::R), + betlang::Language::Rust => Some(Self::Rust), + #[cfg(feature = "lang-scala")] + betlang::Language::Scala => Some(Self::Scala), + #[cfg(feature = "lang-bash")] + betlang::Language::Shell => Some(Self::Bash), + #[cfg(feature = "lang-sql")] + betlang::Language::Sql => Some(Self::Sql), + #[cfg(feature = "lang-swift")] + betlang::Language::Swift => Some(Self::Swift), + #[cfg(feature = "lang-toml")] + betlang::Language::Toml => Some(Self::Toml), + #[cfg(feature = "lang-typescript")] + betlang::Language::TypeScript => Some(Self::TypeScript), + #[cfg(feature = "lang-vb")] + betlang::Language::Vba => Some(Self::VisualBasic), + #[cfg(feature = "lang-verilog")] + betlang::Language::Verilog => Some(Self::Verilog), + #[cfg(feature = "lang-xml")] + betlang::Language::Xml => Some(Self::Xml), + #[cfg(feature = "lang-yaml")] + betlang::Language::Yaml => Some(Self::Yaml), + _ => None, + } + } } define_languages! { @@ -299,3 +430,56 @@ define_languages! { #[cfg(feature = "lang-zsh")] Zsh => "zsh", } + +#[cfg(all(test, feature = "detection"))] +mod tests { + use super::*; + + macro_rules! assert_betlang_mapping { + ($betlang:expr, $feature:literal, $language:expr) => { + assert_eq!(Language::from_betlang_language($betlang), { + #[cfg(feature = $feature)] + { + Some($language) + } + #[cfg(not(feature = $feature))] + { + None + } + }); + }; + } + + #[test] + fn maps_betlang_languages_directly() { + assert_eq!( + Language::from_betlang_language(betlang::Language::Rust), + Some(Language::Rust), + ); + + assert_betlang_mapping!(betlang::Language::Cs, "lang-c-sharp", Language::CSharp); + assert_betlang_mapping!( + betlang::Language::Lisp, + "lang-commonlisp", + Language::CommonLisp + ); + assert_betlang_mapping!( + betlang::Language::ObjectiveC, + "lang-objc", + Language::ObjectiveC + ); + assert_betlang_mapping!(betlang::Language::Shell, "lang-bash", Language::Bash); + assert_betlang_mapping!(betlang::Language::Vba, "lang-vb", Language::VisualBasic); + assert_betlang_mapping!(betlang::Language::Gemfile, "lang-ruby", Language::Ruby); + assert_betlang_mapping!(betlang::Language::Gemspec, "lang-ruby", Language::Ruby); + assert_betlang_mapping!(betlang::Language::Gradle, "lang-groovy", Language::Groovy); + } + + #[test] + fn detect_does_not_fall_back_to_source_contents() { + let source = "use std::fmt;\nfn main() { println!(\"hi\"); }"; + + assert_eq!(Language::detect(source), None); + assert_eq!(Language::detect_source(source), Some(Language::Rust)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 158c6c4..48d3605 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,7 +215,7 @@ pub use advanced::{HighlightError, HighlightQueryErrorKind}; /// Source text to highlight at runtime. /// /// Available with the `runtime` feature. Build one with [`SourceCode::new`], -/// then pass it to [`Code()`]. +/// or with [`SourceCode::builder`], then pass it to [`Code()`]. /// /// ```rust /// use dioxus_code::{Language, SourceCode}; @@ -229,6 +229,42 @@ pub struct SourceCode { language: Language, } +/// Source-first builder for [`SourceCode`]. +/// +/// Call [`SourceCodeBuilder::with_language`] to set the language, then +/// `build()` to produce [`SourceCode`]. With the `detection` feature enabled, +/// `build()` may also be called without an explicit language to detect from +/// the source text. +/// +/// ```rust +/// use dioxus_code::{Language, SourceCode}; +/// +/// let _src = SourceCode::builder("fn main() {}") +/// .with_language(Language::Rust) +/// .build(); +/// ``` +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceCodeBuilder { + source: String, + state: State, +} + +/// Builder state before a [`SourceCodeBuilder`] has a language. +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceCodeBuilderMissingLanguage; + +/// Builder state after a [`SourceCodeBuilder`] has a language. +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct SourceCodeBuilderWithLanguage { + language: Language, +} + #[cfg(feature = "runtime")] #[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] impl SourceCode { @@ -245,6 +281,14 @@ impl SourceCode { } } + /// Start a source-first builder. + pub fn builder(source: impl ToString) -> SourceCodeBuilder { + SourceCodeBuilder { + source: source.to_string(), + state: SourceCodeBuilderMissingLanguage, + } + } + /// Replace the language used to highlight this source. /// /// To set the language from a runtime slug, use [`Language::from_slug`] @@ -277,6 +321,43 @@ impl SourceCode { } } +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +impl SourceCodeBuilder { + /// Set the language that will be used to highlight this source. + pub fn with_language( + self, + language: Language, + ) -> SourceCodeBuilder { + SourceCodeBuilder { + source: self.source, + state: SourceCodeBuilderWithLanguage { language }, + } + } + + /// Finish the builder, detecting the language from the source text. + #[cfg(feature = "detection")] + #[cfg_attr(docsrs, doc(cfg(feature = "detection")))] + pub fn build(self) -> SourceCode { + SourceCode::new(Language::Auto, self.source) + } +} + +#[cfg(feature = "runtime")] +#[cfg_attr(docsrs, doc(cfg(feature = "runtime")))] +impl SourceCodeBuilder { + /// Replace the language that will be used to highlight this source. + pub fn with_language(mut self, language: Language) -> Self { + self.state.language = language; + self + } + + /// Finish the builder. + pub fn build(self) -> SourceCode { + SourceCode::new(self.state.language, self.source) + } +} + #[cfg(feature = "runtime")] pub(crate) struct RawHighlightSpan { pub(crate) start: u32, @@ -477,6 +558,25 @@ mod tests { })); } + #[cfg(feature = "runtime")] + #[test] + fn source_code_builder_builds_after_language_is_set() { + let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}") + .with_language(Language::Rust) + .build() + .into(); + assert_eq!(tree.language(), Language::Rust); + assert_eq!(tree.source(), "fn main() {}"); + } + + #[cfg(feature = "detection")] + #[test] + fn source_code_builder_can_detect_language_when_enabled() { + let tree: advanced::HighlightedSource = SourceCode::builder("fn main() {}").build().into(); + assert_eq!(tree.language(), Language::Rust); + assert_eq!(tree.source(), "fn main() {}"); + } + #[cfg(feature = "macro")] #[test] fn code_str_macro_highlights_inline_source() { @@ -490,4 +590,16 @@ mod tests { span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn" })); } + + #[cfg(all(feature = "macro", feature = "detection"))] + #[test] + fn code_str_macro_detects_inline_source() { + const TREE: advanced::HighlightedSource = code_str!("fn main() { println!(\"hi\"); }"); + + assert_eq!(TREE.language(), Language::Rust); + assert_eq!(TREE.source(), "fn main() { println!(\"hi\"); }"); + assert!(TREE.spans().iter().any(|span| { + span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn" + })); + } }