Skip to content

Commit f7c4ee7

Browse files
Keavonoluseyi
authored andcommitted
Add useful attributes to the JSON and Regex nodes (GraphiteEditor#4069)
* Add useful attributes to the JSON and Regex nodes * Code review fix
1 parent 50e0c0e commit f7c4ee7

4 files changed

Lines changed: 82 additions & 14 deletions

File tree

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1545,11 +1545,11 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
15451545
exports: vec![
15461546
// Primary output: the whole match (String)
15471547
NodeInput::node(NodeId(1), 0),
1548-
// Secondary output: capture groups (Vec<String>)
1548+
// Secondary output: capture groups (Table<String>), each row carries `start`/`end`/`name` attributes from `regex_find`
15491549
NodeInput::node(NodeId(2), 0),
15501550
],
15511551
nodes: [
1552-
// Node 0: regex_find proto node — returns Vec<String> of [whole_match, ...capture_groups]
1552+
// Node 0: regex_find proto node — returns Table<String> of [whole_match, ...capture_groups]
15531553
DocumentNode {
15541554
inputs: vec![
15551555
NodeInput::import(concrete!(String), 0),
@@ -1561,13 +1561,13 @@ fn document_node_definitions() -> HashMap<DefinitionIdentifier, DocumentNodeDefi
15611561
implementation: DocumentNodeImplementation::ProtoNode(text_nodes::regex::regex_find::IDENTIFIER),
15621562
..Default::default()
15631563
},
1564-
// Node 1: index_elements at index 0extracts the whole match as a String
1564+
// Node 1: extract_element at index 0, extracts the whole match as a bare String (drops the row's start/end/name attributes since the unwrapped String can't carry them)
15651565
DocumentNode {
15661566
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
1567-
implementation: DocumentNodeImplementation::ProtoNode(graphic::index_elements::IDENTIFIER),
1567+
implementation: DocumentNodeImplementation::ProtoNode(graphic::extract_element::IDENTIFIER),
15681568
..Default::default()
15691569
},
1570-
// Node 2: omit_element at index 0returns capture groups as Vec<String>
1570+
// Node 2: omit_element at index 0, returns the capture group rows as a Table<String>, preserving each row's start/end/name attributes
15711571
DocumentNode {
15721572
inputs: vec![NodeInput::node(NodeId(0), 0), NodeInput::value(TaggedValue::F64(0.), false)],
15731573
implementation: DocumentNodeImplementation::ProtoNode(graphic::omit_element::IDENTIFIER),

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

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,49 @@ pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
6868
let index = index as i32;
6969

7070
if index < 0 {
71-
collection.omit_index_from_end(-index as usize)
71+
collection.omit_index_from_end(index.unsigned_abs() as usize)
7272
} else {
7373
collection.omit_index(index as usize)
7474
}
7575
}
7676

77+
/// Returns the bare element (without its row attributes) at the specified index in a table.
78+
/// Use this when downstream nodes want just the inner value rather than a single-row table.
79+
/// If no value exists at that index, the element type's default is returned.
80+
#[node_macro::node(category("General"))]
81+
pub fn extract_element<T: Clone + Default + Send + Sync + 'static>(
82+
_: impl Ctx,
83+
/// The table of data to extract from.
84+
#[implementations(
85+
Table<String>,
86+
Table<f64>,
87+
Table<u8>,
88+
Table<NodeId>,
89+
Table<Color>,
90+
Table<GradientStops>,
91+
Table<Vector>,
92+
Table<Raster<CPU>>,
93+
Table<Graphic>,
94+
Table<Artboard>,
95+
)]
96+
table: Table<T>,
97+
/// The index of the item to retrieve, starting from 0 for the first item. Negative indices count backwards from the end of the collection, starting from -1 for the last item.
98+
index: SignedInteger,
99+
) -> T {
100+
let len = table.len();
101+
let index = index as i32;
102+
let resolved = if index < 0 {
103+
let from_end = index.unsigned_abs() as usize;
104+
if from_end > len {
105+
return T::default();
106+
}
107+
len - from_end
108+
} else {
109+
index as usize
110+
};
111+
table.element(resolved).cloned().unwrap_or_default()
112+
}
113+
77114
#[node_macro::node(category("General"))]
78115
async fn map<Item: AnyHash + Send + Sync + core_types::CacheHash>(
79116
ctx: impl Ctx + CloneVarArgs + ExtractAll,

node-graph/nodes/text/src/json.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,13 @@ fn query_json(
210210
let mut results = Vec::new();
211211
resolve_all(&value, &segments, !unquote_strings, &mut results);
212212

213-
results.into_iter().next().unwrap_or_default()
213+
results.into_iter().next().map(|(text, _ty)| text).unwrap_or_default()
214214
}
215215

216216
/// Extracts every matched value from a JSON string using a path expression (see that parameter's description for its syntax). A list of zero or more resultant strings is produced. The `[]` path accessor is used to read more than one value.
217217
///
218+
/// Each row carries a `type` attribute holding the matched value's JSON type (`"string"`, `"number"`, `"bool"`, `"null"`, `"object"`, or `"array"`).
219+
///
218220
/// This is useful in conjunction with the nodes:
219221
/// • **Index Elements**: access the `N`th query result.
220222
/// • **String to Number**: convert numeric query results to numbers.
@@ -246,7 +248,7 @@ fn query_json_all(
246248
let mut results = Vec::new();
247249
resolve_all(&value, &segments, !unquote_strings, &mut results);
248250

249-
results.into_iter().map(TableRow::new_from_element).collect()
251+
results.into_iter().map(|(text, ty)| TableRow::new_from_element(text).with_attribute("type", ty.to_string())).collect()
250252
}
251253

252254
/// A parsed segment of a JSON access path.
@@ -402,6 +404,18 @@ fn json_value_to_string(value: &serde_json::Value, quote_strings: bool) -> Strin
402404
}
403405
}
404406

407+
/// Returns a short JSON-type name (`"string"`, `"number"`, `"bool"`, `"null"`, `"object"`, `"array"`) for a parsed value.
408+
fn json_value_type_name(value: &serde_json::Value) -> &'static str {
409+
match value {
410+
serde_json::Value::String(_) => "string",
411+
serde_json::Value::Number(_) => "number",
412+
serde_json::Value::Bool(_) => "bool",
413+
serde_json::Value::Null => "null",
414+
serde_json::Value::Object(_) => "object",
415+
serde_json::Value::Array(_) => "array",
416+
}
417+
}
418+
405419
/// Navigates a JSON value by one path segment, returning the resulting value (or `None` if the path is invalid).
406420
fn json_navigate<'a>(value: &'a serde_json::Value, segment: &JsonPathSegment) -> Option<&'a serde_json::Value> {
407421
match segment {
@@ -416,7 +430,7 @@ fn json_navigate<'a>(value: &'a serde_json::Value, segment: &JsonPathSegment) ->
416430
}
417431

418432
/// Recursively resolves a path against a JSON value, fanning out at each `[]` and collecting leaf results.
419-
fn resolve_all(value: &serde_json::Value, segments: &[JsonPathSegment], quote_strings: bool, results: &mut Vec<String>) {
433+
fn resolve_all(value: &serde_json::Value, segments: &[JsonPathSegment], quote_strings: bool, results: &mut Vec<(String, &'static str)>) {
420434
// Find the next IterateAll in the remaining segments
421435
let Some(iterate_position) = segments.iter().position(|s| matches!(s, JsonPathSegment::IterateAll)) else {
422436
// No more [] segments, navigate the rest linearly
@@ -425,7 +439,7 @@ fn resolve_all(value: &serde_json::Value, segments: &[JsonPathSegment], quote_st
425439
let Some(next) = json_navigate(current, segment) else { return };
426440
current = next;
427441
}
428-
results.push(json_value_to_string(current, quote_strings));
442+
results.push((json_value_to_string(current, quote_strings), json_value_type_name(current)));
429443
return;
430444
};
431445

node-graph/nodes/text/src/regex.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ fn regex_replace(
8080
/// Finds a regex match in the string and returns its components. The result is a list where the first element is the whole match (`$0`) and subsequent elements are the capture groups (`$1`, `$2`, etc., if any).
8181
///
8282
/// The match index selects which non-overlapping occurrence to return (0 for the first match). Returns an empty list if no match is found at the given index.
83+
///
84+
/// Each row carries `start` and `end` byte-offset attributes pointing into the original string, plus a `name` attribute holding
85+
/// the capture group's name (empty for unnamed groups, and for index 0 which is the whole match).
8386
#[node_macro::node(category(""))]
8487
fn regex_find(
8588
_: impl Ctx,
@@ -111,6 +114,9 @@ fn regex_find(
111114
return Table::new();
112115
};
113116

117+
// Capture group names indexed positionally; index 0 (the whole match) is always None.
118+
let capture_names: Vec<Option<String>> = regex.capture_names().map(|name| name.map(str::to_string)).collect();
119+
114120
// Collect all matches since we need to support negative indexing
115121
let matches: Vec<_> = regex.captures_iter(&string).filter_map(|c| c.ok()).collect();
116122

@@ -131,12 +137,20 @@ fn regex_find(
131137

132138
// Index 0 is the whole match, 1+ are capture groups
133139
(0..captures.len())
134-
.map(|i| captures.get(i).map_or(String::new(), |m| m.as_str().to_string()))
135-
.map(TableRow::new_from_element)
140+
.map(|i| {
141+
let captured = captures.get(i);
142+
let text = captured.map_or(String::new(), |m| m.as_str().to_string());
143+
let start = captured.map_or(0_u64, |m| m.start() as u64);
144+
let end = captured.map_or(0_u64, |m| m.end() as u64);
145+
let name = capture_names.get(i).cloned().flatten().unwrap_or_default();
146+
TableRow::new_from_element(text).with_attribute("start", start).with_attribute("end", end).with_attribute("name", name)
147+
})
136148
.collect()
137149
}
138150

139151
/// Finds all non-overlapping matches of a regular expression pattern in the string, returning a list of the matched substrings.
152+
///
153+
/// Each row carries `start` and `end` byte-offset attributes pointing into the original string.
140154
#[node_macro::node(category("Text: Regex"))]
141155
fn regex_find_all(
142156
_: impl Ctx,
@@ -169,8 +183,11 @@ fn regex_find_all(
169183
regex
170184
.find_iter(&string)
171185
.filter_map(|m| m.ok())
172-
.map(|m| m.as_str().to_string())
173-
.map(TableRow::new_from_element)
186+
.map(|m| {
187+
TableRow::new_from_element(m.as_str().to_string())
188+
.with_attribute("start", m.start() as u64)
189+
.with_attribute("end", m.end() as u64)
190+
})
174191
.collect()
175192
}
176193

0 commit comments

Comments
 (0)