aboutsummaryrefslogtreecommitdiff
path: root/alacritty_terminal/src/url.rs
diff options
context:
space:
mode:
Diffstat (limited to 'alacritty_terminal/src/url.rs')
-rw-r--r--alacritty_terminal/src/url.rs331
1 files changed, 27 insertions, 304 deletions
diff --git a/alacritty_terminal/src/url.rs b/alacritty_terminal/src/url.rs
index c3c70267..f1b7934b 100644
--- a/alacritty_terminal/src/url.rs
+++ b/alacritty_terminal/src/url.rs
@@ -1,318 +1,41 @@
-// Copyright 2016 Joe Wilm, The Alacritty Project Contributors
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
+use crate::ansi::TermInfo;
+use crate::index::{Column, Linear, Point};
+use crate::term::Term;
+use std::ops::RangeInclusive;
-use unicode_width::UnicodeWidthChar;
-
-use crate::term::cell::{Cell, Flags};
-
-// See https://tools.ietf.org/html/rfc3987#page-13
-const URL_SEPARATOR_CHARS: [char; 10] = ['<', '>', '"', ' ', '{', '}', '|', '\\', '^', '`'];
-const URL_DENY_END_CHARS: [char; 7] = ['.', ',', ';', ':', '?', '!', '('];
-const URL_SCHEMES: [&str; 8] =
- ["http://", "https://", "mailto:", "news:", "file://", "git://", "ssh://", "ftp://"];
-
-/// URL text and origin of the original click position.
-#[derive(Debug, PartialEq)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd)]
pub struct Url {
- pub text: String,
- pub origin: usize,
+ pub start: Point<usize>,
+ pub end: Point<usize>,
}
-/// Parser for streaming inside-out detection of URLs.
-pub struct UrlParser {
- state: String,
- origin: usize,
-}
-
-impl UrlParser {
- pub fn new() -> Self {
- UrlParser { state: String::new(), origin: 0 }
- }
-
- /// Advance the parser one character to the left.
- pub fn advance_left(&mut self, cell: &Cell) -> bool {
- if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
- self.origin += 1;
- return false;
- }
+impl Url {
+ pub fn new(start: Point<usize>, length: usize, num_cols: usize) -> Self {
+ let unwrapped_end_col = start.col.0 + length - 1;
+ let end_col = unwrapped_end_col % num_cols;
+ let end_line = start.line - unwrapped_end_col / num_cols;
- if self.advance(cell.c, 0) {
- true
- } else {
- self.origin += 1;
- false
- }
+ Url { end: Point::new(end_line, Column(end_col)), start }
}
- /// Advance the parser one character to the right.
- pub fn advance_right(&mut self, cell: &Cell) -> bool {
- if cell.flags.contains(Flags::WIDE_CHAR_SPACER) {
- return false;
- }
-
- self.advance(cell.c, self.state.len())
+ pub fn contains(&self, point: impl Into<Point<usize>>) -> bool {
+ let point = point.into();
+ point.line <= self.start.line
+ && point.line >= self.end.line
+ && (point.line != self.start.line || point.col >= self.start.col)
+ && (point.line != self.end.line || point.col <= self.end.col)
}
- /// Returns the URL if the parser has found any.
- pub fn url(mut self) -> Option<Url> {
- // Remove non-alphabetical characters before the scheme
- // https://tools.ietf.org/html/rfc3986#section-3.1
- if let Some(index) = self.state.find("://") {
- let iter =
- self.state.char_indices().rev().skip_while(|(byte_index, _)| *byte_index >= index);
- for (byte_index, c) in iter {
- match c {
- 'a'..='z' | 'A'..='Z' => (),
- _ => {
- self.origin =
- self.origin.saturating_sub(byte_index + c.width().unwrap_or(1));
- self.state = self.state.split_off(byte_index + c.len_utf8());
- break;
- },
- }
- }
- }
-
- // Remove non-matching parenthesis and brackets
- let mut open_parens_count: isize = 0;
- let mut open_bracks_count: isize = 0;
- for (i, c) in self.state.char_indices() {
- match c {
- '(' => open_parens_count += 1,
- ')' if open_parens_count > 0 => open_parens_count -= 1,
- '[' => open_bracks_count += 1,
- ']' if open_bracks_count > 0 => open_bracks_count -= 1,
- ')' | ']' => {
- self.state.truncate(i);
- break;
- },
- _ => (),
- }
- }
-
- // Track number of quotes
- let mut num_quotes = self.state.chars().filter(|&c| c == '\'').count();
-
- // Remove all characters which aren't allowed at the end of a URL
- while !self.state.is_empty()
- && (URL_DENY_END_CHARS.contains(&self.state.chars().last().unwrap())
- || (num_quotes % 2 != 0 && self.state.ends_with('\''))
- || self.state.ends_with("''")
- || self.state.ends_with("()"))
- {
- if self.state.pop().unwrap() == '\'' {
- num_quotes -= 1;
- }
- }
-
- // Check if string is valid url
- if self.origin > 0 && url::Url::parse(&self.state).is_ok() {
- for scheme in &URL_SCHEMES {
- if self.state.starts_with(scheme) {
- return Some(Url { origin: self.origin - 1, text: self.state });
- }
- }
- }
-
- None
- }
+ pub fn linear_bounds(&self, terminal: &Term) -> RangeInclusive<Linear> {
+ let mut start = self.start;
+ let mut end = self.end;
- fn advance(&mut self, c: char, pos: usize) -> bool {
- if URL_SEPARATOR_CHARS.contains(&c)
- || (c >= '\u{00}' && c <= '\u{1F}')
- || (c >= '\u{7F}' && c <= '\u{9F}')
- {
- true
- } else {
- self.state.insert(pos, c);
- false
- }
- }
-}
-
-#[cfg(test)]
-mod tests {
- use std::mem;
-
- use unicode_width::UnicodeWidthChar;
-
- use crate::clipboard::Clipboard;
- use crate::grid::Grid;
- use crate::index::{Column, Line, Point};
- use crate::message_bar::MessageBuffer;
- use crate::term::cell::{Cell, Flags};
- use crate::term::{Search, SizeInfo, Term};
-
- fn url_create_term(input: &str) -> Term {
- let size = SizeInfo {
- width: 21.0,
- height: 51.0,
- cell_width: 3.0,
- cell_height: 3.0,
- padding_x: 0.0,
- padding_y: 0.0,
- dpr: 1.0,
- };
-
- let width = input.chars().map(|c| if c.width() == Some(2) { 2 } else { 1 }).sum();
- let mut term =
- Term::new(&Default::default(), size, MessageBuffer::new(), Clipboard::new_nop());
- let mut grid: Grid<Cell> = Grid::new(Line(1), Column(width), 0, Cell::default());
-
- let mut i = 0;
- for c in input.chars() {
- grid[Line(0)][Column(i)].c = c;
-
- if c.width() == Some(2) {
- grid[Line(0)][Column(i)].flags.insert(Flags::WIDE_CHAR);
- grid[Line(0)][Column(i + 1)].flags.insert(Flags::WIDE_CHAR_SPACER);
- grid[Line(0)][Column(i + 1)].c = ' ';
- i += 1;
- }
-
- i += 1;
- }
-
- mem::swap(term.grid_mut(), &mut grid);
-
- term
- }
-
- fn url_test(input: &str, expected: &str) {
- let term = url_create_term(input);
- let url = term.url_search(Point::new(0, Column(15)));
- assert_eq!(url.map(|u| u.text), Some(expected.into()));
- }
-
- #[test]
- fn url_skip_invalid() {
- let term = url_create_term("no url here");
- let url = term.url_search(Point::new(0, Column(4)));
- assert_eq!(url, None);
-
- let term = url_create_term(" https://example.org");
- let url = term.url_search(Point::new(0, Column(0)));
- assert_eq!(url, None);
- }
-
- #[test]
- fn url_origin() {
- let term = url_create_term(" test https://example.org ");
- let url = term.url_search(Point::new(0, Column(10)));
- assert_eq!(url.map(|u| u.origin), Some(4));
-
- let term = url_create_term("https://example.org");
- let url = term.url_search(Point::new(0, Column(0)));
- assert_eq!(url.map(|u| u.origin), Some(0));
-
- let term = url_create_term("https://全.org");
- let url = term.url_search(Point::new(0, Column(10)));
- assert_eq!(url.map(|u| u.origin), Some(10));
-
- let term = url_create_term("https://全.org");
- let url = term.url_search(Point::new(0, Column(8)));
- assert_eq!(url.map(|u| u.origin), Some(8));
-
- let term = url_create_term("https://全.org");
- let url = term.url_search(Point::new(0, Column(9)));
- assert_eq!(url.map(|u| u.origin), Some(9));
-
- let term = url_create_term("test@https://example.org");
- let url = term.url_search(Point::new(0, Column(9)));
- assert_eq!(url.map(|u| u.origin), Some(4));
-
- let term = url_create_term("test全https://example.org");
- let url = term.url_search(Point::new(0, Column(9)));
- assert_eq!(url.map(|u| u.origin), Some(3));
- }
-
- #[test]
- fn url_matching_chars() {
- url_test("(https://example.org/test(ing))", "https://example.org/test(ing)");
- url_test("https://example.org/test(ing)", "https://example.org/test(ing)");
- url_test("((https://example.org))", "https://example.org");
- url_test(")https://example.org(", "https://example.org");
- url_test("https://example.org)", "https://example.org");
- url_test("https://example.org(", "https://example.org");
- url_test("(https://one.org/)(https://two.org/)", "https://one.org/");
-
- url_test("https://[2001:db8:a0b:12f0::1]:80", "https://[2001:db8:a0b:12f0::1]:80");
- url_test("([(https://example.org/test(ing))])", "https://example.org/test(ing)");
- url_test("https://example.org/]()", "https://example.org/");
- url_test("[https://example.org]", "https://example.org");
-
- url_test("'https://example.org/test'ing'''", "https://example.org/test'ing'");
- url_test("https://example.org/test'ing'", "https://example.org/test'ing'");
- url_test("'https://example.org'", "https://example.org");
- url_test("'https://example.org", "https://example.org");
- url_test("https://example.org'", "https://example.org");
-
- url_test("(https://example.org/test全)", "https://example.org/test全");
- }
-
- #[test]
- fn url_detect_end() {
- url_test("https://example.org/test\u{00}ing", "https://example.org/test");
- url_test("https://example.org/test\u{1F}ing", "https://example.org/test");
- url_test("https://example.org/test\u{7F}ing", "https://example.org/test");
- url_test("https://example.org/test\u{9F}ing", "https://example.org/test");
- url_test("https://example.org/test\ting", "https://example.org/test");
- url_test("https://example.org/test ing", "https://example.org/test");
- }
-
- #[test]
- fn url_remove_end_chars() {
- url_test("https://example.org/test?ing", "https://example.org/test?ing");
- url_test("https://example.org.,;:)'!/?", "https://example.org");
- url_test("https://example.org'.", "https://example.org");
- url_test("https://example.org/test/?;:", "https://example.org/test/");
- }
-
- #[test]
- fn url_remove_start_chars() {
- url_test("complicated:https://example.org", "https://example.org");
- url_test("test.https://example.org", "https://example.org");
- url_test(",https://example.org", "https://example.org");
- url_test("\u{2502}https://example.org", "https://example.org");
- }
-
- #[test]
- fn url_unicode() {
- url_test("https://xn--example-2b07f.org", "https://xn--example-2b07f.org");
- url_test("https://example.org/\u{2008A}", "https://example.org/\u{2008A}");
- url_test("https://example.org/\u{f17c}", "https://example.org/\u{f17c}");
- url_test("https://üñîçøðé.com/ä", "https://üñîçøðé.com/ä");
- }
+ start = terminal.buffer_to_visible(start);
+ end = terminal.buffer_to_visible(end);
- #[test]
- fn url_schemes() {
- url_test("mailto://example.org", "mailto://example.org");
- url_test("https://example.org", "https://example.org");
- url_test("http://example.org", "http://example.org");
- url_test("news://example.org", "news://example.org");
- url_test("file://example.org", "file://example.org");
- url_test("git://example.org", "git://example.org");
- url_test("ssh://example.org", "ssh://example.org");
- url_test("ftp://example.org", "ftp://example.org");
+ let start = Linear::from_point(terminal.cols(), start);
+ let end = Linear::from_point(terminal.cols(), end);
- assert_eq!(url_create_term("mailto.example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("https:example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("http:example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("news.example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("file:example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("git:example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("ssh:example.org").url_search(Point::default()), None);
- assert_eq!(url_create_term("ftp:example.org").url_search(Point::default()), None);
+ RangeInclusive::new(start, end)
}
}