[
  {
    "path": ".gitignore",
    "content": "node_modules\ntest/actual/\n"
  },
  {
    "path": ".prettierrc",
    "content": "singleQuote: true\narrowParens: avoid\ntrailingComma: none\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"6\"\n  - \"node\"\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# 3.0.0\n\n- Changed minifier to Terser\n- Remove Babel transform to avoid issues such as regenerator-runtime not found (do any needed transforms before this script)\n"
  },
  {
    "path": "LICENSE.txt",
    "content": "The MIT License\n\nCopyright (c) 2021 Peter Coles (http://mrcoles.com/)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Bookmarklet: sane development, familiar format\n\n[![Build Status](https://travis-ci.org/mrcoles/bookmarklet.svg?branch=master)](https://travis-ci.org/mrcoles/bookmarklet)\n[![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier)\n\nBookmarklet 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.\n\nMore 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:\n\n    // ==Bookmarklet==\n    // @name LoveGames\n    // @author Old Gregg\n    // @style !loadOnce https://mrcoles.com/media/css/silly.css\n    // @script https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\n    // ==/Bookmarklet==\n\nMost 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.\n\nNOTE: 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)`.\n\nIn addition, any css files included with `@style` will be injected.\n\nBy 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.\n\nAs of v1.0.0, this now uses Babel with the present \"env\" to make the code backwards compatible before minifying it.\n\nThis project is open to suggestions & pull requests.\n\nAlso, if you’re just looking for a quick way to throw together a bookmarklet, try my [browser-based bookmarklet creator](http://mrcoles.com/bookmarklet/).\n\n### Installation\n\nThe dependency can be found on [NPM as “bookmarklet”](https://www.npmjs.org/package/bookmarklet). You can install it with:\n\n```bash\nnpm install bookmarklet\n```\n\n### Usage\n\nYou can easily see usage by running `bookmarklet -h`:\n\n```bash\n> bookmarklet -h\nBookmarklet v0.0.1 usage: bookmarklet [-d | --demo] source [destination]\n\n-d | --demo - output a demo HTML page for sharing the bookmarklet\nsource      - path to file to read from or `-` for stdin\ndestination - path to file to write to\n```\n\nThe default output is the raw bookmarlet code. _NEW_ add the `--demo` flag to output a test HTML page that includes the bookmarklet on it.\n\n### Testing\n\nA very basic test script can be run via `bash test/run.sh`\n"
  },
  {
    "path": "bin/cli.js",
    "content": "#!/usr/bin/env node\n\nconst path = require('path');\nconst fs = require('fs');\nconst bookmarklet = require('../bookmarklet');\n\n//\n// Input parsing\n//\n\nlet args = process.argv.slice(2);\n\nif (['-V', '--version'].some(flag => args.indexOf(flag) !== -1)) {\n  console.log(bookmarklet.version.join('.'));\n  process.exit(0);\n}\n\nfunction help() {\n  console.error(`\nBookmarklet v${bookmarklet.version.join('.')}\n\nUsage: bookmarklet [options] source [destination]\n  source       path to file to read from or - for stdin\n  destination  path to file to write to\n\nOptions:\n  -d, --demo   generate a demo HTML page\n\nMore info: https://github.com/mrcoles/bookmarklet\n  `);\n}\n\nfunction die(msg) {\n  msg && console.error(`[ERROR] bookmarklet: ${msg}`);\n  process.exit(1);\n}\n\nfunction warn(msg) {\n  console.error(`[WARN] bookmarklet: ${msg}`);\n}\n\n// flags\n\nconst _isArgDemo = arg => arg === '-d' || arg === '--demo';\n\nconst makeDemo = args.some(_isArgDemo);\n\nargs = args.filter(arg => !_isArgDemo(arg));\n\n// help\n\nif (args.length == 0 || args.some(arg => arg === '-h' || arg === '--help')) {\n  help();\n  process.exit(0);\n}\n\n// file paths\n\nif (args.length > 2) {\n  die('invalid arguments, run with --help to see usage.\\n\\n');\n}\n\nlet source = args[0];\nlet destination = args[1];\n\nconst readStdin = source === '-';\n\nif (source && source[0] !== '/' && !readStdin) {\n  source = path.join(process.cwd(), source);\n}\n\nif (destination) {\n  if (destination[0] !== '/') {\n    destination = path.join(process.cwd(), destination);\n  }\n  let isDirectory = destination.endsWith('/');\n  if (!isDirectory) {\n    try {\n      let destStat = fs.statSync(destination);\n      isDirectory = destStat.isDirectory();\n    } catch (e) {}\n  }\n\n  if (isDirectory) {\n    if (readStdin) {\n      die('must name output file if reading from stdin\\n\\n');\n    }\n\n    let filename = path.basename(source);\n    destination = path.join(destination, filename);\n  }\n}\n\n//\n// Main\n//\n\nfunction dataCallback(e, data) {\n  if (e) {\n    die(e.message);\n  }\n\n  data = bookmarklet.parseFile(data);\n\n  if (data.errors) {\n    die(data.errors.join('\\n'));\n  }\n\n  return bookmarklet\n    .convert(data.code, data.options)\n    .then(code => {\n      if (makeDemo) {\n        code = bookmarklet.makeDemo(code, data.options);\n      }\n\n      if (destination) {\n        fs.writeFileSync(destination, code);\n      } else {\n        console.log(code);\n      }\n    })\n    .catch(e => {\n      die(e);\n    });\n}\n\nif (source !== '-') {\n  fs.readFile(source, 'utf8', dataCallback);\n} else {\n  process.stdin.resume();\n  process.stdin.setEncoding('utf8');\n\n  var buffer = '';\n  process.stdin.on('data', data => (buffer += data));\n  process.stdin.on('end', () => dataCallback(false, buffer));\n}\n"
  },
  {
    "path": "bookmarklet.js",
    "content": "const version = [3, 0, 0];\n\n// const babel = require('@babel/core');\n// const babelPresetEnv = require('@babel/preset-env');\nconst md5 = require('md5');\nconst Terser = require('terser');\n\n// metadata\nconst str = 1;\nconst list = 2;\nconst bool = 3;\nconst metadata = {\n  types: {\n    string: str,\n    list: list,\n    boolean: bool\n  },\n  keys: {\n    name: str,\n    version: str,\n    description: str,\n    repository: str,\n    author: str,\n    email: str,\n    url: str,\n    license: str,\n    script: list,\n    style: list\n  }\n};\n\nfunction quoteEscape(x) {\n  return x.replace('\"', '\\\\\"').replace(\"'\", \"\\\\'\");\n}\n\nfunction extractOptions(path) {\n  // Returns {\n  //   path: the updated path string (minus any options)\n  //   opts: plain object of options\n  // }\n  //\n  // You can prefix a path with options in the form of:\n  //\n  // ```\n  // @style !loadOnce !foo=false https://example.com/foo.css\n  // ```\n  //\n  // If there is no `=`, then the value of the option defaults to `true`.\n  // Values get converted via JSON.parse if possible, o/w they're a string.\n  //\n  let opts = {};\n  while (true) {\n    let m = path.match(/^(\\![^\\s]+)\\s+/);\n    if (m) {\n      path = path.substring(m.index + m[0].length);\n      let opt = m[1].substring(1).split('=');\n      opts[opt[0]] = opt[1] === undefined ? true : _fuzzyParse(opt[1]);\n    } else {\n      break;\n    }\n  }\n  return { path, opts };\n}\n\nconst _fuzzyParse = val => {\n  try {\n    return JSON.parse(val);\n  } catch (e) {\n    return val;\n  }\n};\n\nfunction loadScript(code, path, loadOnce) {\n  loadOnce = !!loadOnce;\n  let id = `bookmarklet__script_${md5(path).substring(0, 7)}`;\n  return `\n        function callback(){\n          ${code}\n        }\n\n        if (!${loadOnce} || !document.getElementById(\"${id}\")) {\n          var s = document.createElement(\"script\");\n          if (s.addEventListener) {\n            s.addEventListener(\"load\", callback, false)\n          } else if (s.readyState) {\n            s.onreadystatechange = callback\n          }\n          if (${loadOnce}) {\n            s.id = \"${id}\";\n          }\n          s.src = \"${quoteEscape(path)}\";\n          document.body.appendChild(s);\n        } else {\n          callback();\n        }\n    `;\n}\n\nfunction loadStyle(code, path, loadOnce) {\n  loadOnce = !!loadOnce;\n  let id = `bookmarklet__style_${md5(path).substring(0, 7)}`;\n  return `${code}\n        if (!${loadOnce} || !document.getElementById(\"${id}\")) {\n          var link = document.createElement(\"link\");\n          if (${loadOnce}) {\n            link.id = \"${id}\";\n          }\n          link.rel=\"stylesheet\";\n          link.href = \"${quoteEscape(path)}\";\n          document.body.appendChild(link);\n        }\n    `;\n}\n\nasync function minify(code) {\n  // const result = babel.transform(code, {\n  //   presets: [\n  //     [\n  //       babelPresetEnv,\n  //       {\n  //         targets: 'ie 8', // '> 0.25%, not dead',\n  //         corejs: { version: '3.9', proposals: true },\n  //         useBuiltIns: 'usage'\n  //       }\n  //     ]\n  //   ]\n  // });\n  const result = await Terser.minify(code);\n  return result.code;\n}\n\nasync function convert(code, options) {\n  code = await minify(code);\n  let stylesCode = '';\n\n  if (options.script) {\n    options.script = options.script.reverse();\n    options.script.forEach(s => {\n      let { path, opts } = extractOptions(s);\n      code = loadScript(code, path, opts.loadOnce);\n    });\n    code = await minify(code);\n  }\n\n  if (options.style) {\n    options.style.forEach(s => {\n      let { path, opts } = extractOptions(s);\n      stylesCode = loadStyle(stylesCode, path, opts.loadOnce);\n    });\n    const minifiedStyles = await minify(stylesCode);\n    code = minifiedStyles + code;\n  }\n\n  code = `(function(){${code}})()`;\n  return `javascript:${encodeURIComponent(code)}`;\n}\n\nfunction parseFile(data) {\n  let inMetadataBlock = false;\n  let openMetadata = '==Bookmarklet==';\n  let closeMetadata = '==/Bookmarklet==';\n  let rComment = /^(\\s*\\/\\/\\s*)/;\n  let mdKeys = metadata.keys;\n  let mdTypes = metadata.types;\n  let options = {};\n  let code = [];\n  let errors = [];\n\n  // parse file and gather options from metadata block if available\n  data.match(/[^\\r\\n]+/g).forEach(function (line, i, lines) {\n    // comment\n    if (rComment.test(line)) {\n      let comment = line.replace(rComment, '').trim(),\n        canonicalComment = comment.toLowerCase().replace(/\\s+/g, '');\n\n      if (!inMetadataBlock) {\n        if (canonicalComment == openMetadata.toLowerCase()) {\n          inMetadataBlock = true;\n        }\n      } else {\n        if (canonicalComment == closeMetadata.toLowerCase()) {\n          inMetadataBlock = false;\n        } else {\n          let m = comment.match(/^@([^\\s]+)\\s+(.*)$/);\n          if (m) {\n            let k = m[1];\n            let v = m[2];\n            if (k) {\n              if (mdKeys[k] == mdTypes.list) {\n                options[k] = options[k] || [];\n                options[k].push(v);\n              } else if (mdKeys[k] == mdTypes.boolean) {\n                options[k] = v.toLowerCase() == 'true';\n              } else {\n                options[k] = v;\n              }\n            } else {\n              warn(`ignoring invalid metadata option: '${k}'`);\n            }\n          }\n        }\n      }\n\n      // code\n    } else {\n      code.push(line);\n    }\n\n    if (inMetadataBlock && i + 1 == lines.length) {\n      errors.push(`missing metdata block closing '${closeMetadata}'`);\n    }\n  });\n\n  return {\n    code: code.join('\\n'),\n    options,\n    errors: errors.length ? errors : null\n  };\n}\n\nfunction makeDemo(bookmarkletCode, options) {\n  options = options || {};\n\n  const name = options.name || 'Bookmarklet';\n  const createdWith = 'https://github.com/mrcoles/bookmarklet';\n\n  const html = `<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <style>\n      html,body,div { margin: 0; padding: 0; font: normal 16px/24px Helvetica Neue, Helvetica, sans-serif; color: #333; }\n      #main { max-width: 630px; margin: 3em auto; }\n      .bookmarklet { display: inline-block; padding: .5em 1em; color: #fff; background: #33e; border-radius: 4px; text-decoration: none; }\n      a { color: #33e; }\n    </style>\n  </head>\n  <body>\n    <div id=\"main\">\n      <h1>${name}</h1>\n      <p>\n        Drag this button to your bookmarks bar to save it as a bookmarklet:\n      </p>\n      <p>\n        <a class=\"bookmarklet\" href=\"${bookmarkletCode}\">${name}</a>\n      </p>\n      ${\n        options.repo\n          ? `<p>See source at <a href=\"${options.repo}\">${options.repo}</a></p>`\n          : ''\n      }\n      <p>This page was created with the <a href=\"${createdWith}\">bookmarklet</a> npm library.</p>\n    </div>\n  </body>\n</html>\n`;\n\n  return html;\n}\n\n// Exports\n\nObject.assign(exports, {\n  version,\n  convert,\n  parseFile,\n  makeDemo,\n  metadata\n});\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"bookmarklet\",\n  \"version\": \"3.0.0\",\n  \"description\": \"A JavaScript bookmarklet compiler and demo page creator with greasemonkey userscript-like metadata options\",\n  \"main\": \"bookmarklet.js\",\n  \"bin\": {\n    \"bookmarklet\": \"bin/cli.js\"\n  },\n  \"engines\": {\n    \"node\": \">= 6.0.0\"\n  },\n  \"scripts\": {\n    \"test\": \"bash test/run.sh\"\n  },\n  \"repository\": \"github:mrcoles/bookmarklet\",\n  \"keywords\": [\n    \"bookmarklet\"\n  ],\n  \"author\": {\n    \"name\": \"Peter Coles\"\n  },\n  \"contributors\": [\n    {\n      \"name\": \"Ryan Pavlik\"\n    }\n  ],\n  \"license\": \"MIT\",\n  \"readmeFilename\": \"README.md\",\n  \"dependencies\": {\n    \"md5\": \"^2.2.1\",\n    \"terser\": \"^5.6.1\"\n  }\n}\n"
  },
  {
    "path": "test/bookmarklets/test1.bookmarklet.js",
    "content": "// ==Bookmarklet==\n// @name Test\n// @author Peter\n// @style data:text/css,imaginary%7Bfont-family%3A%20%22ok%22%7D\n// @script https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\n// ==/Bookmarklet==\n\nvar $ = jQuery.noConflict(true),\n    pCount = $('p').size(),\n    divCount = $('div').size(),\n    testElement = $('<imaginary>').appendTo('body'),\n    testPassed = testElement.css('font-family') == 'ok';\n    testElement.remove();\n\nalert('p tags: ' + pCount +\n      '\\ndiv tags: ' + divCount +\n      '\\nDid the test element get styled? ' +\n      (testPassed ? 'Of course!' : 'Nope.'));\n"
  },
  {
    "path": "test/bookmarklets/test2.bookmarklet.js",
    "content": "// ==Bookmarklet==\n// @name Test\n// @author Peter\n// @style !loadOnce data:text/css,imaginary%7Bfont-family%3A%20%22ok%22%7D\n// @script !loadOnce=false https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\n// ==/Bookmarklet==\n\nconst $ = jQuery.noConflict(true);\nconst pCount = $('p').size();\nconst divCount = $('div').size();\nconst testElement = $('<imaginary>').appendTo('body');\nconst testPassed = testElement.css('font-family') === 'ok';\n\ntestElement.remove();\n\nlet run = () => {\n  alert(\n    'p tags: ' +\n      pCount +\n      '\\ndiv tags: ' +\n      divCount +\n      '\\nDid the test element get styled? ' +\n      (testPassed ? 'Of course!' : 'Nope.')\n  );\n};\n\nrun();\n"
  },
  {
    "path": "test/expected/test1.bookmarklet.js",
    "content": "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)()"
  },
  {
    "path": "test/expected/test2.bookmarklet.js",
    "content": "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)()"
  },
  {
    "path": "test/run.sh",
    "content": "#!/usr/bin/env bash\n\nmkdir -p test/actual/\n\nEXIT_STATUS=0\n\nfor f in `ls test/bookmarklets/*.bookmarklet.js`; do\n    filename=$(basename \"$f\")\n    actual=\"test/actual/$filename\"\n    expected=\"test/expected/$filename\"\n    ./bin/cli.js \"$f\" \"$actual\"\n    if [ $? = 0 ]; then\n        diff \"$expected\" \"$actual\" -q > /dev/null\n        if [ $? = 0 ]; then\n            printf \"\\033[0;32m✓ \\033[0mPASSED ${filename}\\n\";\n        else\n            printf \"\\033[0;31m✗ \\033[0mFAILED ${filename}\\n\"\n            EXIT_STATUS=1\n        fi\n    else\n        printf \"\\033[0;31m✗ \\033[0mEXCEPTION PARSING ${filename}\\n\"\n        EXIT_STATUS=1\n    fi\ndone\n\nexit $EXIT_STATUS\n"
  }
]