diff --git a/src/web/crate_details.rs b/src/web/crate_details.rs index f8f117911..293c95f7a 100644 --- a/src/web/crate_details.rs +++ b/src/web/crate_details.rs @@ -57,6 +57,7 @@ pub(crate) struct CrateDetails { pub(crate) metadata: MetaData, is_library: Option, pub(crate) license: Option, + pub(crate) parsed_license: Option>, pub(crate) documentation_url: Option, pub(crate) total_items: Option, pub(crate) documented_items: Option, @@ -225,6 +226,12 @@ impl CrateDetails { None => None, }; + let parsed_license = krate + .license + .as_ref() + .map(|x| &**x) + .map(super::licenses::parse_license); + let mut crate_details = CrateDetails { name: krate.name, version: version.clone(), @@ -250,6 +257,7 @@ impl CrateDetails { documentation_url, is_library: krate.is_library, license: krate.license, + parsed_license, documented_items: krate.documented_items, total_items: krate.total_items, total_items_needing_examples: krate.total_items_needing_examples, diff --git a/src/web/licenses.rs b/src/web/licenses.rs new file mode 100644 index 000000000..3fe983914 --- /dev/null +++ b/src/web/licenses.rs @@ -0,0 +1,737 @@ +use regex::Regex; +use std::collections::HashSet; +use std::sync::LazyLock; + +static LICENSES: LazyLock> = LazyLock::new(|| LICENSE_LIST.iter().copied().collect()); +// License IDs are `idstring = 1*(ALPHA / DIGIT / "-" / "." )` (https://spdx.github.io/spdx-spec/v3.0.1/annexes/spdx-license-expressions/) +static LICENSE_ID_REGEX: LazyLock = + LazyLock::new(|| Regex::new("[A-Za-z0-9.-]+").expect("Known regex must compile")); + +/// A segment of an SPDX license string +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum LicenseSegment { + /// This is a set of glue tokens like OR, AND, `/`, `(`, `)`, etc. + /// + /// Future improvement possibility: format glue tokens differently + GlueTokens(String), + /// This is a license that is found on the SPDX website + Spdx(String), + /// This is a license that is not found on the SPDX website + UnknownLicense(String), +} + +// Code similar to https://github.com/rust-lang/crates.io/blob/ed13e9f3ea3e84b7baa39d63fb8d2f7056eec3a1/app/utils/license.js#L752-L783 +pub fn parse_license(license: &str) -> Vec { + let mut last_end = 0; + let mut segments = Vec::new(); + for mat in LICENSE_ID_REGEX.find_iter(license) { + if mat.range().start != last_end { + segments.push(LicenseSegment::GlueTokens( + license[last_end..mat.range().start].into(), + )); + } + last_end = mat.range().end; + let license = mat.as_str().to_string(); + if LICENSES.contains(&*license) { + segments.push(LicenseSegment::Spdx(license)) + } else { + segments.push(LicenseSegment::UnknownLicense(license)) + } + } + if last_end != license.len() { + segments.push(LicenseSegment::GlueTokens( + license[last_end..license.len()].into(), + )); + } + segments +} + +#[test] +fn test_parse_license() { + fn glue(x: &str) -> LicenseSegment { + LicenseSegment::GlueTokens(x.into()) + } + fn spdx(x: &str) -> LicenseSegment { + LicenseSegment::Spdx(x.into()) + } + fn unknown(x: &str) -> LicenseSegment { + LicenseSegment::UnknownLicense(x.into()) + } + assert_eq!( + parse_license(" Apache-2.0 "), + vec![glue(" "), spdx("Apache-2.0"), glue(" ")] + ); + assert_eq!( + parse_license("Apache-2.0/MIT"), + vec![spdx("Apache-2.0"), glue("/"), spdx("MIT")] + ); + assert_eq!( + parse_license("(Apache-2.0/MIT) AND Unicode-3.0"), + vec![ + glue("("), + spdx("Apache-2.0"), + glue("/"), + spdx("MIT"), + glue(") AND "), + spdx("Unicode-3.0") + ] + ); + assert_eq!( + parse_license("Ferris-Totally-Legal-License-3.5"), + vec![unknown("Ferris-Totally-Legal-License-3.5")] + ); +} + +// Same list as that in crates.io: https://github.com/rust-lang/crates.io/blob/ed13e9f3ea3e84b7baa39d63fb8d2f7056eec3a1/app/utils/license.js#L49-L50 +// from https://raw.githubusercontent.com/spdx/license-list-data/refs/heads/main/json/licenses.json +//
  • - - {{ crate::icons::IconScaleUnbalancedFlip.render_solid(false, false, "") }} {{ krate.license.as_deref().unwrap_or_default() }} - + + {{ crate::icons::IconScaleUnbalancedFlip.render_solid(false, false, "") }} + {%- if let Some(parsed_licenses) = krate.parsed_license -%} + {%- for item in parsed_licenses -%} + {%- match item -%} + {%- when crate::web::licenses::LicenseSegment::Spdx with (license) -%} + {{ license }} + {%- when crate::web::licenses::LicenseSegment::UnknownLicense with (license) -%} + {{ license }} + {%- when crate::web::licenses::LicenseSegment::GlueTokens with (tokens) -%} + {{ tokens }} + {%- endmatch -%} + {%- endfor -%} + {%- endif -%} +
  • diff --git a/templates/style/_navbar.scss b/templates/style/_navbar.scss index db95ef4cc..47523b770 100644 --- a/templates/style/_navbar.scss +++ b/templates/style/_navbar.scss @@ -65,7 +65,7 @@ div.nav-container { } } - .pure-menu-link { + .pure-menu-link,.pure-menu-text { font-size: 12.8px; font-weight: 400; color: var(--color-navbar-standard); @@ -73,6 +73,11 @@ div.nav-container { &.description { font-size: 14.4px; } + } + a.pure-menu-sublink { + color: var(--color-navbar-standard); + } + .pure-menu-link,.pure-menu-sublink, a.pure-menu-sublink { // Improves menu link readability when inverting the colors on focus. // Vendor do background-color #eee, looks weird on different theme. @@ -135,7 +140,7 @@ div.nav-container { outline: unset; } - .docsrs-logo, .pure-menu-item a { + .docsrs-logo, .pure-menu-item a.pure-menu-link, .pure-menu-item .pure-menu-text { padding: 6.4px 16px 6.4px 16px; } @@ -316,7 +321,7 @@ div.nav-container { border-right: 1px solid var(--color-border); } - a.pure-menu-link { + a.pure-menu-link,.pure-menu-item { word-wrap: normal; white-space: normal; }