Recently Written · git

sbm-android

sbm bookmarks for Android: fuzzy search, share to add, sync with bm and the browser add-on

git clone https://github.com/equwal/sbm-android

Log | Files | Refs


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 }