Skip to content

o0101/Bookmate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

65 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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:

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])

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])

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

  1. Find your Bookmark folder location
  2. Mount it
  3. Read a top-level bookmark folder
  4. 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 πŸ˜… β€” I'm sorry, other documentation should go here. πŸ‘·β€β™€οΈ

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 πŸ’Ή

  • emit change events for URL bookmark additions, deletions and name changes
  • existsSync
  • readFileSync
  • writeFileSync
  • readdirSync
  • mkdirSync
  • 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;
    }
  }
}