Repository: mrcoles/bookmarklet
Branch: master
Commit: 0600069a0520
Files: 14
Total size: 17.8 KB
Directory structure:
gitextract_epxp27wt/
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── bin/
│ └── cli.js
├── bookmarklet.js
├── package.json
└── test/
├── bookmarklets/
│ ├── test1.bookmarklet.js
│ └── test2.bookmarklet.js
├── expected/
│ ├── test1.bookmarklet.js
│ └── test2.bookmarklet.js
└── run.sh
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
node_modules
test/actual/
================================================
FILE: .prettierrc
================================================
singleQuote: true
arrowParens: avoid
trailingComma: none
================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
- "6"
- "node"
================================================
FILE: CHANGELOG.md
================================================
# 3.0.0
- Changed minifier to Terser
- Remove Babel transform to avoid issues such as regenerator-runtime not found (do any needed transforms before this script)
================================================
FILE: LICENSE.txt
================================================
The MIT License
Copyright (c) 2021 Peter Coles (http://mrcoles.com/)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# Bookmarklet: sane development, familiar format
[](https://travis-ci.org/mrcoles/bookmarklet)
[](https://github.com/prettier/prettier)
Bookmarklet is a nodejs module for compiling bookmarklets in server-side code and directly from the shell. You can run it on any JavaScript file—it will minify it using uglify-js, wrap it in a self executing function, and return an escaped bookmarklet.
More so, it supports a metadata block—modeled after the [greasemonkey userscript metadata block](http://wiki.greasespot.net/Metadata_Block)—to specify metadata, external stylesheets and script includes, which can look like this:
// ==Bookmarklet==
// @name LoveGames
// @author Old Gregg
// @style !loadOnce https://mrcoles.com/media/css/silly.css
// @script https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// ==/Bookmarklet==
Most notably, you can specify any external scripts that you’d like your bookmarklet to include via the `@script` rule, which can be repeated as many times as you’d like.
NOTE: currently with script includes you have to handle `noConflict` scenarios yourself, e.g., you might want to start off a script with `var $ = jQuery.noConflict(true)`.
In addition, any css files included with `@style` will be injected.
By default, every time the bookmark is hit, it will add the script and style tags again. You customize each one per line by adding a `!loadOnce` declaration between the `@style` or `@script` param and the path for the asset. See the example above.
As of v1.0.0, this now uses Babel with the present "env" to make the code backwards compatible before minifying it.
This project is open to suggestions & pull requests.
Also, if you’re just looking for a quick way to throw together a bookmarklet, try my [browser-based bookmarklet creator](http://mrcoles.com/bookmarklet/).
### Installation
The dependency can be found on [NPM as “bookmarklet”](https://www.npmjs.org/package/bookmarklet). You can install it with:
```bash
npm install bookmarklet
```
### Usage
You can easily see usage by running `bookmarklet -h`:
```bash
> bookmarklet -h
Bookmarklet v0.0.1 usage: bookmarklet [-d | --demo] source [destination]
-d | --demo - output a demo HTML page for sharing the bookmarklet
source - path to file to read from or `-` for stdin
destination - path to file to write to
```
The default output is the raw bookmarlet code. _NEW_ add the `--demo` flag to output a test HTML page that includes the bookmarklet on it.
### Testing
A very basic test script can be run via `bash test/run.sh`
================================================
FILE: bin/cli.js
================================================
#!/usr/bin/env node
const path = require('path');
const fs = require('fs');
const bookmarklet = require('../bookmarklet');
//
// Input parsing
//
let args = process.argv.slice(2);
if (['-V', '--version'].some(flag => args.indexOf(flag) !== -1)) {
console.log(bookmarklet.version.join('.'));
process.exit(0);
}
function help() {
console.error(`
Bookmarklet v${bookmarklet.version.join('.')}
Usage: bookmarklet [options] source [destination]
source path to file to read from or - for stdin
destination path to file to write to
Options:
-d, --demo generate a demo HTML page
More info: https://github.com/mrcoles/bookmarklet
`);
}
function die(msg) {
msg && console.error(`[ERROR] bookmarklet: ${msg}`);
process.exit(1);
}
function warn(msg) {
console.error(`[WARN] bookmarklet: ${msg}`);
}
// flags
const _isArgDemo = arg => arg === '-d' || arg === '--demo';
const makeDemo = args.some(_isArgDemo);
args = args.filter(arg => !_isArgDemo(arg));
// help
if (args.length == 0 || args.some(arg => arg === '-h' || arg === '--help')) {
help();
process.exit(0);
}
// file paths
if (args.length > 2) {
die('invalid arguments, run with --help to see usage.\n\n');
}
let source = args[0];
let destination = args[1];
const readStdin = source === '-';
if (source && source[0] !== '/' && !readStdin) {
source = path.join(process.cwd(), source);
}
if (destination) {
if (destination[0] !== '/') {
destination = path.join(process.cwd(), destination);
}
let isDirectory = destination.endsWith('/');
if (!isDirectory) {
try {
let destStat = fs.statSync(destination);
isDirectory = destStat.isDirectory();
} catch (e) {}
}
if (isDirectory) {
if (readStdin) {
die('must name output file if reading from stdin\n\n');
}
let filename = path.basename(source);
destination = path.join(destination, filename);
}
}
//
// Main
//
function dataCallback(e, data) {
if (e) {
die(e.message);
}
data = bookmarklet.parseFile(data);
if (data.errors) {
die(data.errors.join('\n'));
}
return bookmarklet
.convert(data.code, data.options)
.then(code => {
if (makeDemo) {
code = bookmarklet.makeDemo(code, data.options);
}
if (destination) {
fs.writeFileSync(destination, code);
} else {
console.log(code);
}
})
.catch(e => {
die(e);
});
}
if (source !== '-') {
fs.readFile(source, 'utf8', dataCallback);
} else {
process.stdin.resume();
process.stdin.setEncoding('utf8');
var buffer = '';
process.stdin.on('data', data => (buffer += data));
process.stdin.on('end', () => dataCallback(false, buffer));
}
================================================
FILE: bookmarklet.js
================================================
const version = [3, 0, 0];
// const babel = require('@babel/core');
// const babelPresetEnv = require('@babel/preset-env');
const md5 = require('md5');
const Terser = require('terser');
// metadata
const str = 1;
const list = 2;
const bool = 3;
const metadata = {
types: {
string: str,
list: list,
boolean: bool
},
keys: {
name: str,
version: str,
description: str,
repository: str,
author: str,
email: str,
url: str,
license: str,
script: list,
style: list
}
};
function quoteEscape(x) {
return x.replace('"', '\\"').replace("'", "\\'");
}
function extractOptions(path) {
// Returns {
// path: the updated path string (minus any options)
// opts: plain object of options
// }
//
// You can prefix a path with options in the form of:
//
// ```
// @style !loadOnce !foo=false https://example.com/foo.css
// ```
//
// If there is no `=`, then the value of the option defaults to `true`.
// Values get converted via JSON.parse if possible, o/w they're a string.
//
let opts = {};
while (true) {
let m = path.match(/^(\![^\s]+)\s+/);
if (m) {
path = path.substring(m.index + m[0].length);
let opt = m[1].substring(1).split('=');
opts[opt[0]] = opt[1] === undefined ? true : _fuzzyParse(opt[1]);
} else {
break;
}
}
return { path, opts };
}
const _fuzzyParse = val => {
try {
return JSON.parse(val);
} catch (e) {
return val;
}
};
function loadScript(code, path, loadOnce) {
loadOnce = !!loadOnce;
let id = `bookmarklet__script_${md5(path).substring(0, 7)}`;
return `
function callback(){
${code}
}
if (!${loadOnce} || !document.getElementById("${id}")) {
var s = document.createElement("script");
if (s.addEventListener) {
s.addEventListener("load", callback, false)
} else if (s.readyState) {
s.onreadystatechange = callback
}
if (${loadOnce}) {
s.id = "${id}";
}
s.src = "${quoteEscape(path)}";
document.body.appendChild(s);
} else {
callback();
}
`;
}
function loadStyle(code, path, loadOnce) {
loadOnce = !!loadOnce;
let id = `bookmarklet__style_${md5(path).substring(0, 7)}`;
return `${code}
if (!${loadOnce} || !document.getElementById("${id}")) {
var link = document.createElement("link");
if (${loadOnce}) {
link.id = "${id}";
}
link.rel="stylesheet";
link.href = "${quoteEscape(path)}";
document.body.appendChild(link);
}
`;
}
async function minify(code) {
// const result = babel.transform(code, {
// presets: [
// [
// babelPresetEnv,
// {
// targets: 'ie 8', // '> 0.25%, not dead',
// corejs: { version: '3.9', proposals: true },
// useBuiltIns: 'usage'
// }
// ]
// ]
// });
const result = await Terser.minify(code);
return result.code;
}
async function convert(code, options) {
code = await minify(code);
let stylesCode = '';
if (options.script) {
options.script = options.script.reverse();
options.script.forEach(s => {
let { path, opts } = extractOptions(s);
code = loadScript(code, path, opts.loadOnce);
});
code = await minify(code);
}
if (options.style) {
options.style.forEach(s => {
let { path, opts } = extractOptions(s);
stylesCode = loadStyle(stylesCode, path, opts.loadOnce);
});
const minifiedStyles = await minify(stylesCode);
code = minifiedStyles + code;
}
code = `(function(){${code}})()`;
return `javascript:${encodeURIComponent(code)}`;
}
function parseFile(data) {
let inMetadataBlock = false;
let openMetadata = '==Bookmarklet==';
let closeMetadata = '==/Bookmarklet==';
let rComment = /^(\s*\/\/\s*)/;
let mdKeys = metadata.keys;
let mdTypes = metadata.types;
let options = {};
let code = [];
let errors = [];
// parse file and gather options from metadata block if available
data.match(/[^\r\n]+/g).forEach(function (line, i, lines) {
// comment
if (rComment.test(line)) {
let comment = line.replace(rComment, '').trim(),
canonicalComment = comment.toLowerCase().replace(/\s+/g, '');
if (!inMetadataBlock) {
if (canonicalComment == openMetadata.toLowerCase()) {
inMetadataBlock = true;
}
} else {
if (canonicalComment == closeMetadata.toLowerCase()) {
inMetadataBlock = false;
} else {
let m = comment.match(/^@([^\s]+)\s+(.*)$/);
if (m) {
let k = m[1];
let v = m[2];
if (k) {
if (mdKeys[k] == mdTypes.list) {
options[k] = options[k] || [];
options[k].push(v);
} else if (mdKeys[k] == mdTypes.boolean) {
options[k] = v.toLowerCase() == 'true';
} else {
options[k] = v;
}
} else {
warn(`ignoring invalid metadata option: '${k}'`);
}
}
}
}
// code
} else {
code.push(line);
}
if (inMetadataBlock && i + 1 == lines.length) {
errors.push(`missing metdata block closing '${closeMetadata}'`);
}
});
return {
code: code.join('\n'),
options,
errors: errors.length ? errors : null
};
}
function makeDemo(bookmarkletCode, options) {
options = options || {};
const name = options.name || 'Bookmarklet';
const createdWith = 'https://github.com/mrcoles/bookmarklet';
const html = `<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html,body,div { margin: 0; padding: 0; font: normal 16px/24px Helvetica Neue, Helvetica, sans-serif; color: #333; }
#main { max-width: 630px; margin: 3em auto; }
.bookmarklet { display: inline-block; padding: .5em 1em; color: #fff; background: #33e; border-radius: 4px; text-decoration: none; }
a { color: #33e; }
</style>
</head>
<body>
<div id="main">
<h1>${name}</h1>
<p>
Drag this button to your bookmarks bar to save it as a bookmarklet:
</p>
<p>
<a class="bookmarklet" href="${bookmarkletCode}">${name}</a>
</p>
${
options.repo
? `<p>See source at <a href="${options.repo}">${options.repo}</a></p>`
: ''
}
<p>This page was created with the <a href="${createdWith}">bookmarklet</a> npm library.</p>
</div>
</body>
</html>
`;
return html;
}
// Exports
Object.assign(exports, {
version,
convert,
parseFile,
makeDemo,
metadata
});
================================================
FILE: package.json
================================================
{
"name": "bookmarklet",
"version": "3.0.0",
"description": "A JavaScript bookmarklet compiler and demo page creator with greasemonkey userscript-like metadata options",
"main": "bookmarklet.js",
"bin": {
"bookmarklet": "bin/cli.js"
},
"engines": {
"node": ">= 6.0.0"
},
"scripts": {
"test": "bash test/run.sh"
},
"repository": "github:mrcoles/bookmarklet",
"keywords": [
"bookmarklet"
],
"author": {
"name": "Peter Coles"
},
"contributors": [
{
"name": "Ryan Pavlik"
}
],
"license": "MIT",
"readmeFilename": "README.md",
"dependencies": {
"md5": "^2.2.1",
"terser": "^5.6.1"
}
}
================================================
FILE: test/bookmarklets/test1.bookmarklet.js
================================================
// ==Bookmarklet==
// @name Test
// @author Peter
// @style data:text/css,imaginary%7Bfont-family%3A%20%22ok%22%7D
// @script https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// ==/Bookmarklet==
var $ = jQuery.noConflict(true),
pCount = $('p').size(),
divCount = $('div').size(),
testElement = $('<imaginary>').appendTo('body'),
testPassed = testElement.css('font-family') == 'ok';
testElement.remove();
alert('p tags: ' + pCount +
'\ndiv tags: ' + divCount +
'\nDid the test element get styled? ' +
(testPassed ? 'Of course!' : 'Nope.'));
================================================
FILE: test/bookmarklets/test2.bookmarklet.js
================================================
// ==Bookmarklet==
// @name Test
// @author Peter
// @style !loadOnce data:text/css,imaginary%7Bfont-family%3A%20%22ok%22%7D
// @script !loadOnce=false https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// ==/Bookmarklet==
const $ = jQuery.noConflict(true);
const pCount = $('p').size();
const divCount = $('div').size();
const testElement = $('<imaginary>').appendTo('body');
const testPassed = testElement.css('font-family') === 'ok';
testElement.remove();
let run = () => {
alert(
'p tags: ' +
pCount +
'\ndiv tags: ' +
divCount +
'\nDid the test element get styled? ' +
(testPassed ? 'Of course!' : 'Nope.')
);
};
run();
================================================
FILE: test/expected/test1.bookmarklet.js
================================================
javascript:(function()%7Bvar%20link%3Ddocument.createElement(%22link%22)%3Blink.rel%3D%22stylesheet%22%2Clink.href%3D%22data%3Atext%2Fcss%2Cimaginary%257Bfont-family%253A%2520%2522ok%2522%257D%22%2Cdocument.body.appendChild(link)%3Bfunction%20callback()%7Bvar%20e%3DjQuery.noConflict(!0)%2Ca%3De(%22p%22).size()%2Ct%3De(%22div%22).size()%2Cs%3De(%22%3Cimaginary%3E%22).appendTo(%22body%22)%2Cn%3D%22ok%22%3D%3Ds.css(%22font-family%22)%3Bs.remove()%2Calert(%22p%20tags%3A%20%22%2Ba%2B%22%5Cndiv%20tags%3A%20%22%2Bt%2B%22%5CnDid%20the%20test%20element%20get%20styled%3F%20%22%2B(n%3F%22Of%20course!%22%3A%22Nope.%22))%7Dvar%20s%3Ddocument.createElement(%22script%22)%3Bs.addEventListener%3Fs.addEventListener(%22load%22%2Ccallback%2C!1)%3As.readyState%26%26(s.onreadystatechange%3Dcallback)%2Cs.src%3D%22https%3A%2F%2Fajax.googleapis.com%2Fajax%2Flibs%2Fjquery%2F1.9.1%2Fjquery.min.js%22%2Cdocument.body.appendChild(s)%3B%7D)()
================================================
FILE: test/expected/test2.bookmarklet.js
================================================
javascript:(function()%7Bif(!document.getElementById(%22bookmarklet__style_d2037dd%22))%7Bvar%20link%3Ddocument.createElement(%22link%22)%3Blink.id%3D%22bookmarklet__style_d2037dd%22%2Clink.rel%3D%22stylesheet%22%2Clink.href%3D%22data%3Atext%2Fcss%2Cimaginary%257Bfont-family%253A%2520%2522ok%2522%257D%22%2Cdocument.body.appendChild(link)%7Dfunction%20callback()%7Bconst%20e%3DjQuery.noConflict(!0)%2Ca%3De(%22p%22).size()%2Ct%3De(%22div%22).size()%2Cs%3De(%22%3Cimaginary%3E%22).appendTo(%22body%22)%2Cn%3D%22ok%22%3D%3D%3Ds.css(%22font-family%22)%3Bs.remove()%3Balert(%22p%20tags%3A%20%22%2Ba%2B%22%5Cndiv%20tags%3A%20%22%2Bt%2B%22%5CnDid%20the%20test%20element%20get%20styled%3F%20%22%2B(n%3F%22Of%20course!%22%3A%22Nope.%22))%7Dvar%20s%3Ddocument.createElement(%22script%22)%3Bs.addEventListener%3Fs.addEventListener(%22load%22%2Ccallback%2C!1)%3As.readyState%26%26(s.onreadystatechange%3Dcallback)%2Cs.src%3D%22https%3A%2F%2Fajax.googleapis.com%2Fajax%2Flibs%2Fjquery%2F1.9.1%2Fjquery.min.js%22%2Cdocument.body.appendChild(s)%3B%7D)()
================================================
FILE: test/run.sh
================================================
#!/usr/bin/env bash
mkdir -p test/actual/
EXIT_STATUS=0
for f in `ls test/bookmarklets/*.bookmarklet.js`; do
filename=$(basename "$f")
actual="test/actual/$filename"
expected="test/expected/$filename"
./bin/cli.js "$f" "$actual"
if [ $? = 0 ]; then
diff "$expected" "$actual" -q > /dev/null
if [ $? = 0 ]; then
printf "\033[0;32m✓ \033[0mPASSED ${filename}\n";
else
printf "\033[0;31m✗ \033[0mFAILED ${filename}\n"
EXIT_STATUS=1
fi
else
printf "\033[0;31m✗ \033[0mEXCEPTION PARSING ${filename}\n"
EXIT_STATUS=1
fi
done
exit $EXIT_STATUS
gitextract_epxp27wt/
├── .gitignore
├── .prettierrc
├── .travis.yml
├── CHANGELOG.md
├── LICENSE.txt
├── README.md
├── bin/
│ └── cli.js
├── bookmarklet.js
├── package.json
└── test/
├── bookmarklets/
│ ├── test1.bookmarklet.js
│ └── test2.bookmarklet.js
├── expected/
│ ├── test1.bookmarklet.js
│ └── test2.bookmarklet.js
└── run.sh
SYMBOL INDEX (12 symbols across 2 files)
FILE: bin/cli.js
function help (line 18) | function help() {
function die (line 33) | function die(msg) {
function warn (line 38) | function warn(msg) {
function dataCallback (line 98) | function dataCallback(e, data) {
FILE: bookmarklet.js
function quoteEscape (line 32) | function quoteEscape(x) {
function extractOptions (line 36) | function extractOptions(path) {
function loadScript (line 73) | function loadScript(code, path, loadOnce) {
function loadStyle (line 99) | function loadStyle(code, path, loadOnce) {
function minify (line 115) | async function minify(code) {
function convert (line 132) | async function convert(code, options) {
function parseFile (line 158) | function parseFile(data) {
function makeDemo (line 221) | function makeDemo(bookmarkletCode, options) {
Condensed preview — 14 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (20K chars).
[
{
"path": ".gitignore",
"chars": 26,
"preview": "node_modules\ntest/actual/\n"
},
{
"path": ".prettierrc",
"chars": 57,
"preview": "singleQuote: true\narrowParens: avoid\ntrailingComma: none\n"
},
{
"path": ".travis.yml",
"chars": 46,
"preview": "language: node_js\nnode_js:\n - \"6\"\n - \"node\"\n"
},
{
"path": "CHANGELOG.md",
"chars": 163,
"preview": "# 3.0.0\n\n- Changed minifier to Terser\n- Remove Babel transform to avoid issues such as regenerator-runtime not found (do"
},
{
"path": "LICENSE.txt",
"chars": 1093,
"preview": "The MIT License\n\nCopyright (c) 2021 Peter Coles (http://mrcoles.com/)\n\nPermission is hereby granted, free of charge, to "
},
{
"path": "README.md",
"chars": 2752,
"preview": "# Bookmarklet: sane development, familiar format\n\n[;\nconst fs = require('fs');\nconst bookmarklet = require('../bookmarklet"
},
{
"path": "bookmarklet.js",
"chars": 6851,
"preview": "const version = [3, 0, 0];\n\n// const babel = require('@babel/core');\n// const babelPresetEnv = require('@babel/preset-en"
},
{
"path": "package.json",
"chars": 664,
"preview": "{\n \"name\": \"bookmarklet\",\n \"version\": \"3.0.0\",\n \"description\": \"A JavaScript bookmarklet compiler and demo page creat"
},
{
"path": "test/bookmarklets/test1.bookmarklet.js",
"chars": 596,
"preview": "// ==Bookmarklet==\n// @name Test\n// @author Peter\n// @style data:text/css,imaginary%7Bfont-family%3A%20%22ok%22%7D\n// @s"
},
{
"path": "test/bookmarklets/test2.bookmarklet.js",
"chars": 681,
"preview": "// ==Bookmarklet==\n// @name Test\n// @author Peter\n// @style !loadOnce data:text/css,imaginary%7Bfont-family%3A%20%22ok%2"
},
{
"path": "test/expected/test1.bookmarklet.js",
"chars": 925,
"preview": "javascript:(function()%7Bvar%20link%3Ddocument.createElement(%22link%22)%3Blink.rel%3D%22stylesheet%22%2Clink.href%3D%22"
},
{
"path": "test/expected/test2.bookmarklet.js",
"chars": 1040,
"preview": "javascript:(function()%7Bif(!document.getElementById(%22bookmarklet__style_d2037dd%22))%7Bvar%20link%3Ddocument.createEl"
},
{
"path": "test/run.sh",
"chars": 653,
"preview": "#!/usr/bin/env bash\n\nmkdir -p test/actual/\n\nEXIT_STATUS=0\n\nfor f in `ls test/bookmarklets/*.bookmarklet.js`; do\n file"
}
]
About this extraction
This page contains the full source code of the mrcoles/bookmarklet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 14 files (17.8 KB), approximately 5.6k tokens, and a symbol index with 12 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.