all: initial commit
- ID
c0cd2d7- date
2026-09-27 06:41:29+00:00- author
Alex Chan <alex@alexwlchan.net>- message
all: initial commit- changed files
8 files, 1252 additions
Changed files
.gitignore (0 → 8)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
CHANGELOG.md (0 → 133)
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..b6d3630
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,6 @@
+# CHANGELOG
+
+## v1.0.0 - 2026-09-27
+
+Initial release.
+This release includes a lot of URL cleaning logic ported from a previous tool.
Cargo.lock (0 → 10157)
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..b7c3025
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,386 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+
+[[package]]
+name = "assert_cmd"
+version = "2.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6"
+dependencies = [
+ "anstyle",
+ "bstr",
+ "libc",
+ "predicates",
+ "predicates-core",
+ "predicates-tree",
+ "wait-timeout",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "bstr"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
+dependencies = [
+ "memchr",
+ "regex-automata",
+ "serde_core",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486"
+
+[[package]]
+name = "clean-url"
+version = "1.0.0"
+dependencies = [
+ "anyhow",
+ "assert_cmd",
+ "clap",
+ "predicates",
+ "urlencoding",
+ "urlparse",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "difflib"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8"
+
+[[package]]
+name = "float-cmp"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "libc"
+version = "0.2.189"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
+
+[[package]]
+name = "memchr"
+version = "2.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
+
+[[package]]
+name = "normalize-line-endings"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "predicates"
+version = "3.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe"
+dependencies = [
+ "anstyle",
+ "difflib",
+ "float-cmp",
+ "normalize-line-endings",
+ "predicates-core",
+ "regex",
+]
+
+[[package]]
+name = "predicates-core"
+version = "1.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144"
+
+[[package]]
+name = "predicates-tree"
+version = "1.0.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2"
+dependencies = [
+ "predicates-core",
+ "termtree",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.107"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "regex"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
+
+[[package]]
+name = "serde_core"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.229"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "syn"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8593e8e72159ed2257d083c7a454a85cbf854f37a0966d8d483aff8c8a3ebcee"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "termtree"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.26"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
+
+[[package]]
+name = "urlencoding"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a1f0175e03a0973cf4afd476bef05c26e228520400eb1fd473ad417b1c00ffb"
+
+[[package]]
+name = "urlparse"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "110352d4e9076c67839003c7788d8604e24dcded13e0b375af3efaa8cf468517"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "wait-timeout"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
Cargo.toml (0 → 247)
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..869bd51
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "clean-url"
+version = "1.0.0"
+edition = "2024"
+
+[dependencies]
+clap = { version = "4.5.60", features = ["derive"] }
+urlencoding = "1.0.0"
+urlparse = "0.7.3"
+
+[dev-dependencies]
+anyhow = "1.0.104"
+assert_cmd = "2"
+predicates = "3"
LICENSE (0 → 1053)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..e732212
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2026 Alex Chan
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
+THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
+OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
README.md (0 → 1368)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..907484e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,35 @@
+# clean-url
+
+clean-url is a command-line tool for cleaning URLs.
+
+I have text expansion macros that insert the URLs from my frontmost browser window, and this tool cleans up the URL before insertion.
+It removes tracking and referral parameters, unwanted URL suffixes, and generally de-junks URLs.
+
+## Installation
+
+You need Rust installed; I recommend using [Rustup].
+Then clone this repository and compile the code:
+
+```console
+$ git clone "git://alexwlchan.net/projects/clean-url.git"
+$ cd clean-url
+$ cargo install --path .
+```
+
+[Rustup]: https://rustup.rs/
+
+## Usage
+
+clean-url takes a single argument: the URL to be cleaned.
+For example:
+
+```console
+$ clean-url "https://www.etsy.com/uk/listing/689034321/pride-flag-planets-complete-set-cross?ls=s&ga_order=most_relevant&ga_search_type=all&ga_view_type=gallery&ga_search_query=mathysphere+pride+planets&ref=sr_gallery-1-3&dd=1&content_source=750fbda2-0f23-409a-b29d-9714dc4e0ba3%253ALTcadfd6ad1b51d9f2c43bf9fc61aa37ee3ee5cc68&organic_search_click=1&logging_key=750fbda2-0f23-409a-b29d-9714dc4e0ba3%3ALTcadfd6ad1b51d9f2c43bf9fc61aa37ee3ee5cc68"
+https://www.etsy.com/uk/listing/689034321/pride-flag-planets-complete-set-cross⏎
+```
+
+Note: output is printed without a trailing new line, so it integrates cleanly with my text expansion macros.
+
+## License
+
+This project is shared under the [MIT license](./LICENSE).
src/clean_url.rs (0 → 30623)
diff --git a/src/clean_url.rs b/src/clean_url.rs
new file mode 100644
index 0000000..61b6fa4
--- /dev/null
+++ b/src/clean_url.rs
@@ -0,0 +1,737 @@
+use urlencoding::encode as urlencode;
+use urlparse::{Query, Url, parse_qs, urlparse, urlunparse};
+
+fn partial_urlencode(value: &str) -> String {
+ // The urlencode from the urlencoding library goes further than I like,
+ // and also URL encodes ASCII digits. Reverse that stuff.
+ urlencode(value)
+ .replace("%30", "0")
+ .replace("%31", "1")
+ .replace("%32", "2")
+ .replace("%33", "3")
+ .replace("%34", "4")
+ .replace("%35", "5")
+ .replace("%36", "6")
+ .replace("%37", "7")
+ .replace("%38", "8")
+ .replace("%39", "9")
+}
+
+/// Re-encode a query string for Rust
+fn encode_querystring(query: Query) -> Option<String> {
+ let mut query_components: Vec<String> = vec![];
+ for (key, value) in query {
+ for v in value.iter() {
+ query_components.push(format!("{}={}", key, partial_urlencode(v)));
+ }
+ }
+ if query_components.len() > 0 {
+ Some(query_components.join("&"))
+ } else {
+ None
+ }
+}
+
+/// Strip tracking junk and URL suffixes.
+pub fn clean_url(url: &str) -> String {
+ let mut parsed_url = urlparse(url);
+
+ // Always get the desktop version of Twitter URLs
+ if parsed_url.netloc == "mobile.twitter.com" {
+ parsed_url.netloc = String::from("twitter.com");
+ }
+
+ // Always get the non-mobile version of nytimes.com URLs
+ if parsed_url.netloc == "mobile.nytimes.com" {
+ parsed_url.netloc = String::from("nytimes.com");
+ }
+
+ // Remove any tracking junk from Amazon URLs so they're not a
+ // ridiculous length
+ if (parsed_url.netloc == "www.amazon.co.uk") || (parsed_url.netloc == "smile.amazon.co.uk") {
+ parsed_url.query = None;
+ let mut new_path = String::from("");
+ for component in parsed_url.path.split("ref=") {
+ new_path = String::from(component);
+ break;
+ }
+ parsed_url.path = new_path;
+ }
+
+ // Strip tracking junk from Medium, Mashable and Buzzfeed
+ if (parsed_url.netloc == "medium.com")
+ || (parsed_url.netloc == "www.buzzfeed.com")
+ || (parsed_url.netloc == "mashable.com")
+ {
+ parsed_url.fragment = None;
+ }
+
+ // Strip tracking junk from TikTok URLs
+ if parsed_url.netloc == "www.tiktok.com" {
+ parsed_url.query = None;
+ }
+
+ // Remove '#notes' from Tumblr URLs
+ if parsed_url.netloc.ends_with("tumblr.com") {
+ remove_query_params(
+ &mut parsed_url,
+ &["_branch_match_id", "_branch_referrer", "source"],
+ );
+
+ parsed_url.fragment = match parsed_url.fragment {
+ Some(fragment) => {
+ if fragment == "notes" {
+ None
+ } else {
+ Some(fragment)
+ }
+ }
+ None => None,
+ };
+ }
+
+ // Remove &feature=youtu.be from YouTube URLs
+ if parsed_url.netloc.ends_with("youtube.com") {
+ remove_query_params(&mut parsed_url, &["app", "feature"]);
+ }
+
+ // Remove any tracking parameters which:
+ //
+ // * Come from UTM (Urchin Tracking Module)
+ // * Come from Cloudflare tracking
+ // * Come from HubSpot
+ //
+ parsed_url.query = match parsed_url.query {
+ Some(qs) => {
+ let mut query = parse_qs(&qs);
+ let ignore_keys: Vec<_> = query
+ .keys()
+ .filter(|key| {
+ key.starts_with("utm_") || key.starts_with("__cf") || key.starts_with("hsa")
+ })
+ .map(|k| k.clone())
+ .collect();
+ for key in ignore_keys {
+ query.remove(&key);
+ }
+ encode_querystring(query)
+ }
+ None => None,
+ };
+
+ // Tidy up the query and anchor links in modules on docs.python.org
+ if parsed_url.netloc == "docs.python.org" {
+ // If this is a module page, scrap any module- fragment.
+ parsed_url.fragment = match parsed_url.fragment {
+ Some(fragment) => {
+ if fragment.starts_with("module-") {
+ None
+ } else {
+ Some(fragment)
+ }
+ }
+ None => None,
+ };
+
+ // Scrap the highlight
+ remove_query_param(&mut parsed_url, "highlight");
+ }
+
+ // If I'm on a GitHub pull request and looking at the files tab,
+ // link to the top of the pull request.
+ if parsed_url.netloc == "github.com" {
+ if parsed_url.path.ends_with("/files") {
+ parsed_url.path = parsed_url.path.replace("/files", "");
+ }
+ }
+
+ // Remove tracking query parameters from telegraph.co.uk URLs
+ // https://github.com/alexwlchan/safari.rs/issues/48
+ if parsed_url.netloc == "www.telegraph.co.uk" {
+ remove_query_param(&mut parsed_url, "WT.mc_id");
+ }
+
+ // Remove tracking parameters from Etsy URLs.
+ if parsed_url.netloc == "www.etsy.com" {
+ remove_query_params(
+ &mut parsed_url,
+ &[
+ "awc",
+ "click_key",
+ "click_sum",
+ "content_source",
+ "crt",
+ "dd",
+ "frs",
+ "logging_key",
+ "ls",
+ "pro",
+ "source",
+ "sts",
+ ],
+ );
+
+ parsed_url.query = match parsed_url.query {
+ Some(qs) => {
+ let mut query = parse_qs(&qs);
+ let utm_keys: Vec<_> = query
+ .keys()
+ .filter(|key| {
+ key.starts_with("ga_")
+ || key.starts_with("ref")
+ || key.starts_with("organic_search_click")
+ })
+ .map(|k| k.clone())
+ .collect();
+ for key in utm_keys {
+ query.remove(&key);
+ }
+ encode_querystring(query)
+ }
+ None => None,
+ };
+ }
+
+ // Un-mobile-ify blogspot links.
+ if parsed_url.netloc.ends_with("blogspot.com") {
+ remove_query_param(&mut parsed_url, "m");
+ }
+
+ // Un-mobile-ify blogspot links.
+ if parsed_url.netloc == "www.redbubble.com" {
+ remove_query_params(&mut parsed_url, &["asc", "ref"]);
+ }
+
+ // Always remove the _ga Google Analytics tracking parameter.
+ remove_query_param(&mut parsed_url, "_ga");
+
+ // Always remove the Google Click ID tracking parameters.
+ remove_query_param(&mut parsed_url, "gclid");
+
+ // Remove tracking parameters from stacks.wellcomecollection.org URLs,
+ // which are from Medium.
+ if parsed_url.netloc == "stacks.wellcomecollection.org" {
+ remove_query_param(&mut parsed_url, "source");
+ }
+
+ // Remove tracking parameters from Wordery URLs.
+ if parsed_url.netloc == "wordery.com" {
+ remove_query_param(&mut parsed_url, "cTrk");
+ }
+
+ // Remove sharing/referral parameter from Twitter URLs.
+ if parsed_url.netloc == "twitter.com" {
+ remove_query_params(&mut parsed_url, &["cxt", "ref_src", "ref_url", "s"]);
+ }
+
+ // Remove the scheme/host from localhost:5757 links, which is the
+ // local instance of my website. I just want the path/query.
+ if parsed_url.netloc == "localhost:5757" {
+ parsed_url.netloc = "".to_string();
+ parsed_url.scheme = "".to_string();
+ return urlunparse(parsed_url).replace("://", "");
+ }
+
+ // Convert links to questions on Stack Overflow to insert my sharing
+ // link for the Announcer badge.
+ // This code is autogenerated by the `se_referral_autogen.py` script.
+ fix_se_referral(&mut parsed_url, "academia.stackexchange.com", "7658");
+ fix_se_referral(&mut parsed_url, "anime.stackexchange.com", "7674");
+ fix_se_referral(&mut parsed_url, "apple.stackexchange.com", "14295");
+ fix_se_referral(&mut parsed_url, "area51.stackexchange.com", "47881");
+ fix_se_referral(&mut parsed_url, "askubuntu.com", "265738");
+ fix_se_referral(&mut parsed_url, "aviation.stackexchange.com", "1372");
+ fix_se_referral(&mut parsed_url, "bicycles.stackexchange.com", "17362");
+ fix_se_referral(&mut parsed_url, "biology.stackexchange.com", "16115");
+ fix_se_referral(&mut parsed_url, "bricks.stackexchange.com", "445");
+ fix_se_referral(&mut parsed_url, "chemistry.stackexchange.com", "5443");
+ fix_se_referral(&mut parsed_url, "chess.stackexchange.com", "2564");
+ fix_se_referral(&mut parsed_url, "christianity.stackexchange.com", "10196");
+ fix_se_referral(&mut parsed_url, "codegolf.stackexchange.com", "13285");
+ fix_se_referral(&mut parsed_url, "codereview.stackexchange.com", "36525");
+ fix_se_referral(&mut parsed_url, "cogsci.stackexchange.com", "7973");
+ fix_se_referral(
+ &mut parsed_url,
+ "communitybuilding.stackexchange.com",
+ "372",
+ );
+ fix_se_referral(&mut parsed_url, "cooking.stackexchange.com", "25134");
+ fix_se_referral(&mut parsed_url, "crypto.stackexchange.com", "1185");
+ fix_se_referral(&mut parsed_url, "diy.stackexchange.com", "25263");
+ fix_se_referral(&mut parsed_url, "dsp.stackexchange.com", "8360");
+ fix_se_referral(&mut parsed_url, "earthscience.stackexchange.com", "352");
+ fix_se_referral(&mut parsed_url, "elementaryos.stackexchange.com", "3586");
+ fix_se_referral(&mut parsed_url, "english.stackexchange.com", "22597");
+ fix_se_referral(&mut parsed_url, "gaming.stackexchange.com", "73524");
+ fix_se_referral(&mut parsed_url, "gardening.stackexchange.com", "813");
+ fix_se_referral(&mut parsed_url, "gis.stackexchange.com", "26054");
+ fix_se_referral(&mut parsed_url, "graphicdesign.stackexchange.com", "19347");
+ fix_se_referral(&mut parsed_url, "law.stackexchange.com", "2488");
+ fix_se_referral(&mut parsed_url, "lifehacks.stackexchange.com", "11245");
+ fix_se_referral(&mut parsed_url, "linguistics.stackexchange.com", "2722");
+ fix_se_referral(&mut parsed_url, "math.stackexchange.com", "24160");
+ fix_se_referral(&mut parsed_url, "matheducators.stackexchange.com", "661");
+ fix_se_referral(&mut parsed_url, "mathematica.stackexchange.com", "5190");
+ fix_se_referral(&mut parsed_url, "mathoverflow.net", "38734");
+ fix_se_referral(&mut parsed_url, "mechanics.stackexchange.com", "14629");
+ fix_se_referral(&mut parsed_url, "meta.stackexchange.com", "226928");
+ fix_se_referral(&mut parsed_url, "money.stackexchange.com", "13518");
+ fix_se_referral(&mut parsed_url, "movies.stackexchange.com", "9285");
+ fix_se_referral(
+ &mut parsed_url,
+ "networkengineering.stackexchange.com",
+ "16668",
+ );
+ fix_se_referral(&mut parsed_url, "opensource.stackexchange.com", "2909");
+ fix_se_referral(&mut parsed_url, "parenting.stackexchange.com", "14304");
+ fix_se_referral(&mut parsed_url, "patents.stackexchange.com", "3882");
+ fix_se_referral(&mut parsed_url, "philosophy.stackexchange.com", "16838");
+ fix_se_referral(&mut parsed_url, "photo.stackexchange.com", "29700");
+ fix_se_referral(&mut parsed_url, "physics.stackexchange.com", "38614");
+ fix_se_referral(&mut parsed_url, "politics.stackexchange.com", "5590");
+ fix_se_referral(&mut parsed_url, "productivity.stackexchange.com", "1990");
+ fix_se_referral(&mut parsed_url, "puzzling.stackexchange.com", "4692");
+ fix_se_referral(&mut parsed_url, "rpg.stackexchange.com", "22823");
+ fix_se_referral(&mut parsed_url, "salesforce.stackexchange.com", "36215");
+ fix_se_referral(&mut parsed_url, "scifi.stackexchange.com", "3567");
+ fix_se_referral(&mut parsed_url, "security.stackexchange.com", "9814");
+ fix_se_referral(&mut parsed_url, "serverfault.com", "206273");
+ fix_se_referral(&mut parsed_url, "skeptics.stackexchange.com", "5416");
+ fix_se_referral(
+ &mut parsed_url,
+ "softwareengineering.stackexchange.com",
+ "94977",
+ );
+ fix_se_referral(&mut parsed_url, "space.stackexchange.com", "1003");
+ fix_se_referral(&mut parsed_url, "sqa.stackexchange.com", "7301");
+ fix_se_referral(&mut parsed_url, "stackapps.com", "21515");
+ fix_se_referral(&mut parsed_url, "stackoverflow.com", "1558022");
+ fix_se_referral(&mut parsed_url, "stats.stackexchange.com", "32450");
+ fix_se_referral(&mut parsed_url, "superuser.com", "243137");
+ fix_se_referral(&mut parsed_url, "tex.stackexchange.com", "9668");
+ fix_se_referral(&mut parsed_url, "travel.stackexchange.com", "12415");
+ fix_se_referral(&mut parsed_url, "unix.stackexchange.com", "43183");
+ fix_se_referral(&mut parsed_url, "ux.stackexchange.com", "9976");
+ fix_se_referral(&mut parsed_url, "webapps.stackexchange.com", "45296");
+ fix_se_referral(&mut parsed_url, "webmasters.stackexchange.com", "35749");
+ fix_se_referral(&mut parsed_url, "workplace.stackexchange.com", "14106");
+ fix_se_referral(&mut parsed_url, "worldbuilding.stackexchange.com", "2575");
+ fix_se_referral(&mut parsed_url, "writers.stackexchange.com", "11018");
+
+ urlunparse(parsed_url)
+}
+
+/// Remove a query parameter from a URL.
+fn remove_query_param(parsed_url: &mut Url, query_param: &str) {
+ remove_query_params(parsed_url, &[query_param]);
+}
+
+/// Remove multiple query parameters from a URL.
+fn remove_query_params(parsed_url: &mut Url, query_params: &[&str]) {
+ parsed_url.query = match parsed_url.query {
+ Some(ref qs) => {
+ let mut query = parse_qs(qs);
+ for param in query_params {
+ query.remove(*param);
+ }
+ encode_querystring(query)
+ }
+ None => None,
+ };
+}
+
+/// Turn a URL into a Stack Exchange referral link.
+///
+/// - `parsed_url` - the `Url` structure returned by urlparse
+/// - `hostname` - hostname of the SE site in question
+/// - `user_id` - your user ID on the SE site
+///
+fn fix_se_referral(parsed_url: &mut Url, hostname: &str, user_id: &str) {
+ if parsed_url.netloc != hostname {
+ return;
+ }
+
+ // A question URL is of the form
+ //
+ // http://stackoverflow.com/questions/:question_id/:question_title
+ //
+ if !parsed_url.path.starts_with("/questions") {
+ return;
+ }
+
+ // Take a copy of the original path to get around ownership rules
+ let original_path = parsed_url.path.to_owned();
+
+ let new_path = match original_path.split("/").nth(2) {
+ Some(path_component) => {
+ // Check it's a number
+ match path_component.parse::<i32>() {
+ Ok(q_id) => {
+ // Check if there's an answer fragment
+ match parsed_url.fragment.to_owned() {
+ Some(fragment) => match fragment.parse::<i32>() {
+ Ok(ans_id) => Some(format!("/a/{}/{}", ans_id, user_id)),
+ Err(_) => None,
+ },
+ None => Some(format!("/q/{}/{}", q_id, user_id)),
+ }
+ }
+ Err(_) => None,
+ }
+ }
+ None => None,
+ };
+
+ // If we got something interesting, update the URL.
+ match new_path {
+ Some(p) => {
+ parsed_url.path = p;
+ parsed_url.fragment = None;
+ }
+ None => (),
+ };
+}
+
+#[cfg(test)]
+mod clean_url_tests {
+ use crate::clean_url::clean_url;
+
+ macro_rules! clean_url_tests {
+ ($($name:ident: $value:expr,)*) => {
+ $(
+ #[test]
+ fn $name() {
+ let (input, expected) = $value;
+ assert_eq!(expected, clean_url(input));
+ }
+ )*
+ }
+ }
+
+ clean_url_tests! {
+ etsy_bear: (
+ "https://www.etsy.com/uk/listing/721435625/bear-cross-stitch-pattern-pdf-bear?ls=s&ga_order=most_relevant&ga_search_type=all&ga_view_type=gallery&ga_search_query=bear+cross+stitch+pattern&ref=sr_gallery-1-12&sts=1&dd=1&content_source=00ca7d91-b721-4f6f-8b8e-128c10da15f7%253ALT6e4e75eacc959cc45062f0ebc0d7ede920b198fe&organic_search_click=1&logging_key=00ca7d91-b721-4f6f-8b8e-128c10da15f7%3ALT6e4e75eacc959cc45062f0ebc0d7ede920b198fe",
+ "https://www.etsy.com/uk/listing/721435625/bear-cross-stitch-pattern-pdf-bear"
+ ),
+
+ etsy_readme: (
+ "https://www.etsy.com/uk/listing/689034321/pride-flag-planets-complete-set-cross?ls=s&ga_order=most_relevant&ga_search_type=all&ga_view_type=gallery&ga_search_query=mathysphere+pride+planets&ref=sr_gallery-1-3&dd=1&content_source=750fbda2-0f23-409a-b29d-9714dc4e0ba3%253ALTcadfd6ad1b51d9f2c43bf9fc61aa37ee3ee5cc68&organic_search_click=1&logging_key=750fbda2-0f23-409a-b29d-9714dc4e0ba3%3ALTcadfd6ad1b51d9f2c43bf9fc61aa37ee3ee5cc68",
+ "https://www.etsy.com/uk/listing/689034321/pride-flag-planets-complete-set-cross"
+ ),
+
+ self_localhost_homepage: (
+ "http://localhost:5757/",
+ "/"
+ ),
+
+ self_localhost_page: (
+ "http://localhost:5757/book-reviews/",
+ "/book-reviews/"
+ ),
+
+ twitter_mobile: (
+ "https://mobile.twitter.com/Breaking911/status/822589852596191235",
+ "https://twitter.com/Breaking911/status/822589852596191235"
+ ),
+
+ regular_twitter: (
+ "https://twitter.com/WholesomeMeme/status/846421658835537921",
+ "https://twitter.com/WholesomeMeme/status/846421658835537921"
+ ),
+
+ twitter_with_tracking_param: (
+ "https://twitter.com/b0rk/status/1627763836479479811?cxt=HHwWhoCxubil_ZYtAAAA",
+ "https://twitter.com/b0rk/status/1627763836479479811"
+ ),
+
+ amazon_product: (
+ "https://www.amazon.co.uk/dp/B01DFKBL68/ref=gw_aucc_comb_AB_clean_2?pf_rd_r=N1MF2PWADRHS55427ETG&pf_rd_p=c11a2c11-c670-46ff-87a1-c1eef4fad652",
+ "https://www.amazon.co.uk/dp/B01DFKBL68/"
+ ),
+
+ amazon_smile_product: (
+ "https://smile.amazon.co.uk/dp/B01DFKBL68/ref=gw_aucc_comb_AB_clean_2?pf_rd_r=N1MF2PWADRHS55427ETG&pf_rd_p=c11a2c11-c670-46ff-87a1-c1eef4fad652",
+ "https://smile.amazon.co.uk/dp/B01DFKBL68/"
+ ),
+
+ medium_with_tracking: (
+ "https://medium.com/@anildash/forget-why-its-time-to-get-to-work-c49ac5f0da20#.sjyskxdsz",
+ "https://medium.com/@anildash/forget-why-its-time-to-get-to-work-c49ac5f0da20"
+ ),
+
+ tumblr_with_notes: (
+ "http://azurelunatic.tumblr.com/post/155525051123/things-about-hufflepuffs-539#notes",
+ "http://azurelunatic.tumblr.com/post/155525051123/things-about-hufflepuffs-539"
+ ),
+
+ tumblr_with_tracking: (
+ "https://www.tumblr.com/tightleish/709085976781324289?_branch_referrer=H4sIAAAAAAAAAwXBAQqAIAwAwB8509Vmv6kQpyhFzvD53Ynq03eAQ42OdtbXXHcDzUm0xtwFyAbLa6CNePEOHQeYOGL6Cpbp6Qe%2F0rcpQAAAAA%3D%3D&source=share&_branch_match_id=1137383385327673172",
+ "https://www.tumblr.com/tightleish/709085976781324289"
+ ),
+
+ buzzfeed_with_tracking: (
+ "https://www.buzzfeed.com/katienotopoulos/the-end-of-apple-man#.biqmkzz8kn",
+ "https://www.buzzfeed.com/katienotopoulos/the-end-of-apple-man"
+ ),
+
+ mashable_with_tracking: (
+ "http://mashable.com/2016/03/21/apple-liam-recycling-robot/#b9y4lv3m4qqX",
+ "http://mashable.com/2016/03/21/apple-liam-recycling-robot/"
+ ),
+
+ youtube_plain_video: (
+ "https://www.youtube.com/watch?v=zB4I68XVPzQ",
+ "https://www.youtube.com/watch?v=zB4I68XVPzQ",
+ ),
+
+ youtube_with_timestamp: (
+ "https://www.youtube.com/watch?v=D68cUzqcTrg&feature=youtu.be#t=1m",
+ "https://www.youtube.com/watch?v=D68cUzqcTrg#t=1m",
+ ),
+
+ youtube_with_trailing_feature: (
+ "https://www.youtube.com/watch?v=PbJqNa0_Oz0&feature=youtu.be",
+ "https://www.youtube.com/watch?v=PbJqNa0_Oz0",
+ ),
+
+ youtube_with_leading_feature: (
+ "https://www.youtube.com/watch?feature=youtu.be&v=oPo4n9tBPsk",
+ "https://www.youtube.com/watch?v=oPo4n9tBPsk",
+ ),
+
+ youtube_with_only_feature: (
+ "https://www.youtube.com/watch?feature=youtu.be",
+ "https://www.youtube.com/watch",
+ ),
+
+ youtube_channel: (
+ "https://www.youtube.com/user/TheQIElves",
+ "https://www.youtube.com/user/TheQIElves"
+ ),
+
+ youtube_with_app: (
+ "https://www.youtube.com/watch?app=desktop&v=51HbzsFhh04",
+ "https://www.youtube.com/watch?v=51HbzsFhh04"
+ ),
+
+ single_utm_tracker: (
+ "https://example.com?utm_medium=social",
+ "https://example.com",
+ ),
+
+ multiple_utm_tracker: (
+ "https://example.com?utm_medium=social&utm_source=twitter",
+ "https://example.com",
+ ),
+
+ multiple_utm_tracker_with_others: (
+ "https://example.com?utm_medium=social&foo=bar&utm_source=twitter",
+ "https://example.com?foo=bar",
+ ),
+
+ url_with_spaces: (
+ "https://example.com?foo=bar%20baz",
+ "https://example.com?foo=bar%20baz"
+ ),
+
+ url_with_numerals: (
+ "https://example.com?foo=bar0baz",
+ "https://example.com?foo=bar0baz"
+ ),
+
+ python_docs_bare: (
+ "https://docs.python.org/3.5/library/subprocess.html",
+ "https://docs.python.org/3.5/library/subprocess.html"
+ ),
+
+ python_docs_with_anchor: (
+ "https://docs.python.org/3.5/library/subprocess.html#subprocess.run",
+ "https://docs.python.org/3.5/library/subprocess.html#subprocess.run"
+ ),
+
+ python_docs_with_highlight: (
+ "https://docs.python.org/3.5/library/subprocess.html?highlight=subprocess",
+ "https://docs.python.org/3.5/library/subprocess.html"
+ ),
+
+ python_docs_with_highlight_anchor: (
+ "https://docs.python.org/3.5/library/subprocess.html?highlight=subprocess#subprocess.run",
+ "https://docs.python.org/3.5/library/subprocess.html#subprocess.run"
+ ),
+
+ python_docs_with_highlight_module_anchor: (
+ "https://docs.python.org/3.5/library/subprocess.html?highlight=subprocess#module-subprocess",
+ "https://docs.python.org/3.5/library/subprocess.html"
+ ),
+
+ stack_overflow_non_question: (
+ "http://stackoverflow.com/questions/tagged/html+regex",
+ "http://stackoverflow.com/questions/tagged/html+regex"
+ ),
+
+ stack_overflow_question: (
+ "http://stackoverflow.com/questions/1732348/regex-match-open-tags-except-xhtml-self-contained-tags",
+ "http://stackoverflow.com/q/1732348/1558022"
+ ),
+
+ stack_overflow_answer: (
+ "http://stackoverflow.com/questions/82831/how-do-i-check-whether-a-file-exists-using-python#82852",
+ "http://stackoverflow.com/a/82852/1558022"
+ ),
+
+ sff_se_non_question: (
+ "https://scifi.stackexchange.com/review",
+ "https://scifi.stackexchange.com/review"
+ ),
+
+ sff_se_question: (
+ "https://scifi.stackexchange.com/questions/58980/how-did-lupin-forget-there-was-a-full-moon",
+ "https://scifi.stackexchange.com/q/58980/3567"
+ ),
+
+ sff_se_answer: (
+ "https://scifi.stackexchange.com/questions/39201/which-owls-did-fred-and-george-weasley-achieve/39218#39218",
+ "https://scifi.stackexchange.com/a/39218/3567"
+ ),
+
+ telegraph_bare: (
+ "http://www.telegraph.co.uk/news/2017/06/09/ruth-davidson-planning-scottish-tory-breakaway-challenges-theresa/",
+ "http://www.telegraph.co.uk/news/2017/06/09/ruth-davidson-planning-scottish-tory-breakaway-challenges-theresa/"
+ ),
+
+ strip_telegraph_tracking: (
+ "http://www.telegraph.co.uk/news/2017/06/09/ruth-davidson-planning-scottish-tory-breakaway-challenges-theresa/?WT.mc_id=tmg_share_tw",
+ "http://www.telegraph.co.uk/news/2017/06/09/ruth-davidson-planning-scottish-tory-breakaway-challenges-theresa/"
+ ),
+
+ se_comments: (
+ "https://stackoverflow.com/questions/406230/regular-expression-to-match-a-line-that-doesnt-contain-a-word#comment9209422_406230",
+ "https://stackoverflow.com/questions/406230/regular-expression-to-match-a-line-that-doesnt-contain-a-word#comment9209422_406230"
+ ),
+
+ ga_tracking_parameter: (
+ "https://example.org?_ga=1234",
+ "https://example.org"
+ ),
+
+ ga_tracking_parameter_and_others: (
+ "https://example.org?_ga=1234&foo=bar",
+ "https://example.org?foo=bar"
+ ),
+
+ nytimes_mobile_url: (
+ "https://mobile.nytimes.com/2017/02/24/style/modern-love-when-your-greatest-romance-is-friendship.html",
+ "https://nytimes.com/2017/02/24/style/modern-love-when-your-greatest-romance-is-friendship.html"
+ ),
+
+ nytimes_non_mobile_url: (
+ "https://nytimes.com/2017/02/24/style/modern-love-when-your-greatest-romance-is-friendship.html",
+ "https://nytimes.com/2017/02/24/style/modern-love-when-your-greatest-romance-is-friendship.html"
+ ),
+
+ github_pr: (
+ "https://github.com/wellcometrust/platform/pull/1892",
+ "https://github.com/wellcometrust/platform/pull/1892"
+ ),
+
+ github_pr_with_files: (
+ "https://github.com/wellcometrust/platform/pull/1892/files",
+ "https://github.com/wellcometrust/platform/pull/1892"
+ ),
+
+ etsy_link: (
+ "https://www.etsy.com/uk/listing/473409127/space-sampler-cross-stitch-pattern-pdf?ga_order=most_relevant&ref=sr_gallery-1-3&ga_view_type=gallery&organic_search_click=1&ga_search_type=all&ga_search_query=space%20sampler",
+ "https://www.etsy.com/uk/listing/473409127/space-sampler-cross-stitch-pattern-pdf"
+ ),
+
+ etsy_section_link: (
+ "https://www.etsy.com/uk/shop/Vitraaze?source=aw§ion_id=17807678&awc=6091_1577546787_d9f1b918042bbd02fe406d61dae715e7",
+ "https://www.etsy.com/uk/shop/Vitraaze?section_id=17807678"
+ ),
+
+ etsy_pro_link: (
+ "https://www.etsy.com/uk/listing/704478659/gothic-cathedral-window-ornate-cross?pro=1",
+ "https://www.etsy.com/uk/listing/704478659/gothic-cathedral-window-ornate-cross"
+ ),
+
+ etsy_link_with_tracking: (
+ "https://www.etsy.com/uk/listing/731582894/personalised-song-lyric-or-poetry?frs=1",
+ "https://www.etsy.com/uk/listing/731582894/personalised-song-lyric-or-poetry",
+ ),
+
+ etsy_link_with_tmore_tracking: (
+ "https://www.etsy.com/uk/listing/1487193769/progress-goose-pdf-gay-pride-month-cross?click_sum=15f8aef5&click_key=d478a4a346e8318cc55887564ddfef8f5687fb60%3A1487193769&crt=1",
+ "https://www.etsy.com/uk/listing/1487193769/progress-goose-pdf-gay-pride-month-cross",
+ ),
+
+ blogspot: (
+ "https://example.blogspot.com/2020/01/01/my_first_post.html",
+ "https://example.blogspot.com/2020/01/01/my_first_post.html"
+ ),
+
+ blogspot_mobile_link: (
+ "https://example.blogspot.com/2020/01/01/my_first_post.html?m=1",
+ "https://example.blogspot.com/2020/01/01/my_first_post.html"
+ ),
+
+ redbubble_plain: (
+ "https://www.redbubble.com/people/m1sfire/works/42386806-pixel-pride-genderfluid",
+ "https://www.redbubble.com/people/m1sfire/works/42386806-pixel-pride-genderfluid"
+ ),
+
+ redbubble_referrer: (
+ "https://www.redbubble.com/people/m1sfire/works/42386806-pixel-pride-genderfluid?ref=recent-owner",
+ "https://www.redbubble.com/people/m1sfire/works/42386806-pixel-pride-genderfluid"
+ ),
+
+ redbubble_asc: (
+ "https://www.redbubble.com/people/m1sfire/works/42386806-pixel-pride-genderfluid?asc=u",
+ "https://www.redbubble.com/people/m1sfire/works/42386806-pixel-pride-genderfluid"
+ ),
+
+ stacks_medium_referrer: (
+ "https://stacks.wellcomecollection.org/a-sprinkling-of-azure-6cef6e150fb2?source=collection_home---4------0———————————",
+ "https://stacks.wellcomecollection.org/a-sprinkling-of-azure-6cef6e150fb2"
+ ),
+
+ wordery_with_tracking_param: (
+ "https://wordery.com/digging-for-words-angela-burke-kunkel-9781984892638?cTrk=MTgwMTkyNTkxfDVmYjgzYzU5YzQ4Mjc6MToxOjVmYjgzYzE4ZTdhNzQ4LjczMzU3NDQxOjVhMzBiMGYx",
+ "https://wordery.com/digging-for-words-angela-burke-kunkel-9781984892638"
+ ),
+
+ twitter_with_share_link: (
+ "https://twitter.com/ScotRail/status/1355928580895731713?s=20",
+ "https://twitter.com/ScotRail/status/1355928580895731713"
+ ),
+
+ twitter_with_ref: (
+ "https://twitter.com/thingskatedid/status/1386077306381242371?ref_url=https%3A%2F%2Fwww.benweintraub.com%2F&ref_src=twsrc%5Etfw%7Ctwcamp%5Etweetembed%7Ctwterm%5E1386077306381242371%7Ctwgr%5E13dd6945ce97e163ca6ca5d28b54fc562b161b2f%7Ctwcon%5Es1_",
+ "https://twitter.com/thingskatedid/status/1386077306381242371"
+ ),
+
+ twitter_with_cxt: (
+ "https://twitter.com/b0rk/status/1627763836479479811?cxt=HHwWhoCxubil_ZYtAAAA",
+ "https://twitter.com/b0rk/status/1627763836479479811"
+ ),
+
+ url_with_cloudflare_query_param: (
+ "https://example.org/page?__cf_chl_jschl_tk__=1",
+ "https://example.org/page"
+ ),
+
+ url_with_hubspot_query_param: (
+ "https://imgix.com/pricing?hsa_grp=131223639264&hsa_acc=8534109361&gclid=CjwKCAiA-8SdBhBGEiwAWdgtcHVTpIo3PdadyKLfI4Wv3CdUUhcRISS0QNoitcBtZeJxiCJ_ZKowvhoCg68QAvD_BwE&hsa_src=g&hsa_cam=13763501966&hsa_mt=b&hsa_net=adwords&hsa_ad=598338680789&hsa_tgt=kwd-340899810185&hsa_ver=3&hsa_kw=imgix%20pricing",
+ "https://imgix.com/pricing"
+ ),
+
+ url_with_google_click_id_query_param: (
+ "https://imgix.com/pricing?gclid=CjwKCAiA-8SdBhBGEiwAWdgtcHVTpIo3PdadyKLfI4Wv3CdUUhcRISS0QNoitcBtZeJxiCJ_ZKowvhoCg68QAvD_BwE",
+ "https://imgix.com/pricing"
+ ),
+
+ tiktok_with_tracking_junk: (
+ "https://www.tiktok.com/@example/video/1234567890?sec_user_id=ABCDEFGHIJ&u_code=dgfffl6mjl6be3&share_app_id=1233×tamp=1631110682",
+ "https://www.tiktok.com/@example/video/1234567890"
+ ),
+ }
+}
src/main.rs (0 → 1633)
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..351cd23
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,54 @@
+#![deny(warnings)]
+
+use clap::Parser;
+
+mod clean_url;
+use crate::clean_url::clean_url;
+
+#[derive(Parser)]
+#[command(version, about = "Clean tracking and unwanted parameters from URLs.", long_about = None)]
+struct Cli {
+ /// URL to clean
+ #[arg()]
+ url: String,
+}
+
+fn main() {
+ let cli = Cli::parse();
+
+ print!("{}", clean_url(&cli.url));
+}
+
+#[cfg(test)]
+mod test_cli {
+ use anyhow::Result;
+ use assert_cmd::Command;
+ use predicates::prelude::*;
+
+ #[test]
+ fn it_prints_the_version() -> Result<()> {
+ // Match strings like `clean-url 1.2.3`
+ let is_version_string = predicate::str::is_match(r"^clean-url [0-9]+\.[0-9]+\.[0-9]+\n$")?;
+
+ Command::cargo_bin("clean-url")?
+ .arg("--version")
+ .assert()
+ .success()
+ .stdout(is_version_string)
+ .stderr("");
+
+ Ok(())
+ }
+
+ #[test]
+ fn it_runs_the_readme_example() -> Result<()> {
+ Command::cargo_bin("clean-url")?
+ .arg("https://www.etsy.com/uk/listing/689034321/pride-flag-planets-complete-set-cross?ls=s&ga_order=most_relevant&ga_search_type=all&ga_view_type=gallery&ga_search_query=mathysphere+pride+planets&ref=sr_gallery-1-3&dd=1&content_source=750fbda2-0f23-409a-b29d-9714dc4e0ba3%253ALTcadfd6ad1b51d9f2c43bf9fc61aa37ee3ee5cc68&organic_search_click=1&logging_key=750fbda2-0f23-409a-b29d-9714dc4e0ba3%3ALTcadfd6ad1b51d9f2c43bf9fc61aa37ee3ee5cc68")
+ .assert()
+ .success()
+ .stdout("https://www.etsy.com/uk/listing/689034321/pride-flag-planets-complete-set-cross")
+ .stderr("");
+
+ Ok(())
+ }
+}