Link finder
URLs and emails found in plain text, with trailing punctuation handled, and the text turned into safe HTML with clickable, nofollow links.
Version 1.0.0 · 4 functions · 1 crate
Functions
What use link_extract.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use links_find, links_html from link_extract (Quillo 0.6).
links_find(text: str, bare: bool) → listor an errorThe links in a text, in order: [{ kind, text, href, start, end }], kind "url" or "email", href ready for an <a> (https:// added to a bare domain, mailto: to an email), start and end counted in characters as slice counts. bare true also finds links without a scheme (example.com/page), which can catch words like "file.txt".
links_html(text: str, nofollow: bool, bare: bool) → stror an errorThe text as HTML, escaped, with its URLs and emails as <a> links and line breaks as <br>: safe for what visitors write. nofollow adds rel="nofollow ugc noopener" and target="_blank" to the web links, as comment sections should.
links_emails(text: str) → listor an errorThe distinct email addresses in a text, in lower case, in the order first found: for pulling contacts out of a pasted list or a signature.
links_hosts(text: str, bare: bool) → listor an errorThe distinct hosts the text links to (example.com, www. kept), for checking comments against a block list.
Get it
$ quillo install link_extract # link_extract.qwer into this folder$ quillo install link_extract --example # and its example, link_extract.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/link_extract.qwer$ curl -O https://quillolang.org/plugins/files/link_extract.qweThen, in your program, all of it or only what you call:
use link_extract.qweruse links_find, links_html from link_extract // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
linkify = "0.10"Example
// Comments shown with their links made clickable (and nofollow), and a check of where they point.//// quillo run link_extract.qwe then open http://localhost:8080/commentuse "link_extract.qwer"let COMMENT = "Great post! The docs (https://quillolang.org/docs/routes) helped.\nMail me at Ada@Example.com, or see www.example.org/talk. <b>not bold</b>"route GET /comment = raw(`<p>${links_html(COMMENT, true, true)?}</p>`)route GET /links = { found: links_find(COMMENT, true)? | map((l) => `${l.kind}: ${l.href} at ${l.start}`), hosts: links_hosts(COMMENT, true)?, emails: links_emails("Ada <ada@example.com>; bob@example.org, ADA@example.com")?,}$ quillo run link_extract.qwe$ curl 'localhost:8080/comment'$ curl 'localhost:8080/links'Source
//! URLs and email addresses found in plain text (comments, chat messages, notes), with trailing punctuation and brackets handled as people write them, and the text turned into safe HTML with the links made clickable.//! version: 1.0.0//!//! [dependencies]//! linkify = "0.10"use linkify::{LinkFinder, LinkKind};use serde_json::{json, Value};const MAX_TEXT: usize = 2 << 20;fn finder(bare: bool) -> LinkFinder { let mut f = LinkFinder::new(); f.kinds(&[LinkKind::Url, LinkKind::Email]); f.url_must_have_scheme(!bare); f}fn size(text: &str) -> Result<(), String> { if text.len() > MAX_TEXT { return Err("the text is at most 2 MB".into()); } Ok(())}fn href(s: &str, kind: &LinkKind) -> String { match kind { LinkKind::Email => format!("mailto:{}", s), _ if s.contains("://") => s.to_string(), _ => format!("https://{}", s), }}fn escape(s: &str) -> String { s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """).replace('\'', "'")}/// The links in a text, in order: `[{ kind, text, href, start, end }]`, `kind` "url" or "email", `href` ready for an `<a>` (`https://` added to a bare domain, `mailto:` to an email),/// `start` and `end` counted in characters as `slice` counts. `bare` true also finds links without a scheme (`example.com/page`), which can catch words like "file.txt".pub fn links_find(text: &str, bare: bool) -> Result<Vec<Value>, String> { size(text)?; let mut out = Vec::new(); for l in finder(bare).links(text) { let kind = if matches!(l.kind(), LinkKind::Email) { "email" } else { "url" }; let start = text[..l.start()].chars().count(); out.push(json!({ "kind": kind, "text": l.as_str(), "href": href(l.as_str(), l.kind()), "start": start, "end": start + l.as_str().chars().count() })); if out.len() >= 10_000 { break; } } Ok(out)}/// The text as HTML, escaped, with its URLs and emails as `<a>` links and line breaks as `<br>`: safe for what visitors write./// `nofollow` adds `rel="nofollow ugc noopener"` and `target="_blank"` to the web links, as comment sections should.pub fn links_html(text: &str, nofollow: bool, bare: bool) -> Result<String, String> { size(text)?; let mut out = String::with_capacity(text.len() + 64); for span in finder(bare).spans(text) { match span.kind() { Some(k) => { let h = href(span.as_str(), k); let rel = if nofollow && !matches!(k, LinkKind::Email) { " rel=\"nofollow ugc noopener\" target=\"_blank\"" } else { "" }; out.push_str(&format!("<a href=\"{}\"{}>{}</a>", escape(&h), rel, escape(span.as_str()))); } None => out.push_str(&escape(span.as_str()).replace("\r\n", "\n").replace('\n', "<br>\n")), } } Ok(out)}/// The distinct email addresses in a text, in lower case, in the order first found: for pulling contacts out of a pasted list or a signature.pub fn links_emails(text: &str) -> Result<Vec<String>, String> { size(text)?; let mut f = LinkFinder::new(); f.kinds(&[LinkKind::Email]); let mut out: Vec<String> = Vec::new(); for l in f.links(text) { let e = l.as_str().to_lowercase(); if !out.contains(&e) { out.push(e); } } Ok(out)}/// The distinct hosts the text links to (`example.com`, `www.` kept), for checking comments against a block list.pub fn links_hosts(text: &str, bare: bool) -> Result<Vec<String>, String> { size(text)?; let mut out: Vec<String> = Vec::new(); for l in finder(bare).links(text).filter(|l| matches!(l.kind(), LinkKind::Url)) { let s = l.as_str(); let rest = s.split_once("://").map(|x| x.1).unwrap_or(s); let host = rest.split(['/', '?', '#']).next().unwrap_or("").rsplit('@').next().unwrap_or("").split(':').next().unwrap_or("").to_lowercase(); if !host.is_empty() && !out.contains(&host) { out.push(host); } } Ok(out)}Related
html_text.qwer · 1.0.0HTML as readable plain text for the text part of an email: headings, lists and tables laid out, links listed at the end, and one-line previews.7 views · 7 downloadsTemplates templates.qwer · 1.0.0Jinja templates your users can edit for emails, invoices and pages: sandboxed, escaped for HTML, with filters such as money and truncate, and checked before they are saved.7 views · 8 downloadsCSS inliner css_inline.qwer · 1.0.0CSS inlined into HTML for email: style blocks and extra CSS written into each element's style attribute, with nothing ever fetched.7 views · 6 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 downloadsMJML email mjml.qwer · 1.0.0MJML email templates rendered to responsive HTML email for Gmail, Outlook and Apple Mail, with placeholders filled in and escaped; no Node needed.8 views · 9 downloadsSanitize HTML sanitize.qwer · 1.0.0HTML written by users made safe to show: scripts, event handlers and javascript: links removed, the formatting kept.7 views · 7 downloadsThread 0 posts
On Link finder: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.