{"id":13476450,"url":"https://github.com/mems/bookmarklets-context-menu","last_synced_at":"2025-03-27T02:32:59.717Z","repository":{"id":48175481,"uuid":"82490901","full_name":"mems/bookmarklets-context-menu","owner":"mems","description":"WebExtension allow to execute bookmarklets as privileged scripts","archived":false,"fork":false,"pushed_at":"2020-05-02T10:26:34.000Z","size":371,"stargazers_count":85,"open_issues_count":2,"forks_count":6,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-08-01T16:45:06.923Z","etag":null,"topics":["bookmarklets","bookmarks","contextmenu","csp","webextension"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mems.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-02-19T21:30:11.000Z","updated_at":"2024-07-06T20:20:35.000Z","dependencies_parsed_at":"2022-09-14T12:31:19.536Z","dependency_job_id":null,"html_url":"https://github.com/mems/bookmarklets-context-menu","commit_stats":null,"previous_names":[],"tags_count":8,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mems%2Fbookmarklets-context-menu","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mems%2Fbookmarklets-context-menu/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mems%2Fbookmarklets-context-menu/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mems%2Fbookmarklets-context-menu/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mems","download_url":"https://codeload.github.com/mems/bookmarklets-context-menu/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245769526,"owners_count":20669195,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bookmarklets","bookmarks","contextmenu","csp","webextension"],"created_at":"2024-07-31T16:01:30.487Z","updated_at":"2025-03-27T02:32:59.406Z","avatar_url":"https://github.com/mems.png","language":"JavaScript","funding_links":[],"categories":["JavaScript"],"sub_categories":[],"readme":"![Firefox capture of Bookmarklets context menu: Context menu opened with a folder and a bookmarklet \"Copy page as Markdown link\"](capture_firefox_desktop.png)\n\nThis extension is available for [Firefox (via AMO)](https://addons.mozilla.org/firefox/addon/bookmarklets-context-menu/)\n\n## Why use Bookmarklets context menu\n\n[Current browsers' implementations of bookmarklets are broken](#why-current-browsers-implementations-of-bookmarklets-are-broken): bookmarklet are executed as author's script, but should be executed as user's scripts (with higher pivileges).\n\nTo circumvent this restrictions, the extension _Bookmarklets context menu_ create a context menu with all bookmarklets available from user's bookmarks and executed it on demand as [content script](https://developer.mozilla.org/Add-ons/WebExtensions/Content_scripts). This allow access to a secured isolated environement, with higher privileges than author's scripts (allow to copy to clipboard without require user intialized event, cross domain fetch).\n\n\u003c!--\nThis context is defined as:\n\n- `this` (global) is an extended `Window` object, include a small subset of WebExtension APIs and [DOM object `wrappedJSObject` property](https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Content_scripts#Accessing_page_script_objects_from_content_scripts): `const Sandbox = {browser, chrome, ...window}`\n- `self` is the same as `window`, the top frame's global object\n--\u003e\n\nIf the page block the context menu, you can use the browser action of context menu:\n\n![Firefox capture of Bookmarklets context menu: Browser action highlighted](capture_firefox_browser_action.png)\n\n## Why current browsers' implementations of bookmarklets are broken?\n\n\u003e consider users over authors over implementors over specifiers over theoretical purity.\n\n— [HTML Design Principles](https://www.w3.org/TR/html-design-principles/#priority-of-constituencies)\n\nWith current implementation, bookmarklet usage is restricted because bookmarklets are not executed as privileged scripts, but as author's script.\nThat means bookmarklet are subject to security measures like \u003cabbr title=\"Content Security Policy\"\u003eCSP\u003c/abbr\u003e and \u003cabbr title=\"Cross-Origin Resource Sharing\"\u003eCORS\u003c/abbr\u003e which make it difficulte to use or impossible in some cases.\n\nThis no more the cas with Chrome and Firefox (see [bug 1267027](https://bugzilla.mozilla.org/show_bug.cgi?id=1267027), [Intent to implement](https://groups.google.com/forum/m/#!msg/mozilla.dev.platform/EVKMSAY__lA/8b1ctuJgBwAJ), [bug 1406278](https://bugzilla.mozilla.org/show_bug.cgi?id=1406278), [bug 1478037](https://bugzilla.mozilla.org/show_bug.cgi?id=1478037#c22)), but CSP still applied on subressources\n\nSee also [Wiki pages](https://github.com/mems/bookmarklets-context-menu/wiki)\n\n## Limitations of this extension\n\nThis doesn't fix broken implementations. It's just an alternative.\n\n- work well on Firefox Desktop, untested on Chrome and any other browser that support [WebExtension API](https://developer.mozilla.org/en-US/Add-ons/WebExtensions)\n- ~~[Edge don't support bookmarks WebExtension API](https://developer.mozilla.org/Add-ons/WebExtensions/API/bookmarks#Browser_compatibility)~~\n- options UI looks ugly, see [1275287 - Implement `chrome_style` in options V2 API.](https://bugzilla.mozilla.org/show_bug.cgi?id=1275287)\n- **CSP still applied to subresources** (like scripts, styles, medias, etc.). That means with a super strict CSP `default-src 'none'`, you can't use any additional scripts, styles nor medias.\n\n## Permissions required by the extension\n\nThe following permissions are used by the extension:\n\n- `bookmarks`: read the bookmark tree to get all bookmarklets\n- `contextMenus`: create context menus based on bookmarklets founded in bookmarks\n- `activeTab`: execute bookmarklet script in the active tab\n- `clipboardWrite` and `clipboardRead`: allow to use [`document.execCommand('cut'/'copy'/'paste')`](https://developer.mozilla.org/Add-ons/WebExtensions/Interact_with_the_clipboard) in bookmarklets\n- `storage`: store some preferences like \"flat context menu\"\n- `\u003call_urls\u003e`: allow bookmarklets to perform `fetch()` or `XMLHttpRequest` without crossdomain limitations\n\n## How to write a bookmarklet\n\n**It's not recommended to use external resources.** External resources are affected by CSP.\n\nIf the result of the bookmarklet is other than `undefined` (`void 0`), it will be used as HTML source of a new document opened in the same tab: `javascript:\"\u003cspan style='text-decoration:underline'\u003eUnderlined text\u003c/span\u003e\"`\n\nAn example of a bookmarklet that copy the document's title (`document.title`):\n\n```\njavascript:(s=\u003e{let%20d=document,l=e=\u003e{d.removeEventListener(\"copy\",l);e.preventDefault();e.clipboardData.setData(\"text/plain\",s);};if(d.activeElement.tagName==\"IFRAME\"){let%20s=d.createElement(\"span\");s.tabIndex=-1;s.style.position=\"fixed\";d.body.appendChild(s);s.focus();s.remove()}d.addEventListener(\"copy\",l);d.execCommand(\"copy\");a.focus()})(document.title)\n```\n\nAn example of a bookmarklet that copy the page as [Markdown link](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet#links):\n\n```\njavascript:{let%20e=document,t=e.URL,a=[e.title].concat(Array.from(e.querySelectorAll(\"h1,h2\")),t).reduce((e,t)=\u003ee||t.textContent\u0026\u0026t.textContent.trim().replace(/\\s+/g,\"%20\")||t.trim(),\"\"),n=(e,t,a=\"\u0026%23x\",n=\";\")=\u003ee.replace(t,e=\u003ea+e.charCodeAt(0).toString(16).padStart(2,\"0\")+n),r=n(t,/[()\"]/g,\"%25\",\"\"),l=e.contentType.startsWith(\"image/\"),[,i=\"Untitled\"]=/\\/([^/.]+$|[^/]+(?=\\.[^.]*$))/g.exec(new%20URL(t).pathname)||[];i+=\".\"+e.contentType.split(\"/\")[1].split(\"+\")[0];let%20c=o=\u003e{let%20p=o.clipboardData,d=p.setData.bind(p);e.removeEventListener(\"copy\",c),o.preventDefault(),p.clearData(),d(\"text/x-moz-url\",t),d(\"text/uri-list\",t),d(\"text/html\",l?`\u003cimg%20src=\"${r}\"%20alt=\"${n(i,/[\"\u0026\u003c\u003e]/g)}\"\u003e`:`\u003ca%20href=\"${r}\"\u003e${n(a,/[\u0026\u003c\u003e]/g)}\u003c/a\u003e`),d(\"text/plain\",l||a!==t?(l?\"!\":\"\")+\"[\"+(l?i:a).replace(/[\\\\\u003c\u003e\\[\\]]/g,\"\\\\$\u0026\")+\"](\"+r+\")\":r)};if([\"IFRAME\",\"FRAME\"].includes(e.activeElement.tagName)){let%20t=e.createElement(\"span\");t.tabIndex=-1,t.setAttribute(\"aria-hidden\",\"true\"),t.style.position=\"fixed\",e.documentElement.appendChild(t),t.focus(),t.remove()}e.addEventListener(\"copy\",c),e.execCommand(\"copy\")}void(0)\n```\n\nIf you get the following error `Bookmarklet error: SecurityError: The operation is insecure.`, that means you use `document.write(potentiallyUnsafeHTML)`, when you should use `wrappedJSObject.document.write(potentiallyUnsafeHTML)` instead. It's related to content script (privilegied) context vs page context.\n\nNote: It's impossible for the extension to catch asynchronous errors (in listener, setTimeout, etc.) even with a global error handler. You must use a `try...catch` block in your asynchronous functions. Alternatively you can use the add-ons debug mode [`about:debugging`](https://developer.mozilla.org/en-US/docs/Tools/about:debugging#Add-ons)\n\nIf you need `document.execCommand()`, be sure there is no element in / iframe focused:\n\n```js\n// execCommand will not been executed if a frame or an iframe is focused\nif([\"IFRAME\",\"FRAME\"].includes(document.activeElement.tagName)){\n\tlet focusable = document.createElement(\"span\");\n\tfocusable.tabIndex = -1;// focusable\n\tfocusable.setAttribute(\"aria-hidden\", \"true\");// will not be announced by AT\n\tfocusable.style.position = \"fixed\";\n\tdocument.documentElement.appendChild(focusable);// don't use doc.body because in case of frame the body is the frameset and execCommand will not work\n\tfocusable.focus();// force focus, but will not scroll into view, because it have fixed position\n\tfocusable.remove();// remove focus, without force to scroll into view to an other element\n}\nlet listener = event =\u003e {\n\tdocument.removeEventListener(\"copy\", listener);// one time listener\n\t// Do whaterver you want. Exemple: in copy event, use event.clipboardData\n};\ndocument.addEventListener(\"copy\", listener);\ndocument.execCommand(\"copy\");// will dispatch copy event\n```\n\nSee also:\n\n- [Bookmarkleter](http://chriszarate.github.io/bookmarkleter/)\n- [Bookmarklet - Wikipedia](https://en.wikipedia.org/wiki/Bookmarklet#Concept)\n- [Create Bookmarklets - The Right Way](https://code.tutsplus.com/tutorials/create-bookmarklets-the-right-way--net-18154)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmems%2Fbookmarklets-context-menu","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmems%2Fbookmarklets-context-menu","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmems%2Fbookmarklets-context-menu/lists"}