Repository: zhiburt/tabled
Pull request #367 (2023-10-30 14:10:53)

tabled/ Add Content locator `ByContent`
ref #365 

todo: tests
todo: ByCondition ... (change the name?)
todo: Maybe create a abstract factory to create locators, e.g. `Locator::content(), Locator::condition(), Locator::column()`

_____

Just letting you @wbuck know that with it you can do the following.

https://github.com/zhiburt/tabled/blob/ad0c8eeb6487ea97eb875d697900831c612011d7/tabled/examples/target_content.rs#L45-L60

In your case something like a bellow example.

```rust
let ok_color = ...;
let cells_if_status_is_ok = Columns::single(2).and(ByContent::new("UP"));
Modify::new(cells_if_status_is_ok).with(ok_color);
```


Patch:
diff --git a/tabled/Cargo.toml b/tabled/Cargo.toml
index 4904b87..95b6d3e 100644
--- a/tabled/Cargo.toml
+++ b/tabled/Cargo.toml
@@ -269,6 +269,11 @@ name = "chess"
 path = "examples/chess.rs"
 required-features = ["std"]
 
+[[example]]
+name = "target_content"
+path = "examples/target_content.rs"
+required-features = ["std", "derive"]
+
 [features]
 default = ["derive", "macros"]
 std = ["papergrid/std"]
diff --git a/tabled/examples/disable.rs b/tabled/examples/disable.rs
index 1f04de7..9f82449 100644
--- a/tabled/examples/disable.rs
+++ b/tabled/examples/disable.rs
@@ -6,7 +6,7 @@
 
 use tabled::{
     settings::{
-        locator::ByColumnName,
+        location::ByColumnName,
         style::{Border, Style},
         Disable, Modify,
     },
diff --git a/tabled/examples/target_content.rs b/tabled/examples/target_content.rs
new file mode 100644
index 0000000..3bb3fc2
--- /dev/null
+++ b/tabled/examples/target_content.rs
@@ -0,0 +1,61 @@
+//! This example demonstrates using the [`Color`] [setting](tabled::settings) to
+//! stylize text, backgrounds, and borders.
+//!
+//! * 🚩 This example requires the `color` feature.
+//!
+//! * Note how [`Format::content()`] is used to break out [`CellOption`]
+//! specifications. This is helpful for organizing extensive [`Table`] configurations.
+
+use tabled::{
+    settings::{location::ByContent, style::Style, Color, Modify},
+    Table, Tabled,
+};
+
+#[derive(Tabled)]
+struct Job {
+    title: String,
+    #[tabled(display_with = "JobStatus::as_string")]
+    status: JobStatus,
+}
+
+impl Job {
+    fn new(title: &str, status: JobStatus) -> Self {
+        Self {
+            title: title.to_string(),
+            status,
+        }
+    }
+}
+
+enum JobStatus {
+    Open,
+    Closed,
+}
+
+impl JobStatus {
+    fn as_string(&self) -> &'static str {
+        match self {
+            JobStatus::Open => "open",
+            JobStatus::Closed => "closed",
+        }
+    }
+}
+
+fn main() {
+    let data = vec![
+        Job::new("C Developer", JobStatus::Open),
+        Job::new("Rust Developer", JobStatus::Closed),
+        Job::new("Kernel Developer", JobStatus::Open),
+    ];
+
+    let color_open_jobs = Color::BG_WHITE | Color::FG_BLACK;
+    let color_closed_jobs = Color::BG_GREEN | Color::FG_BLACK;
+
+    let mut table = Table::new(data);
+    table
+        .with(Style::empty())
+        .with(Modify::list(ByContent::new("open"), color_open_jobs))
+        .with(Modify::list(ByContent::new("closed"), color_closed_jobs));
+
+    println!("{table}");
+}
diff --git a/tabled/src/settings/disable/mod.rs b/tabled/src/settings/disable/mod.rs
index bfbf03d..45ffe31 100644
--- a/tabled/src/settings/disable/mod.rs
+++ b/tabled/src/settings/disable/mod.rs
@@ -15,7 +15,7 @@ use std::marker::PhantomData;
 
 use crate::{
     grid::records::{ExactRecords, Records, Resizable},
-    settings::{locator::Locator, TableOption},
+    settings::{location::Location, TableOption},
 };
 
 /// Disable removes particular rows/columns from a [`Table`].
@@ -66,7 +66,7 @@ impl<L> Disable<L, TargetColumn> {
     /// - [`ByColumnName`]
     ///
     /// ```rust
-    /// use tabled::{builder::Builder, settings::{Disable, locator::ByColumnName, object::Columns}};
+    /// use tabled::{builder::Builder, settings::{Disable, location::ByColumnName, object::Columns}};
     ///
     /// let mut builder = Builder::default();
     ///
@@ -151,7 +151,7 @@ pub struct TargetColumn;
 
 impl<L, R, D, C> TableOption<R, D, C> for Disable<L, TargetColumn>
 where
-    for<'a> L: Locator<&'a R, Coordinate = usize>,
+    L: Location<R, Coordinate = usize>,
     R: Records + Resizable,
 {
     fn change(mut self, records: &mut R, _: &mut C, _: &mut D) {
@@ -174,7 +174,7 @@ where
 
 impl<L, R, D, C> TableOption<R, D, C> for Disable<L, TargetRow>
 where
-    for<'a> L: Locator<&'a R, Coordinate = usize>,
+    L: Location<R, Coordinate = usize>,
     R: ExactRecords + Resizable,
 {
     fn change(mut self, records: &mut R, _: &mut C, _: &mut D) {
diff --git a/tabled/src/settings/location/by_column_name.rs b/tabled/src/settings/location/by_column_name.rs
new file mode 100644
index 0000000..64f18f8
--- /dev/null
+++ b/tabled/src/settings/location/by_column_name.rs
@@ -0,0 +1,56 @@
+use crate::{
+    grid::config::Entity,
+    grid::records::{ExactRecords, PeekableRecords, Records},
+    settings::location::Location,
+    settings::object::Object,
+};
+
+/// The structure is an implementation of [`Locator`] to search for a column by it's name.
+/// A name is considered be a value in a first row.
+///
+/// So even if in reality there's no header, the first row will be considered to be one.
+#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ByColumnName<S>(S);
+
+impl<S> ByColumnName<S> {
+    /// Constructs a new object of the structure.
+    pub fn new(text: S) -> Self
+    where
+        S: AsRef<str>,
+    {
+        Self(text)
+    }
+}
+
+impl<R, S> Location<R> for ByColumnName<S>
+where
+    S: AsRef<str>,
+    R: Records + ExactRecords + PeekableRecords,
+{
+    type Coordinate = usize;
+    type IntoIter = Vec<usize>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        // todo: can be optimized by creating Iterator
+        (0..records.count_columns())
+            .filter(|col| records.get_text((0, *col)) == self.0.as_ref())
+            .collect::<Vec<_>>()
+    }
+}
+
+impl<S, R> Object<R> for ByColumnName<S>
+where
+    S: AsRef<str>,
+    R: Records + PeekableRecords + ExactRecords,
+{
+    type Iter = std::vec::IntoIter<Entity>;
+
+    fn cells(&self, records: &R) -> Self::Iter {
+        // todo: can be optimized by creating Iterator
+        (0..records.count_columns())
+            .filter(|col| records.get_text((0, *col)) == self.0.as_ref())
+            .map(Entity::Column)
+            .collect::<Vec<_>>()
+            .into_iter()
+    }
+}
diff --git a/tabled/src/settings/location/by_condition.rs b/tabled/src/settings/location/by_condition.rs
new file mode 100644
index 0000000..de8492a
--- /dev/null
+++ b/tabled/src/settings/location/by_condition.rs
@@ -0,0 +1,74 @@
+use crate::{
+    grid::config::Entity,
+    grid::{
+        config::Position,
+        records::{ExactRecords, PeekableRecords, Records},
+    },
+    settings::location::Location,
+    settings::object::Object,
+};
+
+/// The structure is an implementation of [`Locator`] to search for cells with a specified condition.
+#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ByCondition<F>(F);
+
+impl<F> ByCondition<F> {
+    /// Constructs a new object of the structure.
+    pub fn new(search: F) -> Self
+    where
+        F: Fn(&str) -> bool,
+    {
+        Self(search)
+    }
+}
+
+impl<F, R> Location<R> for ByCondition<F>
+where
+    F: Fn(&str) -> bool,
+    R: Records + ExactRecords + PeekableRecords,
+{
+    type Coordinate = Position;
+    type IntoIter = Vec<Position>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        // todo: can be optimized by creating Iterator
+        let cond = &self.0;
+
+        let mut out = vec![];
+        for row in 0..records.count_rows() {
+            for col in 0..records.count_columns() {
+                let text = records.get_text((row, col));
+                if cond(text) {
+                    out.push((row, col));
+                }
+            }
+        }
+
+        out
+    }
+}
+
+impl<F, R> Object<R> for ByCondition<F>
+where
+    F: Fn(&str) -> bool,
+    R: Records + ExactRecords + PeekableRecords,
+{
+    type Iter = std::vec::IntoIter<Entity>;
+
+    fn cells(&self, records: &R) -> Self::Iter {
+        // todo: can be optimized by creating Iterator
+        let cond = &self.0;
+
+        let mut out = vec![];
+        for row in 0..records.count_rows() {
+            for col in 0..records.count_columns() {
+                let text = records.get_text((row, col));
+                if cond(text) {
+                    out.push(Entity::Cell(row, col));
+                }
+            }
+        }
+
+        out.into_iter()
+    }
+}
diff --git a/tabled/src/settings/location/by_content.rs b/tabled/src/settings/location/by_content.rs
new file mode 100644
index 0000000..a339bba
--- /dev/null
+++ b/tabled/src/settings/location/by_content.rs
@@ -0,0 +1,74 @@
+use crate::{
+    grid::config::Entity,
+    grid::{
+        config::Position,
+        records::{ExactRecords, PeekableRecords, Records},
+    },
+    settings::location::Location,
+    settings::object::Object,
+};
+
+/// The structure is an implementation of [`Locator`] to search for cells with a given content.
+#[derive(Debug, Clone, Copy, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
+pub struct ByContent<S>(S);
+
+impl<S> ByContent<S> {
+    /// Constructs a new object of the structure.
+    pub fn new(text: S) -> Self
+    where
+        S: AsRef<str>,
+    {
+        Self(text)
+    }
+}
+
+impl<R, S> Location<R> for ByContent<S>
+where
+    S: AsRef<str>,
+    R: Records + ExactRecords + PeekableRecords,
+{
+    type Coordinate = Position;
+    type IntoIter = Vec<Position>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        // todo: can be optimized by creating Iterator
+        let text = self.0.as_ref();
+
+        let mut out = vec![];
+        for row in 0..records.count_rows() {
+            for col in 0..records.count_columns() {
+                let cell = records.get_text((row, col));
+                if cell.eq(text) {
+                    out.push((row, col));
+                }
+            }
+        }
+
+        out
+    }
+}
+
+impl<S, R> Object<R> for ByContent<S>
+where
+    S: AsRef<str>,
+    R: Records + PeekableRecords + ExactRecords,
+{
+    type Iter = std::vec::IntoIter<Entity>;
+
+    fn cells(&self, records: &R) -> Self::Iter {
+        // todo: can be optimized by creating Iterator
+        let text = self.0.as_ref();
+
+        let mut out = vec![];
+        for row in 0..records.count_rows() {
+            for col in 0..records.count_columns() {
+                let cell = records.get_text((row, col));
+                if cell.eq(text) {
+                    out.push(Entity::Cell(row, col));
+                }
+            }
+        }
+
+        out.into_iter()
+    }
+}
diff --git a/tabled/src/settings/location/locator.rs b/tabled/src/settings/location/locator.rs
new file mode 100644
index 0000000..62269ed
--- /dev/null
+++ b/tabled/src/settings/location/locator.rs
@@ -0,0 +1,34 @@
+use super::{ByColumnName, ByCondition, ByContent};
+
+/// An abstract factory for locations, to be used to find different things on the table.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default, Hash)]
+pub struct Locator;
+
+impl Locator {
+    /// Constructs a new location searcher for a cells with a given content.
+    pub fn content<S>(text: S) -> ByContent<S>
+    where
+        S: AsRef<str>,
+    {
+        ByContent::new(text)
+    }
+
+    /// Constructs a new location searcher for a column by its header.
+    pub fn column<S>(text: S) -> ByColumnName<S>
+    where
+        S: AsRef<str>,
+    {
+        ByColumnName::new(text)
+    }
+
+    /// Constructs a new location searcher with a specified condition closure.
+    ///
+    /// Return `true` if it shall be included in output.
+    /// Otherwise return `false`.
+    pub fn by<F>(condition: F) -> ByCondition<F>
+    where
+        F: Fn(&str) -> bool,
+    {
+        ByCondition::new(condition)
+    }
+}
diff --git a/tabled/src/settings/location/mod.rs b/tabled/src/settings/location/mod.rs
new file mode 100644
index 0000000..28d0716
--- /dev/null
+++ b/tabled/src/settings/location/mod.rs
@@ -0,0 +1,409 @@
+//! The module contains a [`Location`] trait and implementations for it.
+//!
+//! # Example
+//!
+#![cfg_attr(feature = "derive", doc = "```")]
+#![cfg_attr(not(feature = "derive"), doc = "```ignore")]
+//! use tabled::{
+//!     settings::{
+//!         location::Locator,
+//!         object::{Columns, Object},
+//!         Alignment, Modify, Padding,
+//!     },
+//!     Table, Tabled,
+//! };
+//!
+//! #[derive(Tabled)]
+//! struct Reading {
+//!     link: &'static str,
+//!     comment: &'static str,
+//! }
+//!
+//! let data = [
+//!     Reading { link: "https://www.gnu.org/software/grub/manual/multiboot/multiboot.html", comment: "todo" },
+//!     Reading { link: "https://wiki.debian.org/initramfs", comment: "todo" },
+//!     Reading { link: "http://jdebp.uk/FGA/efi-boot-process.html", comment: "todo,2" },
+//!     Reading { link: "https://wiki.debian.org/UEFI", comment: "todo,2" },
+//! ];
+//!
+//! let mut table = Table::new(data);
+//! table.with(Padding::zero());
+//! table.with(Modify::new(Locator::column("link")).with(Alignment::right()));
+//! table.with(Modify::new(Locator::content("todo")).with("todo,1"));
+//! table.with(
+//!     Modify::new(Columns::single(1).intersect(Locator::by(|text| text.contains("todo"))))
+//!         .with(Padding::new(4, 0, 0, 0)),
+//! );
+//!
+//! let output = table.to_string();
+//!
+//! assert_eq!(
+//!     output,
+//!     concat!(
+//!         "+-----------------------------------------------------------------+----------+\n",
+//!         "|                                                             link|comment   |\n",
+//!         "+-----------------------------------------------------------------+----------+\n",
+//!         "|https://www.gnu.org/software/grub/manual/multiboot/multiboot.html|    todo,1|\n",
+//!         "+-----------------------------------------------------------------+----------+\n",
+//!         "|                                https://wiki.debian.org/initramfs|    todo,1|\n",
+//!         "+-----------------------------------------------------------------+----------+\n",
+//!         "|                        http://jdebp.uk/FGA/efi-boot-process.html|    todo,2|\n",
+//!         "+-----------------------------------------------------------------+----------+\n",
+//!         "|                                     https://wiki.debian.org/UEFI|    todo,2|\n",
+//!         "+-----------------------------------------------------------------+----------+",
+//!     ),
+//! );
+//! ```
+
+// todo: Add .modify method for Table
+
+mod by_column_name;
+mod by_condition;
+mod by_content;
+mod locator;
+
+pub use by_column_name::ByColumnName;
+pub use by_condition::ByCondition;
+pub use by_content::ByContent;
+pub use locator::Locator;
+
+use core::ops::Bound;
+use std::{
+    iter::{self, Once},
+    ops::{Range, RangeBounds},
+};
+
+use crate::{
+    grid::records::{ExactRecords, Records},
+    settings::object::{Column, Columns, FirstColumn, FirstRow, LastColumn, LastRow, Row, Rows},
+};
+
+/// Location is an interface which searches for a particular thing in the [`Records`],
+/// and returns coordinate of the foundings if any.
+pub trait Location<Records> {
+    /// A coordinate of the finding.
+    type Coordinate;
+    /// An iterator of the coordinates.
+    /// If it's empty it's considered that nothing is found.
+    type IntoIter: IntoIterator<Item = Self::Coordinate>;
+
+    /// Search for the thing in [`Records`], returning a list of coordinates.
+    fn locate(&mut self, records: &Records) -> Self::IntoIter;
+}
+
+impl<B, R> Location<R> for Columns<B>
+where
+    B: RangeBounds<usize>,
+    R: Records,
+{
+    type Coordinate = usize;
+    type IntoIter = Range<usize>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        let range = self.get_range();
+        let max = records.count_columns();
+        let (from, to) = bounds_to_usize(range.start_bound(), range.end_bound(), max);
+
+        from..to
+    }
+}
+
+impl<R> Location<R> for Column {
+    type Coordinate = usize;
+    type IntoIter = Once<usize>;
+
+    fn locate(&mut self, _: &R) -> Self::IntoIter {
+        iter::once((*self).into())
+    }
+}
+
+impl<R> Location<R> for FirstColumn {
+    type Coordinate = usize;
+    type IntoIter = Once<usize>;
+
+    fn locate(&mut self, _: &R) -> Self::IntoIter {
+        iter::once(0)
+    }
+}
+
+impl<R> Location<R> for LastColumn
+where
+    R: Records,
+{
+    type Coordinate = usize;
+    type IntoIter = Once<usize>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        if records.count_columns() > 0 {
+            iter::once(records.count_columns() - 1)
+        } else {
+            iter::once(0)
+        }
+    }
+}
+
+impl<B, R> Location<R> for Rows<B>
+where
+    R: Records,
+    B: RangeBounds<usize>,
+{
+    type Coordinate = usize;
+    type IntoIter = Range<usize>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        let (from, to) = bounds_to_usize(
+            self.get_range().start_bound(),
+            self.get_range().end_bound(),
+            records.count_columns(),
+        );
+
+        from..to
+    }
+}
+
+impl<R> Location<R> for Row {
+    type Coordinate = usize;
+    type IntoIter = Once<usize>;
+
+    fn locate(&mut self, _: &R) -> Self::IntoIter {
+        iter::once((*self).into())
+    }
+}
+
+impl<R> Location<R> for FirstRow {
+    type Coordinate = usize;
+    type IntoIter = Once<usize>;
+
+    fn locate(&mut self, _: &R) -> Self::IntoIter {
+        iter::once(0)
+    }
+}
+
+impl<R> Location<R> for LastRow
+where
+    R: ExactRecords,
+{
+    type Coordinate = usize;
+    type IntoIter = Once<usize>;
+
+    fn locate(&mut self, records: &R) -> Self::IntoIter {
+        if records.count_rows() > 0 {
+            iter::once(records.count_rows() - 1)
+        } else {
+            iter::once(0)
+        }
+    }
+}
+
+fn bounds_to_usize(
+    left: Bound<&usize>,
+    right: Bound<&usize>,
+    count_elements: usize,
+) -> (usize, usize) {
+    match (left, right) {
+        (Bound::Included(x), Bound::Included(y)) => (*x, y + 1),
+        (Bound::Included(x), Bound::Excluded(y)) => (*x, *y),
+        (Bound::Included(x), Bound::Unbounded) => (*x, count_elements),
+        (Bound::Unbounded, Bound::Unbounded) => (0, count_elements),
+        (Bound::Unbounded, Bound::Included(y)) => (0, y + 1),
+        (Bound::Unbounded, Bound::Excluded(y)) => (0, *y),
+        (Bound::Excluded(_), Bound::Unbounded)
+        | (Bound::Excluded(_), Bound::Included(_))
+        | (Bound::Excluded(_), Bound::Excluded(_)) => {
+            unreachable!("A start bound can't be excluded")
+        }
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        grid::config::Entity,
+        grid::records::vec_records::CellInfo,
+        grid::records::vec_records::VecRecords,
+        settings::location::{ByColumnName, ByCondition, ByContent},
+        settings::object::Object,
+    };
+
+    use Entity::*;
+
+    #[test]
+    fn object_by_column_name_test() {
+        let data = [
+            vec![vec![1, 2, 3], vec![1, 2, 3], vec![1, 2, 3]],
+            vec![vec![1, 2, 3], vec![1, 1, 3], vec![1, 2, 1]],
+            vec![vec![1, 1, 3], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![1, 1, 1], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![0, 1, 1], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![0, 0, 0], vec![1, 1, 3], vec![1, 1, 1]],
+        ];
+
+        assert_eq!(cells(by_colname("1"), &data[0]), [Column(0)]);
+        assert_eq!(cells(by_colname("1"), &data[1]), [Column(0)]);
+        assert_eq!(cells(by_colname("1"), &data[2]), [Column(0), Column(1)]);
+        assert_eq!(
+            cells(by_colname("1"), &data[3]),
+            [Column(0), Column(1), Column(2)]
+        );
+        assert_eq!(cells(by_colname("1"), &data[4]), [Column(1), Column(2)]);
+        assert_eq!(cells(by_colname("1"), &data[5]), []);
+    }
+
+    #[test]
+    fn object_by_content_test() {
+        let data = [
+            vec![vec![1, 2, 3], vec![1, 2, 3], vec![1, 2, 3]],
+            vec![vec![1, 2, 3], vec![1, 1, 3], vec![1, 2, 1]],
+            vec![vec![1, 1, 3], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![1, 1, 1], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![0, 1, 1], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![0, 0, 0], vec![1, 1, 3], vec![1, 1, 1]],
+        ];
+
+        assert_eq!(cells(by_content("1"), &[]), []);
+        assert_eq!(cells(by_content("1"), &[vec![], vec![], vec![]]), []);
+        assert_eq!(
+            cells(by_content("1"), &data[0]),
+            [Cell(0, 0), Cell(1, 0), Cell(2, 0)]
+        );
+        assert_eq!(
+            cells(by_content("1"), &data[1]),
+            [Cell(0, 0), Cell(1, 0), Cell(1, 1), Cell(2, 0), Cell(2, 2)]
+        );
+        assert_eq!(
+            cells(by_content("1"), &data[2]),
+            [
+                Cell(0, 0),
+                Cell(0, 1),
+                Cell(1, 0),
+                Cell(1, 1),
+                Cell(2, 0),
+                Cell(2, 1),
+                Cell(2, 2)
+            ]
+        );
+        assert_eq!(
+            cells(by_content("1"), &data[3]),
+            [
+                Cell(0, 0),
+                Cell(0, 1),
+                Cell(0, 2),
+                Cell(1, 0),
+                Cell(1, 1),
+                Cell(2, 0),
+                Cell(2, 1),
+                Cell(2, 2)
+            ]
+        );
+        assert_eq!(
+            cells(by_content("1"), &data[4]),
+            [
+                Cell(0, 1),
+                Cell(0, 2),
+                Cell(1, 0),
+                Cell(1, 1),
+                Cell(2, 0),
+                Cell(2, 1),
+                Cell(2, 2)
+            ]
+        );
+        assert_eq!(
+            cells(by_content("1"), &data[5]),
+            [Cell(1, 0), Cell(1, 1), Cell(2, 0), Cell(2, 1), Cell(2, 2)]
+        );
+    }
+
+    #[test]
+    fn object_by_condition_test() {
+        let data = [
+            vec![vec![1, 2, 3], vec![1, 2, 3], vec![1, 2, 3]],
+            vec![vec![1, 2, 3], vec![1, 1, 3], vec![1, 2, 1]],
+            vec![vec![1, 1, 3], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![1, 1, 1], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![0, 1, 1], vec![1, 1, 3], vec![1, 1, 1]],
+            vec![vec![0, 0, 0], vec![1, 1, 3], vec![1, 1, 1]],
+        ];
+
+        assert_eq!(cells(by_cond("1"), &[]), []);
+        assert_eq!(cells(by_cond("1"), &[vec![], vec![], vec![]]), []);
+        assert_eq!(
+            cells(by_cond("1"), &data[0]),
+            [Cell(0, 0), Cell(1, 0), Cell(2, 0)]
+        );
+        assert_eq!(
+            cells(by_cond("1"), &data[1]),
+            [Cell(0, 0), Cell(1, 0), Cell(1, 1), Cell(2, 0), Cell(2, 2)]
+        );
+        assert_eq!(
+            cells(by_cond("1"), &data[2]),
+            [
+                Cell(0, 0),
+                Cell(0, 1),
+                Cell(1, 0),
+                Cell(1, 1),
+                Cell(2, 0),
+                Cell(2, 1),
+                Cell(2, 2)
+            ]
+        );
+        assert_eq!(
+            cells(by_cond("1"), &data[3]),
+            [
+                Cell(0, 0),
+                Cell(0, 1),
+                Cell(0, 2),
+                Cell(1, 0),
+                Cell(1, 1),
+                Cell(2, 0),
+                Cell(2, 1),
+                Cell(2, 2)
+            ]
+        );
+        assert_eq!(
+            cells(by_cond("1"), &data[4]),
+            [
+                Cell(0, 1),
+                Cell(0, 2),
+                Cell(1, 0),
+                Cell(1, 1),
+                Cell(2, 0),
+                Cell(2, 1),
+                Cell(2, 2)
+            ]
+        );
+        assert_eq!(
+            cells(by_cond("1"), &data[5]),
+            [Cell(1, 0), Cell(1, 1), Cell(2, 0), Cell(2, 1), Cell(2, 2)]
+        );
+    }
+
+    fn by_colname(text: &str) -> ByColumnName<&str> {
+        ByColumnName::new(text)
+    }
+
+    fn by_content(text: &str) -> ByContent<&str> {
+        ByContent::new(text)
+    }
+
+    fn by_cond(text: &'static str) -> ByCondition<impl Fn(&str) -> bool> {
+        ByCondition::new(move |content| content == text)
+    }
+
+    fn cells<O>(o: O, data: &[Vec<usize>]) -> Vec<Entity>
+    where
+        O: Object<VecRecords<CellInfo<String>>>,
+    {
+        let data = data
+            .iter()
+            .map(|row| {
+                row.iter()
+                    .map(|n| n.to_string())
+                    .map(CellInfo::new)
+                    .collect()
+            })
+            .collect();
+
+        let records = VecRecords::new(data);
+        o.cells(&records).collect::<Vec<_>>()
+    }
+}
diff --git a/tabled/src/settings/locator/mod.rs b/tabled/src/settings/locator/mod.rs
deleted file mode 100644
index b48391b..0000000
--- a/tabled/src/settings/locator/mod.rs
+++ /dev/null
@@ -1,202 +0,0 @@
-//! The module contains a [`Locator`] trait and implementations for it.
-
-use core::ops::Bound;
-use std::{
-    iter::{self, Once},
-    ops::{Range, RangeBounds},
-};
-
-use crate::{
-    grid::config::Entity,
-    grid::records::{ExactRecords, PeekableRecords, Records},
-    settings::object::{
-        Column, Columns, FirstColumn, FirstRow, LastColumn, LastRow, Object, Row, Rows,
-    },
-};
-
-/// Locator is an interface which searches for a particular thing in the [`Records`],
-/// and returns coordinate of the foundings if any.
-pub trait Locator<Records> {
-    /// A coordinate of the finding.
-    type Coordinate;
-    /// An iterator of the coordinates.
-    /// If it's empty it's considered that nothing is found.
-    type IntoIter: IntoIterator<Item = Self::Coordinate>;
-
-    /// Search for the thing in [`Records`], returning a list of coordinates.
-    fn locate(&mut self, records: Records) -> Self::IntoIter;
-}
-
-impl<B, R> Locator<R> for Columns<B>
-where
-    B: RangeBounds<usize>,
-    R: Records,
-{
-    type Coordinate = usize;
-    type IntoIter = Range<usize>;
-
-    fn locate(&mut self, records: R) -> Self::IntoIter {
-        let range = self.get_range();
-        let max = records.count_columns();
-        let (from, to) = bounds_to_usize(range.start_bound(), range.end_bound(), max);
-
-        from..to
-    }
-}
-
-impl<R> Locator<R> for Column {
-    type Coordinate = usize;
-    type IntoIter = Once<usize>;
-
-    fn locate(&mut self, _: R) -> Self::IntoIter {
-        iter::once((*self).into())
-    }
-}
-
-impl<R> Locator<R> for FirstColumn {
-    type Coordinate = usize;
-    type IntoIter = Once<usize>;
-
-    fn locate(&mut self, _: R) -> Self::IntoIter {
-        iter::once(0)
-    }
-}
-
-impl<R> Locator<R> for LastColumn
-where
-    R: Records,
-{
-    type Coordinate = usize;
-    type IntoIter = Once<usize>;
-
-    fn locate(&mut self, records: R) -> Self::IntoIter {
-        if records.count_columns() > 0 {
-            iter::once(records.count_columns() - 1)
-        } else {
-            iter::once(0)
-        }
-    }
-}
-
-impl<B, R> Locator<R> for Rows<B>
-where
-    R: Records,
-    B: RangeBounds<usize>,
-{
-    type Coordinate = usize;
-    type IntoIter = Range<usize>;
-
-    fn locate(&mut self, records: R) -> Self::IntoIter {
-        let (from, to) = bounds_to_usize(
-            self.get_range().start_bound(),
-            self.get_range().end_bound(),
-            records.count_columns(),
-        );
-
-        from..to
-    }
-}
-
-impl<R> Locator<R> for Row {
-    type Coordinate = usize;
-    type IntoIter = Once<usize>;
-
-    fn locate(&mut self, _: R) -> Self::IntoIter {
-        iter::once((*self).into())
-    }
-}
-
-impl<R> Locator<R> for FirstRow {
-    type Coordinate = usize;
-    type IntoIter = Once<usize>;
-
-    fn locate(&mut self, _: R) -> Self::IntoIter {
-        iter::once(0)
-    }
-}
-
-impl<R> Locator<R> for LastRow
-where
-    R: ExactRecords,
-{
-    type Coordinate = usize;
-    type IntoIter = Once<usize>;
-
-    fn locate(&mut self, records: R) -> Self::IntoIter {
-        if records.count_rows() > 0 {
-            iter::once(records.count_rows() - 1)
-        } else {
-            iter::once(0)
-        }
-    }
-}
-
-/// The structure is an implementation of [`Locator`] to search for a column by it's name.
-/// A name is considered be a value in a first row.
-///
-/// So even if in reality there's no header, the first row will be considered to be one.
-#[derive(Debug, Clone, Copy)]
-pub struct ByColumnName<S>(S);
-
-impl<S> ByColumnName<S> {
-    /// Constructs a new object of the structure.
-    pub fn new(text: S) -> Self
-    where
-        S: AsRef<str>,
-    {
-        Self(text)
-    }
-}
-
-impl<R, S> Locator<R> for ByColumnName<S>
-where
-    S: AsRef<str>,
-    R: Records + ExactRecords + PeekableRecords,
-{
-    type Coordinate = usize;
-    type IntoIter = Vec<usize>;
-
-    fn locate(&mut self, records: R) -> Self::IntoIter {
-        // todo: can be optimized by creating Iterator
-        (0..records.count_columns())
-            .filter(|col| records.get_text((0, *col)) == self.0.as_ref())
-            .collect::<Vec<_>>()
-    }
-}
-
-impl<S, R> Object<R> for ByColumnName<S>
-where
-    S: AsRef<str>,
-    R: Records + PeekableRecords + ExactRecords,
-{
-    type Iter = std::vec::IntoIter<Entity>;
-
-    fn cells(&self, records: &R) -> Self::Iter {
-        // todo: can be optimized by creating Iterator
-        (0..records.count_columns())
-            .filter(|col| records.get_text((0, *col)) == self.0.as_ref())
-            .map(Entity::Column)
-            .collect::<Vec<_>>()
-            .into_iter()
-    }
-}
-
-fn bounds_to_usize(
-    left: Bound<&usize>,
-    right: Bound<&usize>,
-    count_elements: usize,
-) -> (usize, usize) {
-    match (left, right) {
-        (Bound::Included(x), Bound::Included(y)) => (*x, y + 1),
-        (Bound::Included(x), Bound::Excluded(y)) => (*x, *y),
-        (Bound::Included(x), Bound::Unbounded) => (*x, count_elements),
-        (Bound::Unbounded, Bound::Unbounded) => (0, count_elements),
-        (Bound::Unbounded, Bound::Included(y)) => (0, y + 1),
-        (Bound::Unbounded, Bound::Excluded(y)) => (0, *y),
-        (Bound::Excluded(_), Bound::Unbounded)
-        | (Bound::Excluded(_), Bound::Included(_))
-        | (Bound::Excluded(_), Bound::Excluded(_)) => {
-            unreachable!("A start bound can't be excluded")
-        }
-    }
-}
diff --git a/tabled/src/settings/mod.rs b/tabled/src/settings/mod.rs
index a89037c..238edbd 100644
--- a/tabled/src/settings/mod.rs
+++ b/tabled/src/settings/mod.rs
@@ -84,7 +84,7 @@ pub mod height;
 pub mod highlight;
 #[cfg(feature = "std")]
 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
-pub mod locator;
+pub mod location;
 #[cfg(feature = "std")]
 #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
 pub mod measurement;
diff --git a/tabled/src/settings/width/util.rs b/tabled/src/settings/width/util.rs
index 92cc48c..576b206 100644
--- a/tabled/src/settings/width/util.rs
+++ b/tabled/src/settings/width/util.rs
@@ -262,4 +262,46 @@ mod tests {
             "\u{1b}[5m\u{1b}[48;2;12;200;100m\u{1b}[33mC\u{1b}[25m\u{1b}[39m\u{1b}[49m"
         )
     }
+
+    #[test]
+    #[cfg(feature = "color")]
+    fn test_srip_osc() {
+        assert_eq!(
+            strip_osc("just a string here"),
+            (String::from("just a string here"), None)
+        );
+        assert_eq!(
+            strip_osc("/etc/rc.conf"),
+            (String::from("/etc/rc.conf"), None)
+        );
+        assert_eq!(
+            strip_osc(
+                "https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982"
+            ),
+            (String::from("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982"), None)
+        );
+
+        assert_eq!(
+            strip_osc(&build_link_prefix_suffix("just a string here")),
+            (String::default(), Some(String::from("just a string here")))
+        );
+        assert_eq!(
+            strip_osc(&build_link_prefix_suffix("/etc/rc.conf")),
+            (String::default(), Some(String::from("/etc/rc.conf")))
+        );
+        assert_eq!(
+            strip_osc(
+                &build_link_prefix_suffix("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982")
+            ),
+            (String::default(), Some(String::from("https://gitlab.com/finestructure/swiftpackageindex-builder/-/pipelines/1054655982")))
+        );
+
+        #[cfg(feature = "color")]
+        fn build_link_prefix_suffix(url: &str) -> String {
+            // https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+            let osc8 = "\x1b]8;;";
+            let st = "\x1b\\";
+            format!("{osc8}{url}{st}")
+        }
+    }
 }


Test patch:
diff --git a/tabled/tests/settings/alignment_test.rs b/tabled/tests/settings/alignment_test.rs
index 0a8145a..5bb080c 100644
--- a/tabled/tests/settings/alignment_test.rs
+++ b/tabled/tests/settings/alignment_test.rs
@@ -1,7 +1,7 @@
 #![cfg(feature = "std")]
 
 use tabled::settings::{
-    locator::ByColumnName,
+    location::ByColumnName,
     object::{Columns, Rows, Segment},
     Alignment, Modify, Padding, Style,
 };
diff --git a/tabled/tests/settings/disable_test.rs b/tabled/tests/settings/disable_test.rs
index cf50acf..0183e79 100644
--- a/tabled/tests/settings/disable_test.rs
+++ b/tabled/tests/settings/disable_test.rs
@@ -1,7 +1,7 @@
 #![cfg(feature = "std")]
 
 use tabled::settings::{
-    locator::ByColumnName,
+    location::ByColumnName,
     object::{Columns, Rows, Segment},
     style::{HorizontalLine, Style},
     Alignment, Disable, Modify,