app/src/main/java/com/equwal/sbm/Tsv.kt (3552 bytes)
1 package com.equwal.sbm 2 3 import java.net.URLEncoder 4 5 /** One line of an sbm bookmark file: URL<tab>description<tab>tags. */ 6 data class Bookmark(val url: String, val desc: String, val tags: List<String>) 7 8 /** 9 * The bookmark file of sbm, as bm reads and writes it. This file has no 10 * Android code, so the tests run on any JVM. 11 */ 12 object Tsv { 13 private val control = Regex("[\t\r\n]") 14 private val scheme = Regex("^[A-Za-z][A-Za-z0-9+.-]*://") 15 private val space = Regex("\\s+") 16 private val web = Regex("""https?://\S+""") 17 18 /** The bookmarks in the text of a file. Blank lines and lines that start with # are not bookmarks. */ 19 fun parse(text: String): List<Bookmark> = 20 text.split('\n').mapNotNull { raw -> 21 val line = raw.removeSuffix("\r") 22 if (line.isBlank() || line.startsWith("#")) null else parseLine(line) 23 } 24 25 private fun parseLine(line: String): Bookmark? { 26 val fields = line.split('\t') 27 if (fields.size == 1) { 28 // A line of the old format: "URL description | tags". bm-migrate 29 // converts it. Until then the first word is the URL. 30 val words = line.trim().split(space, limit = 2) 31 return Bookmark(words[0], words.getOrElse(1) { "" }, emptyList()) 32 } 33 val url = fields[0].trim() 34 if (url.isEmpty()) return null 35 val tags = fields.getOrElse(2) { "" }.split(' ').filter { it.isNotEmpty() } 36 return Bookmark(url, fields[1], tags) 37 } 38 39 /** The line of a bookmark, without the newline. */ 40 fun format(b: Bookmark): String = 41 b.url.filterNot { it.isWhitespace() } + "\t" + clean(b.desc) + "\t" + 42 b.tags.flatMap { it.split(space) }.filter { it.isNotEmpty() }.joinToString(" ") 43 44 /** Text for one field: tabs and line breaks become spaces. */ 45 fun clean(text: String): String = control.replace(text, " ") 46 47 /** 48 * The same page for bm: two URLs match when they differ only in the 49 * scheme, a leading www., trailing slashes or the case of the host. 50 */ 51 fun norm(url: String): String { 52 val u = scheme.replace(url, "") 53 val slash = u.indexOf('/') 54 val host = (if (slash >= 0) u.substring(0, slash) else u).lowercase().removePrefix("www.") 55 val rest = if (slash >= 0) u.substring(slash).trimEnd('/') else "" 56 return host + rest 57 } 58 59 /** The bookmark for the same page as [url], if there is one. */ 60 fun find(bookmarks: List<Bookmark>, url: String): Bookmark? { 61 val n = norm(url) 62 return bookmarks.firstOrNull { norm(it.url) == n } 63 } 64 65 /** The text to add to a file that holds [text], so that [line] is a line of its own. */ 66 fun appendix(text: String, line: String): String = 67 if (text.isEmpty() || text.endsWith("\n")) "$line\n" else "\n$line\n" 68 69 /** The host of a URL, for display. */ 70 fun host(url: String): String = scheme.replace(url, "").substringBefore('/').substringBefore('?').substringBefore('#') 71 72 /** The first web address in shared text. */ 73 fun firstUrl(text: String): String? = web.find(text)?.value 74 75 /** 76 * Where text that is no bookmark goes, as in bm: one word with "://" or 77 * a dot is an address. Other text is a web search. 78 */ 79 fun target(text: String): String { 80 val t = text.trim() 81 if (!t.contains(' ')) { 82 if (t.contains("://")) return t 83 if (t.contains('.')) return "https://$t" 84 } 85 return "https://duckduckgo.com/?q=" + URLEncoder.encode(t, "UTF-8") 86 } 87 }