📗 Bookmate
An append-only key-value store built on Chrome bookmarks, plus an asychronous stream of Bookmark changes. For NodeJS
./src/demo.js:
import Bookmate from './index.js';
console.log(Bookmate);
const path = Bookmate.tryToFindBookmarksLocation();
console.log({path});
Bookmate.mount(path);
const entries = Bookmate.readdirSync(
'bookmark_bar',
{withFileTypes:true}
);
console.log(entries);
Features
Bookmate:
- efficiently observes changes to bookmarks and emits these as an asychronous iterator readable stream
- automatically locates the right Chrome Profile directory in a platform-agnostic way by observing bookmark changes
- is possesed of an fs-like, and simple, NodeJS API: readFileSync, writeFileSync, promisesWatch etc
Get
$ npm i --save bookmate@latest
Demo
import Bookmate from './index.js';
console.log(Bookmate);
const path = Bookmate.tryToFindBookmarksLocation();
console.log({path});
Bookmate.mount(path);
{
const entries = Bookmate.readdirSync('bookmark_bar', {withFileTypes:true});
console.log(entries);
}
let entry;
try {
entry = Bookmate.readFileSync([
'bookmark_bar',
'https://www.dia.mil/'
], {encoding: 'json'});
entry.name += " Hello ";
} catch(e) {
entry = {
name: "DIA",
type: "url"
}
}
console.log({entry});
Bookmate.writeFileSync(['bookmark_bar', 'https://www.dia.mil/'], entry);
{
const entries = Bookmate.readdirSync('bookmark_bar', {withFileTypes:true});
console.log(entries);
}
Above creates (if not exists) a bookmark to DIA, and appends "Hello" to its bookmark name (the title) otherwise
API
readFileSync(path[, options])
-
path
<SerializedPathArray>
|<PathArray>
|<URL>
path to Bookmark URL -
options
<Object>
- Returns:
<string>
|<Buffer>
|<BookmarkNode>
Returns the contents of the Bookmark at the path.
If the encoding option is 'json'
then this function returns a <BookmarkNode>
. Otherwise, if the encoding option is specified then this function returns a string, otherwise it returns a buffer.
It cannot be called on a folder. To get the contents of a folder use readdirSync()
readdirSync(path[, options])
-
path
<SerializedPathArray>
|<PathArray>
-
options
<Object>
-
withFileTypes
<boolean>
Default:false
-
- Returns:
<string[]>
|<BookmarkNode[]>
Reads the contents of the folder.
If options.withFileTypes
is set to true, the result will contain <BookmarkNode>
objects.
Basic usage
See the demo above
- Find your Bookmark folder location
- Mount it
- Read a top-level bookmark folder
- Do anything!
A note about path syntax.
You can supply a path in three ways. Here's the code that enumerates that (and I'll explain it below):
function guardAndNormalizePath(path) {
if ( isSerializedPath(path) ) {
return JSON.parse(path);
} else if ( isArrayPath(path) ) {
return path;
} else if ( typeof path === "string" ) {
if ( isURL(path) ) {
return [path];
} else if ( ! /https?:/.test(path) ) {
return path.split('/').filter(seg => seg.length);
} else {
throw new SystemError('EINVAL',
`Sorry path shorthand ('/' separator syntax)
can not be used with Bookmark URLs.
Please use a path array instead.
`
);
}
} else {
throw new SystemError('EINVAL',
`Sorry path ${
path
} was not in a valid format. Please see the documentation at ${
LIBRARY_REPO
}`
);
}
}
You can supply a path as a JSON.stringified string, like:
const musicFolderPath = [
'bookmark_bar',
'Music Research'
];
const stringifiedPathArray = JSON.stringify(musicFolderPath);
To refer to the folder "Music Research" in your bookmarks bar. Or
const stringifiedPathArray = JSON.stringify([
'bookmark_bar',
'Music Research',
'https://sheetmusic.com'
]);
To refer to the bookmark with URL https://sheetmusic.com
in the same folder.
You can also supply it as simple an array.
Bookmate.readdirSync(musicFolder);
Bookmate.readFileSync(musicFolder.push('https://spotify.com'), {encoding: 'json'});
I'm sure you get it now.
Equivalent to the above are is:
Bookmate.readdirSync('bookmark_bar/Music reserach');
But the following throws an EINVAL
error:
Bookmate.readFileSync('bookmark_bar/Music research/https://spotify.com');
Because URLs can be used as part of this "path shorthand".
🚧
… Well this is a little embarrassing
The outstanding fs-like functions to document currently are:
- existsSync : does a path exist
- writeFileSync : create a bookmark
- mkdirSync : create a bookmark folder
- promisesWatch (*aka bookmarkChanges) : watch for changes to bookmarks (added, deleted, altered)
And other additional functions to document currently are:
- mount : attach Bookmate to the bookmarks directory (fs-like API now works)
- tryToFindBookmarksLocation : try to find the bookmarks directory
- unmount : un-attach Bookmate
- getProfileRootDir : try to get the root profile directory for Chrome
- saveWithChecksum : Chrome bookmarks require a checksum, this ensures that works
- and bookmarkChanges (same as promisesWatch, actually--just an alias!
😜 😉 xx😜 )
And, finally, the types that currently need documenting are:
- BookmarkNode : an object containing bookmark data
- SerializedPathArray : a JSON-ified array containing bookmark path segments
- PathArray : the JSON.parsed version of the above
But, not to worry--they (the fs-ones anyway) are pretty much like the NodeJS fs versions so you can head over there or read the code to know more—until somebody gets around to finishing these docs.
💹
Implementation Progress & Roadmap - [x] emit change events for URL bookmark additions, deletions and name changes
- [x] existsSync
- [x] readFileSync
- [x] writeFileSync
- [x] readdirSync
- [x] mkdirSync
- [x] promisesWatch (*aka bookmarkChanges)
- [ ] emit events for Folder additions, deletions and name changes
Disclaimer
No connection or endorsement expressed or implied with Google, Alphabet, Chrome, Sync or the Chromium authors.
❤️
Contributions Welcome! It's all kind of new so many you can help also set up a contributing guidelines, documentation and so on
⚖️
License AGPL-3.0 © Cris
More examples
Actual production example:
import {bookmarkChanges} from 'bookmate';
// ...
async function startObservingBookmarkChanges() {
for await ( const change of bookmarkChanges() ) {
switch(change.type) {
case 'new': archiveAndIndexURL(change.url); break;
case 'delete': deleteFromIndexAndSearch(change.url); break;
default: break;
}
}
}