diff --git a/REUSE.toml b/REUSE.toml index c7591d0fceb..ffd31cf062b 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -36,6 +36,7 @@ path = [ "biome.json", "cspell.json", "docs/search/scraper-config.json", + "internal/core-macros/link-data.json", "package.json", "pnpm-lock.yaml", "rustfmt.toml", diff --git a/api/rs/build/Cargo.toml b/api/rs/build/Cargo.toml index 7a1a81c0b2a..512cf1d5233 100644 --- a/api/rs/build/Cargo.toml +++ b/api/rs/build/Cargo.toml @@ -22,6 +22,7 @@ default = [] [dependencies] i-slint-compiler = { workspace = true, features = ["default", "rust", "display-diagnostics", "software-renderer", "bundle-translations"] } +i-slint-core-macros = { workspace = true } spin_on = { workspace = true } thiserror = "1" diff --git a/api/rs/slint/Cargo.toml b/api/rs/slint/Cargo.toml index 95ad0f77c90..081708bb840 100644 --- a/api/rs/slint/Cargo.toml +++ b/api/rs/slint/Cargo.toml @@ -173,6 +173,7 @@ backend-android-activity-05 = ["i-slint-backend-android-activity/native-activity i-slint-core = { workspace = true } slint-macros = { workspace = true } i-slint-backend-selector = { workspace = true } +i-slint-core-macros = { workspace = true } const-field-offset = { version = "0.1.2", path = "../../../helper_crates/const-field-offset" } document-features = { version = "0.2.0", optional = true } diff --git a/api/rs/slint/docs.rs b/api/rs/slint/docs.rs index 05046c497c7..c219f4a120b 100644 --- a/api/rs/slint/docs.rs +++ b/api/rs/slint/docs.rs @@ -157,6 +157,7 @@ pub mod mcu { } } +#[i_slint_core_macros::slint_doc] pub mod cargo_features { //! # Feature flags and backend selection. //! Use the following feature flags in your Cargo.toml to enable additional features. @@ -164,7 +165,7 @@ pub mod cargo_features { #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] //! //! More information about the backend and renderers is available in the - #![doc = concat!("[Slint Documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint/src/advanced/backends_and_renderers.html)")] + //![Slint Documentation](slint:backends_and_renderers)")] use crate::*; } diff --git a/api/rs/slint/lib.rs b/api/rs/slint/lib.rs index b26f982c7c9..8f06888d28c 100644 --- a/api/rs/slint/lib.rs +++ b/api/rs/slint/lib.rs @@ -11,13 +11,13 @@ This crate is the main entry point for embedding user interfaces designed with [Slint](https://slint.rs/) in Rust programs. */ -#![doc = concat!("If you are new to Slint, start with the [Walk-through **tutorial**](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint/src/quickstart)")] +#![doc = i_slint_core_macros::slint_doc_str!("If you are new to Slint, start with the [Walk-through **tutorial**](slint:quickstart)")] /*! If you are already familiar with Slint, the following topics provide related information. ## Topics */ -#![doc = concat!("- [The Slint Language Documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint)")] +#![doc = i_slint_core_macros::slint_doc_str!("- [The Slint Language Documentation](slint:index)")] /*! - [Type mappings between .slint and Rust](docs::type_mappings) - [Feature flags and backend selection](docs::cargo_features) - [Slint on Microcontrollers](docs::mcu) @@ -30,7 +30,7 @@ of including them in Rust: - The `.slint` code is [inline in a macro](#the-slint-code-in-a-macro). - The `.slint` code in [external files compiled with `build.rs`](#the-slint-code-in-external-files-is-compiled-with-buildrs) */ -#![doc = concat!(" - The `.slint` code is loaded dynamically at run-time from the file system, by using the [interpreter API](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/rust/slint_interpreter/).")] +#![doc = i_slint_core_macros::slint_doc_str!(" - The `.slint` code is loaded dynamically at run-time from the file system, by using the [interpreter API](slint:rust:slint_interpreter/).")] /*! With the first two methods, the markup code is translated to Rust code and each component is turned into a Rust @@ -59,8 +59,9 @@ fn main() { ### The .slint code in external files is compiled with `build.rs` -When your design becomes bigger in terms of markup code, you may want move it to a dedicated*/ -#![doc = concat!("`.slint` file. It's also possible to split a `.slint` file into multiple files using [modules](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint/src/language/syntax/modules.html).")] +When your design becomes bigger in terms of markup code, you may want move it to a dedicated +`.slint` file. */ +#![doc = i_slint_core_macros::slint_doc_str!("It's also possible to split a `.slint` file into multiple files using [modules](slint:modules).")] /*!Use a [build script](https://doc.rust-lang.org/cargo/reference/build-scripts.html) to compile your main `.slint` file: @@ -173,7 +174,7 @@ To run an async function or a future, use [`spawn_local()`]. ## Exported Global singletons */ -#![doc = concat!("When you export a [global singleton](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint/src/language/syntax/globals.html) from the main file,")] +#![doc = i_slint_core_macros::slint_doc_str!("When you export a [global singleton](slint:globals) from the main file,")] /*! it is also generated with the exported name. Like the main component, the generated struct have inherent method to access the properties and callback: @@ -358,6 +359,7 @@ macro_rules! include_modules { }; } +#[i_slint_core_macros::slint_doc] /// Initialize translations when using the `gettext` feature. /// /// Call this in your main function with the path where translations are located. @@ -371,6 +373,8 @@ macro_rules! include_modules { /// `locale` is a locale name (e.g., `en`, `en_GB`, `fr`), and /// `crate` is the package name obtained from the `CARGO_PKG_NAME` environment variable. /// +/// See also the [Translation documentation](slint:translations). +/// /// ### Example /// ```rust /// fn main() { diff --git a/docs/astro/src/utils/utils.ts b/docs/astro/src/utils/utils.ts index 2f9c207b5db..6b09035d452 100644 --- a/docs/astro/src/utils/utils.ts +++ b/docs/astro/src/utils/utils.ts @@ -194,5 +194,7 @@ type LinkMapType = { }; }; -import linkMapData from "./link-data.json" assert { type: "json" }; +import linkMapData from "../../../../internal/core-macros/link-data.json" assert { + type: "json", +}; export const linkMap: Readonly = linkMapData; diff --git a/internal/backends/winit/Cargo.toml b/internal/backends/winit/Cargo.toml index 9655f1bd539..4ca76f40327 100644 --- a/internal/backends/winit/Cargo.toml +++ b/internal/backends/winit/Cargo.toml @@ -80,4 +80,4 @@ cfg_aliases = { workspace = true } slint = { path = "../../../api/rs/slint", default-features = false, features = ["std", "compat-1-2", "backend-winit", "renderer-software", "raw-window-handle-06"] } [package.metadata.docs.rs] -features = ["wayland", "renderer-software"] +features = ["wayland", "renderer-software", "raw-window-handle-06"] diff --git a/internal/backends/winit/lib.rs b/internal/backends/winit/lib.rs index bd19bbd768b..27226dd54f5 100644 --- a/internal/backends/winit/lib.rs +++ b/internal/backends/winit/lib.rs @@ -318,7 +318,9 @@ impl BackendBuilder { } } -#[doc = concat!("This struct implements the Slint Platform trait. Use this in conjunction with [`slint::platform::set_platform`](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/rust/slint/platform/fn.set_platform.html) to initialize.")] +#[i_slint_core_macros::slint_doc] +/// This struct implements the Slint Platform trait. +/// Use this in conjunction with [`slint::platform::set_platform`](slint:rust:slint/platform/fn.set_platform.html) to initialize. /// Slint to use winit for all windowing system interaction. /// /// ```rust,no_run @@ -352,14 +354,19 @@ pub struct Backend { } impl Backend { - #[doc = concat!("Creates a new winit backend with the default renderer that's compiled in. See the [backend documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/rust/slint/index.html#backends) for")] - /// details on how to select the default renderer. + #[i_slint_core_macros::slint_doc] + /// Creates a new winit backend with the default renderer that's compiled in. + /// + /// See the [backend documentation](slint:backends_and_renderers) for details on how to select the default renderer. pub fn new() -> Result { Self::builder().build() } - #[doc = concat!("Creates a new winit backend with the renderer specified by name. See the [backend documentation](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/rust/slint/index.html#backends) for")] - /// details on how to select the default renderer. + #[i_slint_core_macros::slint_doc] + /// Creates a new winit backend with the renderer specified by name. + /// + /// See the [backend documentation](slint:backends_and_renderers) for details on how to select the default renderer. + /// /// If the renderer name is `None` or the name is not recognized, the default renderer is selected. pub fn new_with_renderer_by_name(renderer_name: Option<&str>) -> Result { let mut builder = Self::builder(); @@ -523,7 +530,9 @@ mod private { pub trait WinitWindowAccessorSealed {} } -#[doc = concat!("This helper trait can be used to obtain access to the [`winit::window::Window`] for a given [`slint::Window`](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/rust/slint/struct.window).")] +#[i_slint_core_macros::slint_doc] +/// This helper trait can be used to obtain access to the [`winit::window::Window`] for a given +/// [`slint::Window`](slint:rust:slint/struct.window).")] pub trait WinitWindowAccessor: private::WinitWindowAccessorSealed { /// Returns true if a [`winit::window::Window`] exists for this window. This is the case if the window is /// backed by this winit backend. diff --git a/internal/core-macros/Cargo.toml b/internal/core-macros/Cargo.toml index c97cd518070..d953e488226 100644 --- a/internal/core-macros/Cargo.toml +++ b/internal/core-macros/Cargo.toml @@ -21,4 +21,5 @@ path = "lib.rs" [dependencies] quote = "1.0" -syn = "2.0" +syn = { version = "2.0", features = ["full", "visit-mut"] } +serde_json = { workspace = true } diff --git a/internal/core-macros/lib.rs b/internal/core-macros/lib.rs index 3bcc790e081..2396019370c 100644 --- a/internal/core-macros/lib.rs +++ b/internal/core-macros/lib.rs @@ -1,6 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 +// Copyright © SixtyFPS GmbH + #![doc = include_str!("README.md")] #![doc(html_logo_url = "https://slint.dev/logo/slint-logo-square-light.svg")] @@ -8,6 +11,8 @@ extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; +mod slint_doc; + /// This derive macro is used with structures in the run-time library that are meant /// to be exposed to the language. The structure is introspected for properties and fields /// marked with the `rtti_field` attribute and generates run-time type information for use @@ -179,3 +184,26 @@ fn callback_arg(ty: &syn::Type) -> Option<(&syn::Type, Option<&syn::Type>)> { pub fn identity(_attr: TokenStream, item: TokenStream) -> TokenStream { item } + +/// To be applied on any item that has documentation comment, it will convert link to `slint:Foo` to the link from the +/// documentation map from link-data.json +#[proc_macro_attribute] +pub fn slint_doc(_attr: TokenStream, item: TokenStream) -> TokenStream { + use syn::visit_mut::VisitMut; + let mut visitor = slint_doc::Visitor::new(); + let mut item = syn::parse_macro_input!(item as syn::Item); + visitor.visit_item_mut(&mut item); + assert!(visitor.1, "No slint link found"); + quote!(#item).into() +} + +/// Same as `slint_doc` but for string literals instead of doc coments (useful for crate level documentation that cannot have an attribute) +#[proc_macro] +pub fn slint_doc_str(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as syn::LitStr); + let mut doc = input.value(); + let mut visitor = slint_doc::Visitor::new(); + visitor.process_string(&mut doc); + assert!(visitor.1, "No slint link found"); + quote!(#doc).into() +} diff --git a/docs/astro/src/utils/link-data.json b/internal/core-macros/link-data.json similarity index 90% rename from docs/astro/src/utils/link-data.json rename to internal/core-macros/link-data.json index cc38f3aee08..d3d3935eba1 100644 --- a/docs/astro/src/utils/link-data.json +++ b/internal/core-macros/link-data.json @@ -167,5 +167,24 @@ }, "WinitBackend": { "href": "/guide/backends-and-renderers/backend_winit/" + }, + "translations": { + "href": "/guide/development/translations" + }, + "backends_and_renderers": { + "href": "/guide/backends-and-renderers/backends_and_renderers/" + }, + "globals": { + "href": "/guide/language/coding/globals/" + }, + "quickstart": { + "href": "/tutorial/quickstart/" + }, + "modules": { + "href": "/guide/language/coding/file/#modules" + }, + "index": { + "href": "/" } -} \ No newline at end of file + +} diff --git a/internal/core-macros/slint_doc.rs b/internal/core-macros/slint_doc.rs new file mode 100644 index 00000000000..4a16520681a --- /dev/null +++ b/internal/core-macros/slint_doc.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Copyright © SixtyFPS GmbH + +pub struct Visitor(serde_json::Value, pub bool); + +impl syn::visit_mut::VisitMut for Visitor { + fn visit_attribute_mut(&mut self, i: &mut syn::Attribute) { + if i.meta.path().is_ident("doc") { + if let syn::Meta::NameValue(syn::MetaNameValue { + value: syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. }), + .. + }) = &mut i.meta + { + let mut doc = lit.value(); + self.process_string(&mut doc); + *lit = syn::LitStr::new(&doc, lit.span()); + } + } + } +} + +impl Visitor { + pub fn new() -> Self { + let link_path = concat!(env!("CARGO_MANIFEST_DIR"), "/link-data.json"); + let link_data = std::fs::read_to_string(link_path).expect("Failed to read link-data.json"); + let link_data: serde_json::Value = + serde_json::from_str(&link_data).expect("Failed to parse link-data.json"); + Self(link_data, false) + } + + pub fn process_string(&mut self, doc: &mut String) { + const NEEDLE: &str = "slint:"; + let mut begin = 0; + // search for all occurrences of "slint:foo" and replace it with the link from link-data.json + while let Some(pos) = doc[begin..].find(NEEDLE).map(|x| x + begin) { + if doc[pos..].starts_with("slint::") { + begin = pos + NEEDLE.len(); + continue; + } + let end = doc[pos + NEEDLE.len()..] + .find([' ', '\n', ']', ')']) + .expect("Failed to find end of link"); + let link = &doc[pos + NEEDLE.len()..][..end]; + let dst = if let Some(rust_link) = link.strip_prefix("rust:") { + format!( + "https://releases.slint.dev/{}/docs/rust/{rust_link}", + env!("CARGO_PKG_VERSION"), + ) + } else if let Some(dst) = self.0.get(link) { + let dst = dst + .get("href") + .expect("Missing href in link-data.json") + .as_str() + .expect("invalid string in link-data.json"); + format!("https://releases.slint.dev/{}/docs/slint{dst}", env!("CARGO_PKG_VERSION"),) + } else { + panic!("Unknown link {}", link); + }; + doc.replace_range(pos..pos + NEEDLE.len() + link.len(), &dst); + begin = pos + dst.len(); + self.1 = true; + } + } +} + +#[test] +fn test_slint_doc() { + let mut visitor = Visitor::new(); + + let mut string = r" + Test [SomeLink](slint:index) + Not in a link: slint:index xxx + slint::index is not a link + slint:index is a link + rust link: slint:rust:foobar + " + .to_owned(); + + visitor.process_string(&mut string); + assert!(visitor.1); + assert_eq!( + string, + format!( + r" + Test [SomeLink](https://releases.slint.dev/{0}/docs/slint/) + Not in a link: https://releases.slint.dev/{0}/docs/slint/ xxx + slint::index is not a link + https://releases.slint.dev/{0}/docs/slint/ is a link + rust link: https://releases.slint.dev/{0}/docs/rust/foobar + ", + env!("CARGO_PKG_VERSION") + ) + ); +} diff --git a/internal/core/api.rs b/internal/core/api.rs index 7d27fb1a6c4..c82813248a7 100644 --- a/internal/core/api.rs +++ b/internal/core/api.rs @@ -702,6 +702,7 @@ impl Window { pub use crate::SharedString; +#[i_slint_core_macros::slint_doc] /// This trait is used to obtain references to global singletons exported in `.slint` /// markup. Alternatively, you can use [`ComponentHandle::global`] to obtain access. /// @@ -734,7 +735,7 @@ pub use crate::SharedString; /// Palette::get(&app).set_foreground_color(slint::Color::from_rgb_u8(255, 255, 255)); /// ``` /// -#[doc = concat!("See also the [language documentation for global singletons](https://slint.dev/releases/", env!("CARGO_PKG_VERSION"), "/docs/slint/src/reference/globals.html) for more information.")] +/// See also the [language documentation for global singletons](slint:globals) for more information. /// /// **Note:** Only globals that are exported or re-exported from the main .slint file will /// be exposed in the API diff --git a/internal/core/translations.rs b/internal/core/translations.rs index 1ab010b2fe9..6d96a257210 100644 --- a/internal/core/translations.rs +++ b/internal/core/translations.rs @@ -336,10 +336,13 @@ fn index_for_locale(languages: &[&'static str]) -> Option { }) } +#[i_slint_core_macros::slint_doc] /// Select the current translation language when using bundled translations. /// This function requires that the application's `.slint` file was compiled with bundled translations.. /// It must be called after creating the first component. /// Returns `Ok` if the language was selected; [`SelectBundledTranslationError`] otherwise. +/// +/// See also the [Translation documentation](slint:translations). pub fn select_bundled_translation(language: &str) -> Result<(), SelectBundledTranslationError> { crate::context::GLOBAL_CONTEXT.with(|ctx| { let Some(ctx) = ctx.get() else {