Files
OneCAD-Draft/src/entities/text.rs
T
Karim Jerbi f1d5284fb9 fix(mtext): support arabic cursive shaping and bidirectional rtl text layout
- Enable fallback font metrics for HarfBuzz shaping in TTF glyph resolver

- Implement Unicode Bidirectional Algorithm (UBA) paragraph base direction detection

- Anchor right-to-left paragraphs to right container boundaries by default

- Reorder line atoms with base direction level to preserve neutral punctuation attachment

- Right-align RTL single-line TEXT and handle editor caret placement on RTL glyphs
2026-09-12 11:06:11 +01:00

763 lines
27 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use acadrust::entities::{Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA};
use crate::command::EntityTransform;
use crate::entities::common::{
edit_angle_prop as edit_angle, edit_prop as edit, num_prop as num_row, parse_f64, ro_prop as ro, square_grip,
};
use crate::entities::text_support::{
resolve_dxf_special_chars, resolve_text_style, text_local_bounds,
};
use crate::entities::traits::{Grippable, PropertyEditable, Transformable, RenderConvertible};
use crate::scene::convert::acad_to_render::{GlyphRun, TextStroke, RenderEntity, RenderObject};
use crate::scene::text::lff;
use crate::scene::model::object::{GripApply, GripDef, PropSection, PropValue, Property};
use crate::scene::model::wire_model::SnapHint;
use crate::t;
/// Combined single-line-text justification (horizontal × vertical) shown as one
/// dropdown value. Horizontal-only modes (Aligned/Middle/Fit) ignore the
/// vertical component the way the underlying alignment does.
fn text_justify_str(h: &HA, v: &VA) -> &'static str {
match (h, v) {
(HA::Aligned, _) => "Aligned",
(HA::Fit, _) => "Fit",
(HA::Middle, _) => "Middle",
(HA::Left, VA::Baseline) => "Left",
(HA::Center, VA::Baseline) => "Center",
(HA::Right, VA::Baseline) => "Right",
(HA::Left, VA::Top) => "Top left",
(HA::Center, VA::Top) => "Top center",
(HA::Right, VA::Top) => "Top right",
(HA::Left, VA::Middle) => "Middle left",
(HA::Center, VA::Middle) => "Middle center",
(HA::Right, VA::Middle) => "Middle right",
(HA::Left, VA::Bottom) => "Bottom left",
(HA::Center, VA::Bottom) => "Bottom center",
(HA::Right, VA::Bottom) => "Bottom right",
}
}
pub(crate) fn sync_text_alignment_point(t: &mut Text) {
let needs_alignment_point = !matches!(
(t.horizontal_alignment, t.vertical_alignment),
(HA::Left, VA::Baseline)
);
if needs_alignment_point {
if matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
let point = t.alignment_point.unwrap_or(t.insertion_point);
let dx = point.x - t.insertion_point.x;
let dy = point.y - t.insertion_point.y;
if dx.hypot(dy) <= 1.0e-9 {
let span = t.height.max(1.0e-6)
* t.width_factor.abs().max(0.01)
* t.value.chars().count().max(1) as f64
* 0.6;
t.alignment_point = Some(acadrust::types::Vector3::new(
t.insertion_point.x + t.rotation.cos() * span,
t.insertion_point.y + t.rotation.sin() * span,
t.insertion_point.z,
));
} else {
t.alignment_point = Some(point);
}
} else if t.alignment_point.is_none() {
t.alignment_point = Some(t.insertion_point);
}
} else {
t.alignment_point = None;
}
}
fn two_point_span(t: &Text) -> Option<(f64, f64)> {
if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) {
return None;
}
let point = t.alignment_point?;
let dx = point.x - t.insertion_point.x;
let dy = point.y - t.insertion_point.y;
let distance = dx.hypot(dy);
(distance > 1.0e-9).then(|| (distance, dy.atan2(dx)))
}
fn displayed_rotation(t: &Text) -> f64 {
two_point_span(t).map_or(t.rotation, |(_, angle)| angle)
}
/// Resolved placement of a TEXT run: the baseline-anchored run origin (WCS xy)
/// plus every parameter needed to lay the glyphs out. Shared by `to_render` (the
/// stroke path) and the SDF-quad text collector so both place text identically.
pub struct TextPlacement {
/// Run-local origin (glyph space `[0,0]` maps here), WCS xy, kept f64.
pub origin: [f64; 2],
pub height: f32,
pub rotation: f32,
pub width_factor: f32,
pub oblique_angle: f32,
pub font: String,
/// Raw entity value (as passed to the tessellator).
pub value: String,
/// Full WCS insertion point, for the Insertion snap.
pub wcs_insertion: [f64; 3],
}
/// Parse a TEXT value's `%%` control codes through acadrust's `parse_plain_text`
/// (the same parser MTEXT uses), then re-encode into the stroke tessellator's
/// inline grammar: specials arrive resolved to Unicode, and `%%u`/`%%o`
/// underline/overline become `\L…\l` / `\O…\o` decoration markers. This keeps
/// TEXT parsing in acadrust rather than OCS's own tokenizer.
pub(crate) fn acad_text_encode(value: &str) -> String {
use acadrust::entities::mtext_format::parse_plain_text;
let doc = parse_plain_text(value);
let mut out = String::new();
for para in &doc.paragraphs {
for span in &para.spans {
let (u, o) = (span.properties.underline(), span.properties.overline());
if u {
out.push_str("\\L");
}
if o {
out.push_str("\\O");
}
out.push_str(&span.text);
if o {
out.push_str("\\o");
}
if u {
out.push_str("\\l");
}
}
}
out
}
pub(crate) fn to_render_at_scale(
t: &Text,
document: &acadrust::CadDocument,
annotation_scale: f32,
) -> RenderEntity {
let p = text_run_placement_at_scale(t, document, annotation_scale);
let snap_pt = glam::DVec3::new(p.wcs_insertion[0], p.wcs_insertion[1], p.wcs_insertion[2]);
// Parse `%%` codes via acadrust, re-encoded for the stroke tessellator.
let value = acad_text_encode(&p.value);
// Strokes are in glyph-local space (origin = [0,0]).
let (strokes, fill_tris) = lff::tessellate_text_ex(
[0.0, 0.0],
p.height,
p.rotation,
p.width_factor,
p.oblique_angle,
&p.font,
&value,
);
RenderEntity {
pick_tris: Vec::new(),
object: RenderObject::Text(vec![TextStroke {
strokes,
origin: p.origin,
color: None,
fill_tris,
plane: None,
run: Some(GlyphRun {
text: value,
font: p.font.clone(),
height: p.height,
rotation: p.rotation,
width_factor: p.width_factor,
oblique: p.oblique_angle,
tracking: 1.0,
bold: false,
}),
}]),
snap_pts: vec![(snap_pt, SnapHint::Insertion)],
tangent_geoms: vec![],
key_vertices: vec![],
fill_tris: vec![],
}
}
/// Compute a TEXT entity's run placement (origin + layout params). Extracted
/// from `to_render` verbatim so the stroke and SDF-quad paths agree exactly.
pub fn text_run_placement_at_scale(
t: &Text,
document: &acadrust::CadDocument,
annotation_scale: f32,
) -> TextPlacement {
let annotation_scale = if annotation_scale.is_finite() && annotation_scale > 1.0e-9 {
annotation_scale
} else {
1.0
};
let normal = (t.normal.x, t.normal.y, t.normal.z);
let (wsx, wsy, wsz) = crate::scene::view::transform::ocs_point_to_wcs(
(
t.insertion_point.x,
t.insertion_point.y,
t.insertion_point.z,
),
normal,
);
let resolved_style = resolve_text_style(&t.style, document);
let font_name = resolved_style.font_name;
// The entity stores the final width factor and oblique angle copied from
// its style at creation. Only fall back to the style when an omitted field
// was read as zero.
let base_wf = if t.width_factor.abs() > 1e-9 {
(t.width_factor as f32).clamp(0.01, 100.0)
} else {
resolved_style.width_factor.max(0.01)
};
// Backward mirrors text left-right (negative width factor); upside-down
// rotates 180° about the anchor. The effective state is the TextStyle flag
// XOR the entity's own generation flags (DXF group 71: bit 2 = backward,
// bit 4 = upside-down). A MIRROR toggles the entity bit for a true glyph
// mirror, and XOR keeps a double mirror an involution.
let eff_backward = resolved_style.is_backward ^ (t.generation_flags & 0x2 != 0);
let eff_upside = resolved_style.is_upside_down ^ (t.generation_flags & 0x4 != 0);
let mut width_factor = if eff_backward { -base_wf } else { base_wf };
let oblique_angle = if t.oblique_angle.abs() > 1e-9 {
t.oblique_angle as f32
} else {
resolved_style.oblique_angle
};
let value_for_bounds = resolve_dxf_special_chars(&t.value);
let mut height = t.height.max(1.0e-9) as f32;
let mut base_rotation = t.rotation as f32;
// Aligned and Fit are true two-point modes. Both derive their baseline
// direction from the endpoints. Aligned scales height uniformly; Fit keeps
// the height and changes only the horizontal factor.
if let Some((span, angle)) = two_point_span(t) {
base_rotation = angle as f32;
if let Some(base_bounds) = text_local_bounds(
&font_name,
&value_for_bounds,
height,
width_factor,
oblique_angle,
) {
if base_bounds.advance > 1.0e-6 {
let scale =
(span as f32 / annotation_scale / base_bounds.advance).max(1.0e-6);
if matches!(t.horizontal_alignment, HA::Aligned) {
height *= scale;
} else {
width_factor *= scale;
}
}
}
}
let rotation = if eff_upside {
base_rotation + std::f32::consts::PI
} else {
base_rotation
};
// Anchor stays f64: large coordinates (UTM etc.) lose ~0.5 units of
// precision when cast to f32, which snaps text baselines onto a coarse
// grid and makes adjacent rows collide. Only the small local offsets
// below are computed in f32.
let anchor: [f64; 2] = match (
&t.horizontal_alignment,
&t.vertical_alignment,
&t.alignment_point,
) {
(HA::Aligned | HA::Fit, _, _) => [t.insertion_point.x, t.insertion_point.y],
(HA::Middle, _, Some(a)) => [a.x, a.y],
(HA::Center | HA::Right, _, Some(a)) => [a.x, a.y],
(_, VA::Bottom | VA::Middle | VA::Top, Some(a)) => [a.x, a.y],
_ => [t.insertion_point.x, t.insertion_point.y],
};
let bounds = text_local_bounds(
&font_name,
&value_for_bounds,
height,
width_factor,
oblique_angle,
);
let (anchor_local_x, anchor_local_y) = if let Some(b) = bounds {
// Horizontal anchor uses the pen advance box [0, advance] so leading /
// trailing spaces keep their width instead of snapping to the inked
// glyphs. Signed by the width factor: backward text grows in −x, so its
// Center/Right offset flips sign too, otherwise the anchor pushes the
// box one way while the strokes run the other (the bounds advance is
// always positive — it uses |width_factor|). Left keeps its 0 reference.
let sign = width_factor.signum();
let is_rtl = {
let mut strong_rtl = false;
for ch in value_for_bounds.chars() {
match unicode_bidi::bidi_class(ch) {
unicode_bidi::BidiClass::R | unicode_bidi::BidiClass::AL => {
strong_rtl = true;
break;
}
unicode_bidi::BidiClass::L => {
break;
}
_ => {}
}
}
strong_rtl
};
let ax = match t.horizontal_alignment {
HA::Left => {
if is_rtl {
b.advance * sign
} else {
0.0
}
}
HA::Center | HA::Middle => b.advance * 0.5 * sign,
HA::Right => {
if is_rtl {
0.0
} else {
b.advance * sign
}
}
HA::Aligned | HA::Fit => 0.0,
};
// Vertical anchor uses the inked extent (cap / baseline geometry).
let ay = match t.vertical_alignment {
VA::Baseline => 0.0,
VA::Bottom => b.ink_min[1],
VA::Middle => (b.ink_min[1] + b.ink_max[1]) * 0.5,
VA::Top => b.ink_max[1],
};
(ax, ay)
} else {
(0.0, 0.0)
};
let (cos_r, sin_r) = (rotation.cos() as f64, rotation.sin() as f64);
// Keep origin as f64 — large coordinates (UTM etc.) must not be cast to
// f32 here; world_offset subtraction happens later in tessellate.rs.
let anchor_f64 = anchor;
let origin: [f64; 2] = [
anchor_f64[0] - (anchor_local_x as f64 * cos_r - anchor_local_y as f64 * sin_r),
anchor_f64[1] - (anchor_local_x as f64 * sin_r + anchor_local_y as f64 * cos_r),
];
TextPlacement {
origin,
height,
rotation,
width_factor,
oblique_angle,
font: font_name,
value: t.value.clone(),
wcs_insertion: [wsx, wsy, wsz],
}
}
fn grips(t: &Text) -> Vec<GripDef> {
let insertion = glam::DVec3::new(
t.insertion_point.x,
t.insertion_point.y,
t.insertion_point.z,
);
let mut grips = vec![square_grip(0, insertion)];
if let Some(point) = t.alignment_point {
grips.push(square_grip(
1,
glam::DVec3::new(point.x, point.y, point.z),
));
}
grips
}
fn properties(t: &Text, text_style_names: &[String]) -> Vec<PropSection> {
// Which geometry rows are live depends on justification. Plain Left text is
// anchored by the insertion point and has no second alignment point; every
// other justification anchors on the alignment point (and recomputes the
// insertion point), except Aligned/Fit which are true two-point spans where
// both points are live.
let is_plain_left = matches!(t.horizontal_alignment, HA::Left)
&& matches!(t.vertical_alignment, VA::Baseline);
let is_aligned = matches!(t.horizontal_alignment, HA::Aligned);
let is_two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
// The visible Properties palette exposes Text alignment as calculated
// coordinates and Position as the editable placement point. For aligned
// and fit text Position targets the first endpoint; for every other
// non-left mode it targets the alignment anchor that actually places the
// glyph run.
let position_uses_alignment = !is_plain_left && !is_two_point;
let ap = t.alignment_point.unwrap_or(t.insertion_point);
let (ax, ay, az) = if !is_plain_left {
(ap.x, ap.y, ap.z)
} else {
(0.0, 0.0, 0.0)
};
let position = if position_uses_alignment {
ap
} else {
t.insertion_point
};
vec![
PropSection {
title: t!("Text").into_owned(),
props: vec![
Property {
label: t!("Contents").into_owned(),
field: "content",
value: PropValue::PlainText(t.value.clone()),
},
Property {
label: t!("Style").into_owned(),
field: "style",
value: PropValue::Choice {
selected: if t.style.trim().is_empty() {
"Standard".into()
} else {
t.style.clone()
},
options: text_style_names.to_vec(),
},
},
ro(t!("Annotative").as_ref(), "annotative", "No"),
Property {
label: t!("Justify").into_owned(),
field: "justify",
value: PropValue::Choice {
selected: text_justify_str(
&t.horizontal_alignment,
&t.vertical_alignment,
)
.to_string(),
options: [
"Left",
"Center",
"Right",
"Aligned",
"Middle",
"Fit",
"Top left",
"Top center",
"Top right",
"Middle left",
"Middle center",
"Middle right",
"Bottom left",
"Bottom center",
"Bottom right",
]
.into_iter()
.map(str::to_string)
.collect(),
},
},
// A style that fixes its height fixes this text's too, so the
// value shows but cannot be changed here.
crate::entities::common::num_prop(
t!("Height").as_ref(),
"height",
t.height,
!is_aligned
&& crate::entities::common::style_fixed_height(&t.style).is_none(),
),
if is_two_point {
ro(
t!("Rotation").as_ref(),
"rotation",
crate::entities::common::format_angle(displayed_rotation(t)),
)
} else {
edit_angle(t!("Rotation").as_ref(), "rotation", t.rotation.to_degrees())
},
edit(t!("Width factor").as_ref(), "width_factor", t.width_factor),
edit_angle(t!("Obliquing").as_ref(), "oblique_angle", t.oblique_angle.to_degrees()),
num_row(t!("Text alignment X").as_ref(), "align_x", ax, false),
num_row(t!("Text alignment Y").as_ref(), "align_y", ay, false),
num_row(t!("Text alignment Z").as_ref(), "align_z", az, false),
],
},
PropSection {
title: t!("Geometry").into_owned(),
props: vec![
num_row(t!("Position X").as_ref(), "ins_x", position.x, true),
num_row(t!("Position Y").as_ref(), "ins_y", position.y, true),
num_row(t!("Position Z").as_ref(), "ins_z", position.z, true),
],
},
PropSection {
title: t!("Misc").into_owned(),
props: vec![
Property {
label: t!("Upside down").into_owned(),
field: "upside_down",
value: PropValue::BoolToggle {
field: "upside_down",
value: t.generation_flags & 0x4 != 0,
},
},
Property {
label: t!("Backward").into_owned(),
field: "backward",
value: PropValue::BoolToggle {
field: "backward",
value: t.generation_flags & 0x2 != 0,
},
},
],
},
]
}
fn apply_geom_prop(t: &mut Text, field: &str, value: &str) {
match field {
"content" => {
t.value = value.to_string();
return;
}
"style" => {
t.style = value.to_string();
return;
}
"justify" => {
let (h, v) = match value {
"Left" => (HA::Left, VA::Baseline),
"Center" => (HA::Center, VA::Baseline),
"Right" => (HA::Right, VA::Baseline),
"Aligned" => (HA::Aligned, VA::Baseline),
"Middle" => (HA::Middle, VA::Baseline),
"Fit" => (HA::Fit, VA::Baseline),
"Top left" => (HA::Left, VA::Top),
"Top center" => (HA::Center, VA::Top),
"Top right" => (HA::Right, VA::Top),
"Middle left" => (HA::Left, VA::Middle),
"Middle center" => (HA::Center, VA::Middle),
"Middle right" => (HA::Right, VA::Middle),
"Bottom left" => (HA::Left, VA::Bottom),
"Bottom center" => (HA::Center, VA::Bottom),
"Bottom right" => (HA::Right, VA::Bottom),
_ => return,
};
t.horizontal_alignment = h;
t.vertical_alignment = v;
sync_text_alignment_point(t);
return;
}
"upside_down" => {
let set = if value == "toggle" {
t.generation_flags & 0x4 == 0
} else {
value == "true"
};
if set {
t.generation_flags |= 0x4;
} else {
t.generation_flags &= !0x4;
}
return;
}
"backward" => {
let set = if value == "toggle" {
t.generation_flags & 0x2 == 0
} else {
value == "true"
};
if set {
t.generation_flags |= 0x2;
} else {
t.generation_flags &= !0x2;
}
return;
}
_ => {}
}
let Some(v) = parse_f64(value) else {
return;
};
match field {
"ins_x" | "ins_y" | "ins_z" => {
let plain_left = matches!(t.horizontal_alignment, HA::Left)
&& matches!(t.vertical_alignment, VA::Baseline);
let two_point = matches!(t.horizontal_alignment, HA::Aligned | HA::Fit);
let target = if !plain_left && !two_point {
let insertion = t.insertion_point;
t.alignment_point.get_or_insert(insertion)
} else {
&mut t.insertion_point
};
match field {
"ins_x" => target.x = v,
"ins_y" => target.y = v,
_ => target.z = v,
}
}
"align_x" | "align_y" | "align_z" => {
// Calculated display rows are intentionally not writable.
return;
}
"height"
if v > 0.0 && !matches!(t.horizontal_alignment, HA::Aligned) =>
{
t.height = v
}
"rotation" if !matches!(t.horizontal_alignment, HA::Aligned | HA::Fit) => {
t.rotation = v.to_radians()
}
"width_factor" if v > 0.0 => t.width_factor = v,
"oblique_angle" if (-85.0..=85.0).contains(&v) => {
t.oblique_angle = v.to_radians()
}
_ => {}
}
}
fn apply_grip(t: &mut Text, grip_id: usize, apply: GripApply) {
match apply {
GripApply::Absolute(p) => {
let target = if grip_id == 1 {
let insertion = t.insertion_point;
t.alignment_point.get_or_insert(insertion)
} else {
&mut t.insertion_point
};
target.x = p.x;
target.y = p.y;
target.z = p.z;
}
GripApply::Translate(d) => {
t.insertion_point.x += d.x;
t.insertion_point.y += d.y;
t.insertion_point.z += d.z;
if let Some(point) = t.alignment_point.as_mut() {
point.x += d.x;
point.y += d.y;
point.z += d.z;
}
}
}
}
fn apply_transform(t: &mut Text, tr: &EntityTransform) {
crate::scene::view::transform::apply_standard_entity_transform(t, tr, |entity, p1, p2| {
crate::scene::view::transform::reflect_xy_point(
&mut entity.insertion_point.x,
&mut entity.insertion_point.y,
p1,
p2,
);
if let Some(ref mut a) = entity.alignment_point {
crate::scene::view::transform::reflect_xy_point(&mut a.x, &mut a.y, p1, p2);
}
let dx = (p2.x - p1.x) as f64;
let dy = (p2.y - p1.y) as f64;
let line_angle = dy.atan2(dx);
entity.rotation = 2.0 * line_angle - entity.rotation;
entity.oblique_angle = -entity.oblique_angle;
});
}
impl RenderConvertible for Text {
fn to_render(&self, document: &acadrust::CadDocument) -> Option<RenderEntity> {
Some(to_render_at_scale(self, document, 1.0))
}
}
impl Grippable for Text {
fn grips(&self) -> Vec<GripDef> {
grips(self)
}
fn apply_grip(&mut self, grip_id: usize, apply: GripApply) {
apply_grip(self, grip_id, apply);
}
fn grip_menu(&self, _grip_id: usize) -> Vec<crate::scene::model::object::GripMenuItem> {
use crate::scene::model::object::{GripMenuAction, GripMenuItem};
vec![
GripMenuItem {
label: "Stretch",
action: GripMenuAction::Stretch,
},
GripMenuItem {
label: "Move with Text",
action: GripMenuAction::MoveWithText,
},
GripMenuItem {
label: "Rotate",
action: GripMenuAction::RotateText,
},
]
}
fn apply_grip_menu(&mut self, _grip_id: usize, _action: crate::scene::model::object::GripMenuAction) {
// Move-with-Text falls through to Stretch (single grip moves
// the whole text); Rotate needs a follow-up angle handled by
// `apply_grip_menu_value`.
}
fn grip_menu_value_prompt(
&self,
_grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
) -> Option<&'static str> {
use crate::scene::model::object::GripMenuAction as A;
match action {
A::RotateText => Some("Rotation (deg)"),
_ => None,
}
}
fn apply_grip_menu_value(
&mut self,
_grip_id: usize,
action: crate::scene::model::object::GripMenuAction,
value: f64,
) {
use crate::scene::model::object::GripMenuAction as A;
if matches!(action, A::RotateText)
&& !matches!(self.horizontal_alignment, HA::Aligned | HA::Fit)
{
self.rotation = value.to_radians();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_arabic_text_placement() {
let mut t = Text::default();
t.value = "بسم الله".to_string();
t.height = 2.5;
t.insertion_point = acadrust::types::Vector3::new(10.0, 20.0, 0.0);
t.horizontal_alignment = HA::Left;
let doc = acadrust::CadDocument::default();
let placement = text_run_placement_at_scale(&t, &doc, 1.0);
// For Arabic text with default HA::Left, origin should be shifted left
// so the right edge of the text sits at the insertion point (X = 10.0).
assert!(placement.origin[0] < 10.0, "Arabic text origin must be to the left of insertion point: origin_x={}", placement.origin[0]);
}
}
impl PropertyEditable for Text {
fn geometry_properties(&self, text_style_names: &[String]) -> Vec<PropSection> {
properties(self, text_style_names)
}
fn apply_geom_prop(&mut self, field: &str, value: &str) {
apply_geom_prop(self, field, value);
}
}
impl Transformable for Text {
fn apply_transform(&mut self, t: &EntityTransform) {
apply_transform(self, t);
}
}
impl crate::entities::traits::TextContent for acadrust::entities::Text {
fn text_content(&self) -> Option<String> {
Some(self.value.clone())
}
fn replace_text(&mut self, search: &str, rep: &str) {
let search_lc = search.to_lowercase();
if self.value.to_lowercase().contains(&search_lc) {
self.value = self.value.replace(search, rep);
}
}
}