Skip to content

Commit 950d2a1

Browse files
committed
Regex Find and Regex Find All
1 parent 7b6c624 commit 950d2a1

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1489,6 +1489,111 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
14891489
description: Cow::Borrowed("TODO"),
14901490
properties: None,
14911491
},
1492+
DocumentNodeDefinition {
1493+
identifier: "Regex Find",
1494+
category: "Text",
1495+
node_template: NodeTemplate {
1496+
document_node: DocumentNode {
1497+
implementation: DocumentNodeImplementation::Network(NodeNetwork {
1498+
exports: vec![
1499+
// Primary output: the whole match (String)
1500+
NodeInput::node(NodeId(1), 0),
1501+
// Secondary output: capture groups (Vec<String>)
1502+
NodeInput::node(NodeId(2), 0),
1503+
],
1504+
nodes: [
1505+
// Node 0: regex_find proto node — returns Vec<String> of [whole_match, ...capture_groups]
1506+
DocumentNode {
1507+
inputs: vec![
1508+
NodeInput::import(concrete!(String), 0),
1509+
NodeInput::import(concrete!(String), 1),
1510+
NodeInput::import(concrete!(f64), 2),
1511+
NodeInput::import(concrete!(bool), 3),
1512+
NodeInput::import(concrete!(bool), 4),
1513+
],
1514+
implementation: DocumentNodeImplementation::ProtoNode(logic::regex_find::IDENTIFIER),
1515+
..Default::default()
1516+
},
1517+
// Node 1: index_elements at index 0 — extracts the whole match as a String
1518+
DocumentNode {
1519+
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
1520+
implementation: DocumentNodeImplementation::ProtoNode(graphic::index_elements::IDENTIFIER),
1521+
..Default::default()
1522+
},
1523+
// Node 2: omit_element at index 0 — returns capture groups as Vec<String>
1524+
DocumentNode {
1525+
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
1526+
implementation: DocumentNodeImplementation::ProtoNode(graphic::omit_element::IDENTIFIER),
1527+
..Default::default()
1528+
},
1529+
]
1530+
.into_iter()
1531+
.enumerate()
1532+
.map(|(id, node)| (NodeId(id as u64), node))
1533+
.collect(),
1534+
..Default::default()
1535+
}),
1536+
inputs: vec![
1537+
NodeInput::value(TaggedValue::String(String::new()), true),
1538+
NodeInput::value(TaggedValue::String(String::new()), false),
1539+
NodeInput::value(TaggedValue::F64(0.), false),
1540+
NodeInput::value(TaggedValue::Bool(false), false),
1541+
NodeInput::value(TaggedValue::Bool(false), false),
1542+
],
1543+
..Default::default()
1544+
},
1545+
persistent_node_metadata: DocumentNodePersistentMetadata {
1546+
input_metadata: vec![
1547+
("String", "The string to search within.").into(),
1548+
("Pattern", "The regular expression pattern to search for.").into(),
1549+
(
1550+
"Match Index",
1551+
"Which occurrence of the pattern to return (0 for the first). Negative indices count from the last match.",
1552+
)
1553+
.into(),
1554+
("Case Insensitive", "Match letters regardless of case.").into(),
1555+
("Multiline", "Make `^` and `$` match the start and end of each line, not just the whole string.").into(),
1556+
],
1557+
output_names: vec!["Match".to_string(), "Captures".to_string()],
1558+
network_metadata: Some(NodeNetworkMetadata {
1559+
persistent_metadata: NodeNetworkPersistentMetadata {
1560+
node_metadata: [
1561+
DocumentNodeMetadata {
1562+
persistent_metadata: DocumentNodePersistentMetadata {
1563+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(0, 0)),
1564+
..Default::default()
1565+
},
1566+
..Default::default()
1567+
},
1568+
DocumentNodeMetadata {
1569+
persistent_metadata: DocumentNodePersistentMetadata {
1570+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(8, -2)),
1571+
..Default::default()
1572+
},
1573+
..Default::default()
1574+
},
1575+
DocumentNodeMetadata {
1576+
persistent_metadata: DocumentNodePersistentMetadata {
1577+
node_type_metadata: NodeTypePersistentMetadata::node(IVec2::new(8, 2)),
1578+
..Default::default()
1579+
},
1580+
..Default::default()
1581+
},
1582+
]
1583+
.into_iter()
1584+
.enumerate()
1585+
.map(|(id, node)| (NodeId(id as u64), node))
1586+
.collect(),
1587+
..Default::default()
1588+
},
1589+
..Default::default()
1590+
}),
1591+
..Default::default()
1592+
},
1593+
},
1594+
description: Cow::Borrowed("Finds a regex match in the string. The primary output is the whole match, and the secondary output is the list of capture groups."),
1595+
properties: None,
1596+
},
14921597
// Aims for interoperable compatibility with:
14931598
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=levl%27%20%3D%20Levels-,%27curv%27%20%3D%20Curves,-%27expA%27%20%3D%20Exposure
14941599
// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#:~:text=Max%20input%20range-,Curves,-Curves%20settings%20files

node-graph/libraries/graphic-types/src/graphic.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,41 @@ impl<T: Clone> AtIndex for Table<T> {
397397
}
398398
}
399399

400+
pub trait OmitIndex {
401+
fn omit_index(&self, index: usize) -> Self;
402+
fn omit_index_from_end(&self, index: usize) -> Self;
403+
}
404+
impl<T: Clone> OmitIndex for Vec<T> {
405+
fn omit_index(&self, index: usize) -> Self {
406+
self.iter().enumerate().filter(|(i, _)| *i != index).map(|(_, v)| v.clone()).collect()
407+
}
408+
409+
fn omit_index_from_end(&self, index: usize) -> Self {
410+
if index == 0 || index > self.len() {
411+
return self.clone();
412+
}
413+
self.omit_index(self.len() - index)
414+
}
415+
}
416+
impl<T: Clone> OmitIndex for Table<T> {
417+
fn omit_index(&self, index: usize) -> Self {
418+
let mut result = Self::default();
419+
for (i, row) in self.iter().enumerate() {
420+
if i != index {
421+
result.push(row.into_cloned());
422+
}
423+
}
424+
result
425+
}
426+
427+
fn omit_index_from_end(&self, index: usize) -> Self {
428+
if index == 0 || index > self.len() {
429+
return self.clone();
430+
}
431+
self.omit_index(self.len() - index)
432+
}
433+
}
434+
400435
// TODO: Eventually remove this migration document upgrade code
401436
pub fn migrate_graphic<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Graphic>, D::Error> {
402437
use serde::Deserialize;

node-graph/nodes/gcore/src/logic.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,87 @@ fn regex_replace(
655655
}
656656
}
657657

658+
/// Finds a regex match in the string and returns its components. The result is a list where the first element is the whole match and
659+
/// subsequent elements are the capture groups (if any). The match index selects which occurrence to return (0 for the first match).
660+
/// Returns an empty list if no match is found at the given index.
661+
#[node_macro::node(category(""))]
662+
fn regex_find(
663+
_: impl Ctx,
664+
/// The string to search within.
665+
string: String,
666+
/// The regular expression pattern to search for.
667+
pattern: String,
668+
/// Which occurrence of the pattern to return, starting from 0 for the first match. Negative indices count backwards from the last match.
669+
match_index: SignedInteger,
670+
/// Match letters regardless of case.
671+
case_insensitive: bool,
672+
/// Make `^` and `$` match the start and end of each line, not just the whole string.
673+
multiline: bool,
674+
) -> Vec<String> {
675+
let flags = match (case_insensitive, multiline) {
676+
(false, false) => "",
677+
(true, false) => "(?i)",
678+
(false, true) => "(?m)",
679+
(true, true) => "(?im)",
680+
};
681+
let full_pattern = format!("{flags}{pattern}");
682+
683+
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
684+
log::error!("Invalid regex pattern: {pattern}");
685+
return Vec::new();
686+
};
687+
688+
// Collect all matches since we need to support negative indexing
689+
let matches: Vec<_> = regex.captures_iter(&string).filter_map(|c| c.ok()).collect();
690+
691+
let match_index = match_index as i32;
692+
let resolved_index = if match_index < 0 {
693+
let from_end = (-match_index) as usize;
694+
if from_end > matches.len() {
695+
return Vec::new();
696+
}
697+
matches.len() - from_end
698+
} else {
699+
match_index as usize
700+
};
701+
702+
let Some(captures) = matches.get(resolved_index) else {
703+
return Vec::new();
704+
};
705+
706+
// Index 0 is the whole match, 1+ are capture groups
707+
(0..captures.len()).map(|i| captures.get(i).map_or(String::new(), |m| m.as_str().to_string())).collect()
708+
}
709+
710+
/// Finds all non-overlapping matches of a regular expression pattern in the string, returning a list of the matched substrings.
711+
#[node_macro::node(category("Text"))]
712+
fn regex_find_all(
713+
_: impl Ctx,
714+
/// The string to search within.
715+
string: String,
716+
/// The regular expression pattern to search for.
717+
pattern: String,
718+
/// Match letters regardless of case.
719+
case_insensitive: bool,
720+
/// Make `^` and `$` match the start and end of each line, not just the whole string.
721+
multiline: bool,
722+
) -> Vec<String> {
723+
let flags = match (case_insensitive, multiline) {
724+
(false, false) => "",
725+
(true, false) => "(?i)",
726+
(false, true) => "(?m)",
727+
(true, true) => "(?im)",
728+
};
729+
let full_pattern = format!("{flags}{pattern}");
730+
731+
let Ok(regex) = fancy_regex::Regex::new(&full_pattern) else {
732+
log::error!("Invalid regex pattern: {pattern}");
733+
return Vec::new();
734+
};
735+
736+
regex.find_iter(&string).filter_map(|m| m.ok()).map(|m| m.as_str().to_string()).collect()
737+
}
738+
658739
/// Iterates over a list of strings, evaluating the mapped operation for each one. Use the *Read String* node to access the current string inside the loop.
659740
#[node_macro::node(category("Text"))]
660741
async fn map_string(

node-graph/nodes/graphic/src/graphic.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,39 @@ where
4646
.unwrap_or_default()
4747
}
4848

49+
/// Returns the collection with the element at the specified index removed.
50+
/// If no value exists at that index, the collection is returned unchanged.
51+
#[node_macro::node(category("General"))]
52+
pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
53+
_: impl Ctx,
54+
/// The collection of data, such as a list or table.
55+
#[implementations(
56+
Vec<f64>,
57+
Vec<u32>,
58+
Vec<u64>,
59+
Vec<DVec2>,
60+
Vec<String>,
61+
Table<Artboard>,
62+
Table<Graphic>,
63+
Table<Vector>,
64+
Table<Raster<CPU>>,
65+
Table<Raster<GPU>>,
66+
Table<Color>,
67+
Table<GradientStops>,
68+
)]
69+
collection: T,
70+
/// The index of the item to remove, starting from 0 for the first item. Negative indices count backwards from the end of the collection, starting from -1 for the last item.
71+
index: SignedInteger,
72+
) -> T {
73+
let index = index as i32;
74+
75+
if index < 0 {
76+
collection.omit_index_from_end(-index as usize)
77+
} else {
78+
collection.omit_index(index as usize)
79+
}
80+
}
81+
4982
#[node_macro::node(category("General"))]
5083
async fn map<Item: AnyHash + Send + Sync + std::hash::Hash>(
5184
ctx: impl Ctx + CloneVarArgs + ExtractAll,

0 commit comments

Comments
 (0)