lib/tsv.js (3107 bytes)
1 // The bookmark file of sbm, as bm reads and writes it: one bookmark per line, 2 // 3 // URL<tab>description<tab>tag tag tag 4 // 5 // Blank lines and lines that start with # are not bookmarks. This file has no 6 // browser code, so the tests run in node. 7 "use strict"; 8 9 const Tsv = (() => { 10 const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//; 11 12 function parseLine(line) { 13 const fields = line.split("\t"); 14 if (fields.length === 1) { 15 // A line of the old format: "URL description | tags". bm-migrate 16 // converts it. Until then the first word is the URL. 17 const words = line.trim().split(/\s+/); 18 return { url: words[0], desc: words.slice(1).join(" "), tags: [] }; 19 } 20 const url = fields[0].trim(); 21 if (url === "") return null; 22 const tags = (fields[2] || "").split(" ").filter((t) => t !== ""); 23 return { url, desc: fields[1], tags }; 24 } 25 26 return { 27 /** The bookmarks in the text of a file. */ 28 parse(text) { 29 const out = []; 30 for (const raw of text.split("\n")) { 31 const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw; 32 if (line.trim() === "" || line.startsWith("#")) continue; 33 const b = parseLine(line); 34 if (b) out.push(b); 35 } 36 return out; 37 }, 38 39 /** Text for one field: tabs and line breaks become spaces. */ 40 clean(text) { 41 return text.replace(/[\t\r\n]/g, " "); 42 }, 43 44 /** The line of a bookmark, without the newline. */ 45 format(b) { 46 const tags = b.tags.flatMap((t) => t.split(/\s+/)).filter((t) => t !== ""); 47 return b.url.replace(/\s/g, "") + "\t" + Tsv.clean(b.desc) + "\t" + tags.join(" "); 48 }, 49 50 /** 51 * The same page for bm: two URLs match when they differ only in the 52 * scheme, a leading www., trailing slashes or the case of the host. 53 */ 54 norm(url) { 55 const u = url.replace(scheme, ""); 56 const slash = u.indexOf("/"); 57 const host = (slash >= 0 ? u.slice(0, slash) : u).toLowerCase().replace(/^www\./, ""); 58 const rest = slash >= 0 ? u.slice(slash).replace(/\/+$/, "") : ""; 59 return host + rest; 60 }, 61 62 /** The bookmark for the same page as url, or undefined. */ 63 find(bookmarks, url) { 64 const n = Tsv.norm(url); 65 return bookmarks.find((b) => Tsv.norm(b.url) === n); 66 }, 67 68 /** The text to add to a file that holds text, so that line is a line of its own. */ 69 appendix(text, line) { 70 return text === "" || text.endsWith("\n") ? line + "\n" : "\n" + line + "\n"; 71 }, 72 73 /** The host of a URL, for display. */ 74 host(url) { 75 return url.replace(scheme, "").split(/[/?#]/)[0]; 76 }, 77 78 /** 79 * Where text that is no bookmark goes, as in bm: one word with "://" or 80 * a dot is an address. Other text is a web search. 81 */ 82 target(text) { 83 const t = text.trim(); 84 if (!t.includes(" ")) { 85 if (t.includes("://")) return t; 86 if (t.includes(".")) return "https://" + t; 87 } 88 return "https://duckduckgo.com/?q=" + encodeURIComponent(t).replace(/%20/g, "+"); 89 }, 90 }; 91 })(); 92 93 if (typeof module !== "undefined") module.exports = Tsv;