New node: 'String Capitalization' (#4043)
* New node: 'String Capitalization' * Clarify comment
This commit is contained in:
parent
a59fed9d1c
commit
328c4f272b
|
|
@ -5497,6 +5497,7 @@ dependencies = [
|
||||||
name = "text-nodes"
|
name = "text-nodes"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"convert_case 0.8.0",
|
||||||
"core-types",
|
"core-types",
|
||||||
"dyn-any",
|
"dyn-any",
|
||||||
"glam",
|
"glam",
|
||||||
|
|
@ -5507,7 +5508,9 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"skrifa 0.40.0",
|
"skrifa 0.40.0",
|
||||||
|
"titlecase",
|
||||||
"tsify",
|
"tsify",
|
||||||
|
"unicode-segmentation",
|
||||||
"vector-types",
|
"vector-types",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
@ -5675,6 +5678,15 @@ version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "titlecase"
|
||||||
|
version = "3.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eb567088a91d59b492520c8149e2be5ce10d5deb2d9a383f3378df3259679d40"
|
||||||
|
dependencies = [
|
||||||
|
"regex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.47.1"
|
version = "1.47.1"
|
||||||
|
|
@ -5992,9 +6004,9 @@ checksum = "9fb421b350c9aff471779e262955939f565ec18b86c15364e6bdf0d662ca7c1f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-segmentation"
|
name = "unicode-segmentation"
|
||||||
version = "1.12.0"
|
version = "1.13.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-vo"
|
name = "unicode-vo"
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,8 @@ log = "0.4"
|
||||||
bitflags = { version = "2.4", features = ["serde"] }
|
bitflags = { version = "2.4", features = ["serde"] }
|
||||||
ctor = "0.2"
|
ctor = "0.2"
|
||||||
convert_case = "0.8"
|
convert_case = "0.8"
|
||||||
|
titlecase = "3.6"
|
||||||
|
unicode-segmentation = "1.13.2"
|
||||||
indoc = "2.0.5"
|
indoc = "2.0.5"
|
||||||
derivative = "2.2"
|
derivative = "2.2"
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
|
|
|
||||||
|
|
@ -2035,6 +2035,7 @@ fn static_node_properties() -> NodeProperties {
|
||||||
map.insert("selective_color_properties".to_string(), Box::new(node_properties::selective_color_properties));
|
map.insert("selective_color_properties".to_string(), Box::new(node_properties::selective_color_properties));
|
||||||
map.insert("exposure_properties".to_string(), Box::new(node_properties::exposure_properties));
|
map.insert("exposure_properties".to_string(), Box::new(node_properties::exposure_properties));
|
||||||
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
|
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
|
||||||
|
map.insert("string_capitalization_properties".to_string(), Box::new(node_properties::string_capitalization_properties));
|
||||||
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
|
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
|
||||||
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
||||||
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
|
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ use graphene_std::raster::{
|
||||||
use graphene_std::raster_types::Image;
|
use graphene_std::raster_types::Image;
|
||||||
use graphene_std::table::{Table, TableRow};
|
use graphene_std::table::{Table, TableRow};
|
||||||
use graphene_std::text::{Font, TextAlign};
|
use graphene_std::text::{Font, TextAlign};
|
||||||
|
use graphene_std::text_nodes::StringCapitalization;
|
||||||
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
use graphene_std::transform::{Footprint, ReferencePoint, ScaleType, Transform};
|
||||||
use graphene_std::vector::misc::BooleanOperation;
|
use graphene_std::vector::misc::BooleanOperation;
|
||||||
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
use graphene_std::vector::misc::{ArcType, CentroidType, ExtrudeJoiningAlgorithm, GridType, InterpolationDistribution, MergeByDistanceAlgorithm, PointSpacingType, RowsOrColumns, SpiralType};
|
||||||
|
|
@ -246,6 +247,7 @@ pub(crate) fn property_from_type(
|
||||||
Some(x) if x == TypeId::of::<RedGreenBlue>() => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
|
Some(x) if x == TypeId::of::<RedGreenBlue>() => enum_choice::<RedGreenBlue>().for_socket(default_info).property_row(),
|
||||||
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
|
Some(x) if x == TypeId::of::<RedGreenBlueAlpha>() => enum_choice::<RedGreenBlueAlpha>().for_socket(default_info).property_row(),
|
||||||
Some(x) if x == TypeId::of::<XY>() => enum_choice::<XY>().for_socket(default_info).property_row(),
|
Some(x) if x == TypeId::of::<XY>() => enum_choice::<XY>().for_socket(default_info).property_row(),
|
||||||
|
Some(x) if x == TypeId::of::<StringCapitalization>() => enum_choice::<StringCapitalization>().for_socket(default_info).property_row(),
|
||||||
Some(x) if x == TypeId::of::<NoiseType>() => enum_choice::<NoiseType>().for_socket(default_info).property_row(),
|
Some(x) if x == TypeId::of::<NoiseType>() => enum_choice::<NoiseType>().for_socket(default_info).property_row(),
|
||||||
Some(x) if x == TypeId::of::<FractalType>() => enum_choice::<FractalType>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if x == TypeId::of::<FractalType>() => enum_choice::<FractalType>().for_socket(default_info).disabled(false).property_row(),
|
||||||
Some(x) if x == TypeId::of::<CellularDistanceFunction>() => enum_choice::<CellularDistanceFunction>().for_socket(default_info).disabled(false).property_row(),
|
Some(x) if x == TypeId::of::<CellularDistanceFunction>() => enum_choice::<CellularDistanceFunction>().for_socket(default_info).disabled(false).property_row(),
|
||||||
|
|
@ -1628,6 +1630,105 @@ pub(crate) fn exposure_properties(node_id: NodeId, context: &mut NodePropertiesC
|
||||||
vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)]
|
vec![LayoutGroup::row(exposure), LayoutGroup::row(offset), LayoutGroup::row(gamma_correction)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn string_capitalization_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
|
use graphene_std::text_nodes::string_capitalization::*;
|
||||||
|
|
||||||
|
// Read the current values before borrowing context mutably for widgets
|
||||||
|
let (is_simple_case, use_joiner_enabled, joiner_value) = match get_document_node(node_id, context) {
|
||||||
|
Ok(document_node) => {
|
||||||
|
let capitalization_input = document_node.inputs.get(CapitalizationInput::INDEX);
|
||||||
|
let capitalization_exposed = capitalization_input.is_some_and(|input| input.is_exposed());
|
||||||
|
// When exposed, the capitalization mode may change dynamically, so we can't assume it's a simple (joiner-inapplicable) mode
|
||||||
|
let is_simple = !capitalization_exposed
|
||||||
|
&& matches!(
|
||||||
|
capitalization_input.and_then(|input| input.as_value()),
|
||||||
|
Some(TaggedValue::StringCapitalization(StringCapitalization::LowerCase | StringCapitalization::UpperCase))
|
||||||
|
);
|
||||||
|
let use_joiner = match document_node.inputs.get(UseJoinerInput::INDEX).and_then(|input| input.as_value()) {
|
||||||
|
Some(&TaggedValue::Bool(x)) => x,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
let joiner = match document_node.inputs.get(JoinerInput::INDEX).and_then(|input| input.as_non_exposed_value()) {
|
||||||
|
Some(TaggedValue::String(x)) => Some(x.clone()),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
(is_simple, use_joiner, joiner)
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Could not get document node in string_capitalization_properties: {err}");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The joiner controls are disabled when lowercase/UPPERCASE are selected (they don't use word boundaries)
|
||||||
|
let joiner_disabled = is_simple_case || !use_joiner_enabled;
|
||||||
|
|
||||||
|
let capitalization = enum_choice::<StringCapitalization>()
|
||||||
|
.for_socket(ParameterWidgetsInfo::new(node_id, CapitalizationInput::INDEX, true, context))
|
||||||
|
.property_row();
|
||||||
|
|
||||||
|
// Joiner row: the UseJoiner checkbox is drawn in the assist area, followed by the Joiner text input
|
||||||
|
let mut joiner_widgets = start_widgets(ParameterWidgetsInfo::new(node_id, JoinerInput::INDEX, false, context));
|
||||||
|
if let Some(joiner) = joiner_value {
|
||||||
|
let joiner_is_empty = joiner.is_empty();
|
||||||
|
joiner_widgets.extend_from_slice(&[
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
CheckboxInput::new(use_joiner_enabled)
|
||||||
|
.disabled(is_simple_case)
|
||||||
|
.on_update(update_value(|x: &CheckboxInput| TaggedValue::Bool(x.checked), node_id, UseJoinerInput::INDEX))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Related).widget_instance(),
|
||||||
|
Separator::new(SeparatorStyle::Unrelated).widget_instance(),
|
||||||
|
TextInput::new(joiner)
|
||||||
|
.placeholder(if joiner_is_empty { "Empty" } else { "" })
|
||||||
|
.disabled(joiner_disabled)
|
||||||
|
.on_update(update_value(|x: &TextInput| TaggedValue::String(x.value.clone()), node_id, JoinerInput::INDEX))
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_instance(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preset buttons for common joiner values, indented to align with the input field
|
||||||
|
let mut joiner_preset_buttons = vec![TextLabel::new("").widget_instance()];
|
||||||
|
add_blank_assist(&mut joiner_preset_buttons);
|
||||||
|
joiner_preset_buttons.push(Separator::new(SeparatorStyle::Unrelated).widget_instance());
|
||||||
|
for (label, value, tooltip) in [
|
||||||
|
("Empty", "", "Join words without any separator."),
|
||||||
|
("Space", " ", "Join words with a space."),
|
||||||
|
("Kebab", "-", "Join words with a hyphen."),
|
||||||
|
("Snake", "_", "Join words with an underscore."),
|
||||||
|
] {
|
||||||
|
let value = value.to_string();
|
||||||
|
joiner_preset_buttons.push(
|
||||||
|
TextButton::new(label)
|
||||||
|
.tooltip_description(tooltip)
|
||||||
|
.disabled(is_simple_case)
|
||||||
|
.on_update(move |_: &TextButton| Message::Batched {
|
||||||
|
messages: Box::new([
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: UseJoinerInput::INDEX,
|
||||||
|
value: TaggedValue::Bool(true),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
NodeGraphMessage::SetInputValue {
|
||||||
|
node_id,
|
||||||
|
input_index: JoinerInput::INDEX,
|
||||||
|
value: TaggedValue::String(value.clone()),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
.on_commit(commit_value)
|
||||||
|
.widget_instance(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec![capitalization, LayoutGroup::row(joiner_widgets), LayoutGroup::row(joiner_preset_buttons)]
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
pub(crate) fn rectangle_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
use graphene_std::vector::generator_nodes::rectangle::*;
|
use graphene_std::vector::generator_nodes::rectangle::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,7 @@ tagged_value! {
|
||||||
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
LuminanceCalculation(raster_nodes::adjustments::LuminanceCalculation),
|
||||||
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
QRCodeErrorCorrectionLevel(vector_nodes::generator_nodes::QRCodeErrorCorrectionLevel),
|
||||||
XY(graphene_core::extract_xy::XY),
|
XY(graphene_core::extract_xy::XY),
|
||||||
|
StringCapitalization(text_nodes::StringCapitalization),
|
||||||
RedGreenBlue(raster_nodes::adjustments::RedGreenBlue),
|
RedGreenBlue(raster_nodes::adjustments::RedGreenBlue),
|
||||||
RedGreenBlueAlpha(raster_nodes::adjustments::RedGreenBlueAlpha),
|
RedGreenBlueAlpha(raster_nodes::adjustments::RedGreenBlueAlpha),
|
||||||
RealTimeMode(graphene_core::animation::RealTimeMode),
|
RealTimeMode(graphene_core::animation::RealTimeMode),
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::blending::BlendMode]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::LuminanceCalculation]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
|
||||||
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlue]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::raster::adjustments::RedGreenBlueAlpha]),
|
||||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
|
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
|
||||||
|
|
@ -193,6 +194,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::LuminanceCalculation]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::vector::QRCodeErrorCorrectionLevel]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::extract_xy::XY]),
|
||||||
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::text_nodes::StringCapitalization]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlue]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::raster::RedGreenBlueAlpha]),
|
||||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
|
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => graphene_std::animation::RealTimeMode]),
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,9 @@ parley = { workspace = true }
|
||||||
skrifa = { workspace = true }
|
skrifa = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
convert_case = { workspace = true }
|
||||||
|
titlecase = { workspace = true }
|
||||||
|
unicode-segmentation = { workspace = true }
|
||||||
|
|
||||||
# Optional workspace dependencies
|
# Optional workspace dependencies
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@ mod path_builder;
|
||||||
mod text_context;
|
mod text_context;
|
||||||
mod to_path;
|
mod to_path;
|
||||||
|
|
||||||
|
use convert_case::{Boundary, Converter, pattern};
|
||||||
use core_types::Color;
|
use core_types::Color;
|
||||||
use core_types::Ctx;
|
use core_types::Ctx;
|
||||||
use core_types::registry::types::TextArea;
|
use core_types::registry::types::{SignedInteger, TextArea};
|
||||||
use core_types::table::Table;
|
use core_types::table::Table;
|
||||||
use dyn_any::DynAny;
|
use dyn_any::DynAny;
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use raster_types::{CPU, Raster};
|
use raster_types::{CPU, Raster};
|
||||||
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
|
|
||||||
// Re-export for convenience
|
// Re-export for convenience
|
||||||
pub use core_types as gcore;
|
pub use core_types as gcore;
|
||||||
|
|
@ -69,6 +71,30 @@ impl Default for TypesettingConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, dyn_any::DynAny, node_macro::ChoiceType, serde::Serialize, serde::Deserialize)]
|
||||||
|
#[widget(Dropdown)]
|
||||||
|
pub enum StringCapitalization {
|
||||||
|
/// "on the origin of species" — Converts all letters to lower case.
|
||||||
|
#[default]
|
||||||
|
#[label("lower case")]
|
||||||
|
LowerCase,
|
||||||
|
/// "ON THE ORIGIN OF SPECIES" — Converts all letters to upper case.
|
||||||
|
#[label("UPPER CASE")]
|
||||||
|
UpperCase,
|
||||||
|
/// "On The Origin Of Species" — Converts the first letter of every word to upper case.
|
||||||
|
#[label("Capital Case")]
|
||||||
|
CapitalCase,
|
||||||
|
/// "On the Origin of Species" — Converts the first letter of significant words to upper case.
|
||||||
|
#[label("Headline Case")]
|
||||||
|
HeadlineCase,
|
||||||
|
/// "On the origin of species" — Converts the first letter of every word to lower case, except the initial word which is made upper case.
|
||||||
|
#[label("Sentence case")]
|
||||||
|
SentenceCase,
|
||||||
|
/// "on The Origin Of Species" — Converts the first letter of every word to upper case, except the initial word which is made lower case.
|
||||||
|
#[label("camel Case")]
|
||||||
|
CamelCase,
|
||||||
|
}
|
||||||
|
|
||||||
/// Constructs a string value which may be set to any plain text.
|
/// Constructs a string value which may be set to any plain text.
|
||||||
#[node_macro::node(category("Value"))]
|
#[node_macro::node(category("Value"))]
|
||||||
fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {
|
fn string_value(_: impl Ctx, _primary: (), string: TextArea) -> String {
|
||||||
|
|
@ -84,7 +110,7 @@ fn to_string(_: impl Ctx, value: String) -> String {
|
||||||
/// Joins two strings together.
|
/// Joins two strings together.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
|
fn string_concatenate(_: impl Ctx, #[implementations(String)] first: String, second: TextArea) -> String {
|
||||||
first.clone() + &second
|
first + &second
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replaces all occurrences of "From" with "To" in the input string.
|
/// Replaces all occurrences of "From" with "To" in the input string.
|
||||||
|
|
@ -94,28 +120,113 @@ fn string_replace(_: impl Ctx, string: String, from: TextArea, to: TextArea) ->
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extracts a substring from the input string, starting at "Start" and ending before "End".
|
/// Extracts a substring from the input string, starting at "Start" and ending before "End".
|
||||||
/// Negative indices count from the end of the string.
|
///
|
||||||
/// If "Start" equals or exceeds "End", the result is an empty string.
|
/// Negative indices count from the end of the string. If the index of "Start" equals or exceeds "End", the result is an empty string.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_slice(_: impl Ctx, string: String, start: f64, end: f64) -> String {
|
fn string_slice(_: impl Ctx, string: String, start: SignedInteger, end: SignedInteger) -> String {
|
||||||
let total_chars = string.chars().count();
|
let total_graphemes = string.graphemes(true).count();
|
||||||
|
|
||||||
let start = if start < 0. {
|
let start = if start < 0. {
|
||||||
total_chars.saturating_sub(start.abs() as usize)
|
total_graphemes.saturating_sub(start.abs() as usize)
|
||||||
} else {
|
} else {
|
||||||
(start as usize).min(total_chars)
|
(start as usize).min(total_graphemes)
|
||||||
};
|
};
|
||||||
let end = if end <= 0. {
|
let end = if end <= 0. {
|
||||||
total_chars.saturating_sub(end.abs() as usize)
|
total_graphemes.saturating_sub(end.abs() as usize)
|
||||||
} else {
|
} else {
|
||||||
(end as usize).min(total_chars)
|
(end as usize).min(total_graphemes)
|
||||||
};
|
};
|
||||||
|
|
||||||
if start >= end {
|
if start >= end {
|
||||||
return String::new();
|
return String::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
string.chars().skip(start).take(end - start).collect()
|
string.graphemes(true).skip(start).take(end - start).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a string's capitalization style to another of the common upper and lower case patterns, optionally joining words with a chosen separator.
|
||||||
|
#[node_macro::node(category("Text"), properties("string_capitalization_properties"))]
|
||||||
|
fn string_capitalization(
|
||||||
|
_: impl Ctx,
|
||||||
|
/// The string to have its letter capitalization converted.
|
||||||
|
string: String,
|
||||||
|
/// The capitalization style to apply.
|
||||||
|
capitalization: StringCapitalization,
|
||||||
|
/// Whether to split the string into words and reconnect with the chosen joiner. When disabled, the existing word structure separators are preserved.
|
||||||
|
use_joiner: bool,
|
||||||
|
/// The string placed between each word.
|
||||||
|
joiner: String,
|
||||||
|
) -> String {
|
||||||
|
// When the joiner is enabled, apply word-level casing and optionally reconnect words with the selected joiner
|
||||||
|
if use_joiner {
|
||||||
|
match capitalization {
|
||||||
|
// Simple case mappings that preserve the string's existing structure
|
||||||
|
StringCapitalization::LowerCase => string.to_lowercase(),
|
||||||
|
StringCapitalization::UpperCase => string.to_uppercase(),
|
||||||
|
|
||||||
|
// Word-aware capitalizations that split on word boundaries and rejoin with the joiner
|
||||||
|
StringCapitalization::CapitalCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::capital).set_delim(&joiner).convert(&string),
|
||||||
|
StringCapitalization::HeadlineCase => {
|
||||||
|
// First split into words with convert_case so word boundaries like "AlphaNumeric" are detected consistently with other modes,
|
||||||
|
// then apply the titlecase crate for smart capitalization (lowercasing short words like "of", "the", etc.),
|
||||||
|
// then rejoin with the custom joiner without mangling the capitalization
|
||||||
|
let spaced = Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::capital).set_delim(" ").convert(&string);
|
||||||
|
let headline = titlecase::titlecase(&spaced);
|
||||||
|
Converter::new().set_boundaries(&[Boundary::SPACE]).set_pattern(pattern::noop).set_delim(&joiner).convert(&headline)
|
||||||
|
}
|
||||||
|
StringCapitalization::SentenceCase => Converter::new()
|
||||||
|
.set_boundaries(&Boundary::defaults())
|
||||||
|
.set_pattern(pattern::sentence)
|
||||||
|
.set_delim(&joiner)
|
||||||
|
.convert(&string),
|
||||||
|
StringCapitalization::CamelCase => Converter::new().set_boundaries(&Boundary::defaults()).set_pattern(pattern::camel).set_delim(&joiner).convert(&string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// When the joiner is disabled, apply only character-level casing while preserving the string's existing structure
|
||||||
|
else {
|
||||||
|
match capitalization {
|
||||||
|
StringCapitalization::LowerCase => string.to_lowercase(),
|
||||||
|
StringCapitalization::UpperCase => string.to_uppercase(),
|
||||||
|
StringCapitalization::CapitalCase => {
|
||||||
|
let mut capitalize_next = true;
|
||||||
|
string.chars().fold(String::with_capacity(string.len()), |mut result, c| {
|
||||||
|
if c.is_whitespace() || c == '_' || c == '-' {
|
||||||
|
capitalize_next = true;
|
||||||
|
result.push(c);
|
||||||
|
} else if capitalize_next {
|
||||||
|
capitalize_next = false;
|
||||||
|
result.extend(c.to_uppercase());
|
||||||
|
} else {
|
||||||
|
result.push(c);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
StringCapitalization::HeadlineCase => titlecase::titlecase(&string),
|
||||||
|
StringCapitalization::SentenceCase => {
|
||||||
|
let mut chars = string.chars();
|
||||||
|
match chars.next() {
|
||||||
|
Some(first) => first.to_uppercase().to_string() + &chars.as_str().to_lowercase(),
|
||||||
|
None => String::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StringCapitalization::CamelCase => {
|
||||||
|
let mut capitalize_next = false;
|
||||||
|
string.chars().fold(String::with_capacity(string.len()), |mut result, c| {
|
||||||
|
if c.is_whitespace() || c == '_' || c == '-' {
|
||||||
|
capitalize_next = true;
|
||||||
|
result.push(c);
|
||||||
|
} else if capitalize_next {
|
||||||
|
capitalize_next = false;
|
||||||
|
result.extend(c.to_uppercase());
|
||||||
|
} else {
|
||||||
|
result.extend(c.to_lowercase());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
// TODO: Return u32, u64, or usize instead of f64 after #1621 is resolved and has allowed us to implement automatic type conversion in the node graph for nodes with generic type inputs.
|
||||||
|
|
@ -123,7 +234,7 @@ fn string_slice(_: impl Ctx, string: String, start: f64, end: f64) -> String {
|
||||||
/// Counts the number of characters in a string.
|
/// Counts the number of characters in a string.
|
||||||
#[node_macro::node(category("Text"))]
|
#[node_macro::node(category("Text"))]
|
||||||
fn string_length(_: impl Ctx, string: String) -> f64 {
|
fn string_length(_: impl Ctx, string: String) -> f64 {
|
||||||
string.chars().count() as f64
|
string.graphemes(true).count() as f64
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Splits a string into a list of substrings based on the specified delimeter.
|
/// Splits a string into a list of substrings based on the specified delimeter.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue