URL tools
URLs by the rules browsers use: parts and query as a map, relative links resolved, a normal form for comparing, tracking parameters stripped, query values set.
Version 1.0.0 · 5 functions · 1 crate
Functions
What use url_tools.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use url_parse, url_join from url_tools (Quillo 0.6).
url_parse(url: str) → anyor an errorA URL's parts: { scheme, username, host, port, path, query, fragment, origin, href }, query a map of its parameters (the last value of a repeated one), port null when it is the scheme's default.
url_join(base: str, relative: str) → stror an errorA link resolved against the page it is on: url_join("https://a.com/blog/post", "../img/x.png") is https://a.com/img/x.png. An absolute relative comes back as it is.
url_strip_tracking(url: str) → stror an errorThe URL without tracking parameters: every utm_*, and fbclid, gclid, msclkid, mc_eid, _ga, igshid and others ad networks add. The rest is left in its order.
url_normalize(url: str) → stror an errorOne form for URLs that mean the same page, for comparing, deduplicating and cache keys: scheme and host in lower case (as parsing does), default port dropped, . and .. resolved, tracking parameters dropped, the rest sorted by name, the fragment dropped, and a lone / path kept.
url_with_query(url: str, params: map) → stror an errorThe URL with query parameters set from params ({ page: 2, sort: "new" }): existing ones replaced in place, new ones added, a null value removing one.
Get it
$ quillo install url_tools # url_tools.qwer into this folder$ quillo install url_tools --example # and its example, url_tools.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/url_tools.qwer$ curl -O https://quillolang.org/plugins/files/url_tools.qweThen, in your program, all of it or only what you call:
use url_tools.qweruse url_parse, url_join from url_tools // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
url = "2"Example
// A link-sharing site: links cleaned of tracking before they are saved, deduplicated by their// normal form, and a page's relative links made absolute.//// quillo run url_tools.qwe then open http://localhost:8080/share?url=https://Shop.Example.com:443/a/../shoes?utm_source=x%26color=red%26fbclid=abc%23reviewsuse "url_tools.qwer"route GET /share ? { url: str } { let clean = url_strip_tracking(url)? { saved: clean, key: url_normalize(url)?, host: url_parse(clean)?.host }}route GET /same = { same: url_normalize("https://example.com/list?b=2&a=1#top")? == url_normalize("HTTPS://EXAMPLE.com:443/list?a=1&b=2&utm_medium=mail")?, joined: url_join("https://example.com/blog/2026/post.html", "../../img/cover.jpg")?, next_page: url_with_query("https://example.com/search?q=mugs&page=1&ref=nav", { page: 2, ref: null, sort: "price" })?, parts: url_parse("https://ada@example.com:8443/a/b?x=1&x=2&y=%C3%A9#frag")?, bad: url_parse("not a url").error,}$ quillo run url_tools.qwe$ curl 'localhost:8080/share?url=https://Shop.Example.com:443/a/../shoes?utm_source=x%26color=red%26fbclid=abc%23reviews'$ curl 'localhost:8080/same'Source
//! URLs taken apart and put together by the WHATWG rules browsers use: parts and query as a map, relative links resolved, a normal form for comparing and caching, tracking parameters (utm_*, fbclid, gclid...) stripped, and query values set.//! version: 1.0.0//!//! [dependencies]//! url = "2"use serde_json::{json, Map, Value};use std::collections::BTreeMap;const TRACKING: [&str; 24] = [ "fbclid", "gclid", "gclsrc", "dclid", "gbraid", "wbraid", "msclkid", "mc_cid", "mc_eid", "_ga", "_gl", "igshid", "yclid", "twclid", "ttclid", "li_fat_id", "mkt_tok", "oly_anon_id", "oly_enc_id", "_hsenc", "_hsmi", "vero_id", "rb_clickid", "s_cid",];fn parse(u: &str) -> Result<url::Url, String> { if u.len() > 32 * 1024 { return Err("a URL is at most 32 KB here".into()); } url::Url::parse(u.trim()).map_err(|e| format!("`{}` is not a URL: {}", u, e))}/// A URL's parts: `{ scheme, username, host, port, path, query, fragment, origin, href }`, `query` a map of its parameters (the last value of a repeated one), `port` null when it is the scheme's default.pub fn url_parse(url: &str) -> Result<Value, String> { let u = parse(url)?; let q: Map<String, Value> = u.query_pairs().map(|(k, v)| (k.into_owned(), json!(v))).collect(); Ok(json!({ "scheme": u.scheme(), "username": u.username(), "host": u.host_str(), "port": u.port(), "path": u.path(), "query": q, "fragment": u.fragment(), "origin": u.origin().ascii_serialization(), "href": u.as_str(), }))}/// A link resolved against the page it is on: `url_join("https://a.com/blog/post", "../img/x.png")` is `https://a.com/img/x.png`. An absolute `relative` comes back as it is.pub fn url_join(base: &str, relative: &str) -> Result<String, String> { parse(base)?.join(relative.trim()).map(|u| u.to_string()).map_err(|e| format!("cannot join `{}`: {}", relative, e))}fn is_tracking(k: &str) -> bool { let k = k.to_ascii_lowercase(); k.starts_with("utm_") || TRACKING.contains(&k.as_str())}/// The URL without tracking parameters: every `utm_*`, and fbclid, gclid, msclkid, mc_eid, _ga, igshid and others ad networks add. The rest is left in its order.pub fn url_strip_tracking(url: &str) -> Result<String, String> { let mut u = parse(url)?; let kept: Vec<(String, String)> = u.query_pairs().filter(|(k, _)| !is_tracking(k)).map(|(k, v)| (k.into_owned(), v.into_owned())).collect(); if kept.is_empty() { u.set_query(None); } else { u.query_pairs_mut().clear().extend_pairs(kept); } Ok(u.to_string())}/// One form for URLs that mean the same page, for comparing, deduplicating and cache keys: scheme and host in lower case (as parsing does),/// default port dropped, `.` and `..` resolved, tracking parameters dropped, the rest sorted by name, the fragment dropped, and a lone `/` path kept.pub fn url_normalize(url: &str) -> Result<String, String> { let mut u = parse(url)?; u.set_fragment(None); let mut kept: Vec<(String, String)> = u.query_pairs().filter(|(k, _)| !is_tracking(k)).map(|(k, v)| (k.into_owned(), v.into_owned())).collect(); kept.sort(); if kept.is_empty() { u.set_query(None); } else { u.query_pairs_mut().clear().extend_pairs(kept); } if let Some(h) = u.host_str().map(|h| h.trim_end_matches('.').to_string()) { let _ = u.set_host(Some(&h)); } Ok(u.to_string())}/// The URL with query parameters set from `params` (`{ page: 2, sort: "new" }`): existing ones replaced in place, new ones added, a null value removing one.pub fn url_with_query(url: &str, params: std::collections::HashMap<String, serde_json::Value>) -> Result<String, String> { let mut u = parse(url)?; let set: BTreeMap<String, Option<String>> = params .into_iter() .map(|(k, v)| (k, match v { Value::Null => None, Value::String(s) => Some(s), other => Some(other.to_string()), })) .collect(); let mut out: Vec<(String, String)> = Vec::new(); let mut done = std::collections::HashSet::new(); for (k, v) in u.query_pairs().map(|(k, v)| (k.into_owned(), v.into_owned())).collect::<Vec<_>>() { match set.get(&k) { Some(Some(nv)) if done.insert(k.clone()) => out.push((k, nv.clone())), Some(_) => {} None => out.push((k, v)), } } for (k, v) in &set { if let Some(v) = v { if !done.contains(k) { out.push((k.clone(), v.clone())); } } } if out.is_empty() { u.set_query(None); } else { u.query_pairs_mut().clear().extend_pairs(out); } Ok(u.to_string())}Related
slug.qwer · 1.0.0Slugs for addresses from titles in any script, unique ones for a blog, and text transliterated to ASCII.7 views · 8 downloadsDomains domains.qwer · 1.0.0Domain names read by the Public Suffix List: registrable domain, subdomain and suffix, same-site checks for redirects and cookies, and international names to punycode and back.7 views · 7 downloadsHTML rewriting html_rewrite.qwer · 1.0.0HTML rewritten as it streams, by CSS selector: links made absolute, attributes set or removed, elements removed or unwrapped, text replaced.8 views · 6 downloadsLink finder link_extract.qwer · 1.0.0URLs and emails found in plain text, with trailing punctuation handled, and the text turned into safe HTML with clickable, nofollow links.7 views · 6 downloadsoEmbed oembed.qwer · 1.0.0Rich embeds from pasted links: the oEmbed endpoint found in a page or from known providers (YouTube, Vimeo, Spotify, X and more), the answer cut down to one sandboxed iframe from an allowed host, and players made without a request.10 views · 6 downloadsrobots.txt robots.qwer · 1.0.0robots.txt read as Google does: whether an agent may fetch a path, its crawl delay and sitemaps; and your own site's robots.txt written.8 views · 7 downloadsThread 0 posts
On URL tools: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.