├── LICENSE ├── README.md ├── background.js ├── clipboard-helper.js ├── common.js ├── icon.png ├── icon128.png ├── icon16.png ├── icon48.png ├── manifest.json ├── options.css ├── options.html ├── options.js ├── popup.css ├── popup.html └── popup.js /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2012 Hiroaki Nakamura 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of 5 | this software and associated documentation files (the "Software"), to deal in 6 | the Software without restriction, including without limitation the rights to 7 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 8 | of the Software, and to permit persons to whom the Software is furnished to do 9 | so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Format Link for Firefox 2 | 3 | ## Why do I need it? 4 | To format the link of the active tab instantly to use in Markdown, reST, HTML, Text, Textile or other formats. 5 | 6 | ## How to use 7 | You can use keyboard shortcuts, context menus, or the toolbar button of Format Link extension 8 | to copy a link in the specified format. Before doing that, you can optionally select some text 9 | which may or may not contain a link. 10 | 11 | ### keyboard shortcut 12 | The keyboard shortcut for "Copy a link in the default format" is shortcut for clicking the 13 | toolbar button. The link is copied in the default format and the popup is shown under 14 | the toolbar button. 15 | 16 | Also there are shortcuts for copying in the link in the corresponding format regardless of 17 | the default format. 18 | 19 | See [Manage extension shortcuts in Firefox | Firefox Help](https://support.mozilla.org/en-US/kb/manage-extension-shortcuts-firefox) for showing and changing keyboard shortcuts. 20 | 21 | ### context menu 22 | Open the context menu and select the "Format Link as XXX" menu item. 23 | "XXX" in the menu item label changes as you change the default format by clicking the "Set as default" button in the popup page for the toolbar button. 24 | 25 | If you want to disable the context menu option, you can uncheck the "Context Menus" checkbox. 26 | 27 | If you check the "Create submenus" in the options page and save the options, 28 | submenus for each format are created under the "Format Link" context menu group. 29 | 30 | ### toolbar button 31 | When you press the toolbar button of "Format Link", the link is copied in the default format, 32 | the popup page becomes open, and the formatted text is shown in the text area. 33 | 34 | If you want to copy the link in different format, you can press one of the radio buttons. 35 | 36 | Also if you want to change the default format, you can press the "Set as default" button. 37 | 38 | ## Flexible settings 39 | You can modify formats in [Tools] -> [Extensions] -> Clik "Options" link in "Format Link" Extension. 40 | In format settings, you can use the mini template language. 41 | 42 | * {{variable}} 43 | * variable = title / url / page_url / text / selected_text 44 | * The value of variable `title` is the HTML page title. 45 | * The value of variable `text` is: 46 | * the selected text if some text is selected, 47 | * the link text if you open the context menu over a link, 48 | * the page title otherwise. 49 | * The value of variable `selected_text` is 50 | * the selected text if some text is selected, 51 | * empty string if no text is selected. 52 | * The value of the variable `url` is the link if you open the context menu over a link, 53 | the first link if selection contains a link, or the HTML page URL otherwise. 54 | * The value of the variable `page_url` is always the HTML page URL. 55 | * No spaces are allowed between variable name and braces. 56 | * {{variable.s("foo","bar")}} 57 | * Which means `variable.replace(new RegExp("foo", 'g'), "bar")` 58 | * You can use escape character \ in strings. 59 | * You must escape the first argument for string and regexp. 60 | For example, `.s("\\[","\\[")` means replacing `\[` with `\\[` 61 | * You can chain multiple .s("foo","bar") 62 | * You can use the escape character \ in strings. For example, you need to escape `\` with `\` like `\\`, 63 | and also you need to escape `{` with `\` like `\{`. See the LaTeX example below. 64 | * Other characters are treated as literal strings. 65 | 66 | Here are examples: 67 | 68 | * Markdown 69 | 70 | ``` 71 | [{{text.s("\\[","\\[").s("\\]","\\]")}}]({{url.s("\\)","%29")}}) 72 | ``` 73 | 74 | * reST 75 | 76 | ``` 77 | {{text}} <{{url}}>`_ 78 | ``` 79 | 80 | * HTML 81 | 82 | ``` 83 | {{text.s("<","<")}} 84 | ``` 85 | 86 | * Text 87 | 88 | ``` 89 | {{text}}\n{{url}} 90 | ``` 91 | 92 | * Redmine Texitile 93 | 94 | ``` 95 | "{{title.s("\"",""").s("\\[","[")}}":{{url}} 96 | ``` 97 | 98 | * LaTeX 99 | 100 | ``` 101 | \\href\{{{url}}\}\{{{text}}\} 102 | ``` 103 | 104 | ### Global variable replacements 105 | 106 | You can preprocess value of each variable before applying above formats. 107 | 108 | For example, you can replace opening and close double quotes in `text` variable 109 | with ASCII doublequote by settings `text` variable global replacements to 110 | `{{text.s("[“”]","\"")}}` or `{{text.s("[\\u201c\\u201d]","\"")}}`. 111 | 112 | ## KNOWN LIMITATIONS 113 | 114 | * Due to security reason, you cannot copy the URL on some pages like addons.mozilla.org. See [Content scripts - Mozilla | MDN](https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts) for details. 115 | 116 | ## License 117 | MIT License. 118 | Source codes are hosted at [Github](https://github.com/hnakamur/FormatLink-Firefox) 119 | 120 | ## Credits 121 | 122 | ### Icon 123 | I synthesized two icons (a pencil and a link) to produce ```icon.png```. 124 | 125 | * A pencil icon from [Onebit free icon set](http://www.icojoy.com/articles/44/) © 2010 [Khodjaev Stanislav](http://www.icojoy.com/), used under a License: These icons are free to use in any kind of commercial or non-commercial project unlimited times. 126 | * A link icon from [Bremen icon set](http://pc.de/icons/#Bremen) © 2010 [Patricia Clausnitzer](http://pc.de/icons/), used under a [Creative Commons Attribution 3.0 License](hhttp://creativecommons.org/licenses/by/3.0/) 127 | 128 | ### Extension 129 | This extension "Format Link" are inspired by extensions below: 130 | 131 | * [Chrome Web Store - Create Link](https://chrome.google.com/webstore/detail/gcmghdmnkfdbncmnmlkkglmnnhagajbm) by [ku (KUMAGAI Kentaro)](https://github.com/ku) 132 | * [Make Link :: Add-ons for Firefox](https://addons.mozilla.org/en-US/firefox/addon/make-link/) by [Rory Parle](https://addons.mozilla.org/en-US/firefox/user/90/) 133 | -------------------------------------------------------------------------------- /background.js: -------------------------------------------------------------------------------- 1 | async function saveDefaultFormat(formatID) { 2 | await browser.storage.sync.set({defaultFormat: formatID}); 3 | } 4 | 5 | (async function() { 6 | browser.commands.onCommand.addListener(async (command) => { 7 | try { 8 | const prefix = 'copy-link-in-format'; 9 | if (command.startsWith(prefix)) { 10 | let formatID = command.substr(prefix.length); 11 | const options = await gettingOptions(); 12 | const format = options['format' + formatID]; 13 | const asHTML = options['html' + formatID]; 14 | await copyLinkToClipboard(format, asHTML, options); 15 | } 16 | } catch (err) { 17 | console.error("FormatLink extension failed to copy URL to clipboard.", err); 18 | } 19 | }); 20 | 21 | try { 22 | const options = await gettingOptions(); 23 | await createContextMenus(options); 24 | browser.contextMenus.onClicked.addListener(async (info, tab) => { 25 | const prefix = "format-link-format"; 26 | if (info.menuItemId.startsWith(prefix)) { 27 | try { 28 | const options = await gettingOptions(); 29 | let formatID = info.menuItemId.substr(prefix.length); 30 | if (formatID === "-default") { 31 | formatID = options.defaultFormat; 32 | } 33 | const format = options['format' + formatID]; 34 | const asHTML = options['html' + formatID]; 35 | await copyLinkToClipboard(format, asHTML, options, info.linkUrl, info.linkText); 36 | } catch (err) { 37 | console.error("FormatLink extension failed to copy URL to clipboard.", err); 38 | } 39 | } 40 | }); 41 | 42 | async function handleMessage(request, sender, sendResponse) { 43 | if (request.messageID === 'update-default-format') { 44 | const formatID = request.formatID; 45 | await saveDefaultFormat(formatID); 46 | 47 | const options = await gettingOptions(); 48 | const defaultFormat = options['title' + request.formatID]; 49 | await browser.contextMenus.update( 50 | "format-link-format-default", 51 | { title: "Format Link as " + defaultFormat }); 52 | sendResponse({response: 'default format updated'}); 53 | } else { 54 | sendResponse({response: 'invalid messageID'}); 55 | } 56 | } 57 | browser.runtime.onMessage.addListener(handleMessage); 58 | } catch (err) { 59 | console.error("failed to create context menu for FormatLink extension", err); 60 | }; 61 | })(); 62 | -------------------------------------------------------------------------------- /clipboard-helper.js: -------------------------------------------------------------------------------- 1 | // This function must be called in a visible page, such as a browserAction popup 2 | // or a content script. Calling it in a background page has no effect! 3 | function FormatLink_copyHTMLToClipboard(text) { 4 | function oncopy(event) { 5 | document.removeEventListener("copy", oncopy, true); 6 | // Hide the event from the page to prevent tampering. 7 | event.stopImmediatePropagation(); 8 | 9 | // Overwrite the clipboard content. 10 | event.preventDefault(); 11 | event.clipboardData.setData("text/plain", text); 12 | event.clipboardData.setData("text/html", text); 13 | } 14 | document.addEventListener("copy", oncopy, true); 15 | 16 | // Requires the clipboardWrite permission, or a user gesture: 17 | document.execCommand("copy"); 18 | } 19 | 20 | function FormatLink_formatLink(format, options, newline, linkUrl, linkText) { 21 | function getFirstLinkInSelection(selection) { 22 | return selection.anchorNode.parentNode.href; 23 | } 24 | 25 | function formatURL(format, url, title, text, selectedText, newline, isVar) { 26 | let result = ''; 27 | let i = 0, len = format.length; 28 | 29 | function parseLiteral(str) { 30 | if (format.substr(i, str.length) === str) { 31 | i += str.length; 32 | return str; 33 | } else { 34 | return null; 35 | } 36 | } 37 | 38 | function parseString() { 39 | let str = ''; 40 | if (parseLiteral('"')) { 41 | while (i < len) { 42 | if (parseLiteral('\\')) { 43 | if (i < len) { 44 | str += format.substr(i++, 1); 45 | } else { 46 | throw new Error('parse error expected "'); 47 | } 48 | } else if (parseLiteral('"')) { 49 | return str; 50 | } else { 51 | if (i < len) { 52 | str += format.substr(i++, 1); 53 | } else { 54 | throw new Error('parse error expected "'); 55 | } 56 | } 57 | } 58 | } else { 59 | return null; 60 | } 61 | } 62 | 63 | function processVar(name, value) { 64 | let work = value; 65 | if (!isVar) { 66 | const varFormat = options[name + '_format']; 67 | if (varFormat) { 68 | work = formatURL(varFormat, url, title, text, selectedText, newline, true); 69 | } 70 | } 71 | while (i < len) { 72 | if (parseLiteral('.s(')) { 73 | let arg1 = parseString(); 74 | if (arg1 != null && parseLiteral(',')) { 75 | let arg2 = parseString(); 76 | if (arg2 != null && parseLiteral(')')) { 77 | let regex = new RegExp(arg1, 'g'); 78 | work = work.replace(regex, arg2); 79 | } else { 80 | throw new Error('parse error'); 81 | } 82 | } else { 83 | throw new Error('parse error'); 84 | } 85 | } else if (parseLiteral('}}')) { 86 | result += work; 87 | return; 88 | } else { 89 | throw new Error('parse error'); 90 | } 91 | } 92 | } 93 | 94 | while (i < len) { 95 | if (parseLiteral('\\')) { 96 | if (parseLiteral('n')) { 97 | result += newline; 98 | // isWindows ? "\r\n" : "\n"; 99 | } else if (parseLiteral('t')) { 100 | result += "\t"; 101 | } else { 102 | result += format.substr(i++, 1); 103 | } 104 | } else if (parseLiteral('{{')) { 105 | if (parseLiteral('title')) { 106 | processVar('title', title); 107 | } else if (parseLiteral('url')) { 108 | processVar('url', url); 109 | } else if (parseLiteral('page_url')) { 110 | processVar('page_url', window.location.href); 111 | } else if (parseLiteral('selected_text')) { 112 | processVar('selected_text', selectedText); 113 | } else if (parseLiteral('text')) { 114 | processVar('text', text); 115 | } 116 | } else { 117 | result += format.substr(i++, 1); 118 | } 119 | } 120 | return result; 121 | } 122 | 123 | let title = document.title; 124 | let text = linkText; 125 | let href = linkUrl; 126 | let selectedText = ''; 127 | let selection = window.getSelection(); 128 | if (selection.rangeCount > 0) { 129 | selectedText = selection.toString().trim(); 130 | console.log('set selectedText from window.getSelection', selectedText); 131 | 132 | if (selectedText) { 133 | let hrefInSelection = getFirstLinkInSelection(selection); 134 | if (!href && hrefInSelection) { 135 | href = hrefInSelection; 136 | } 137 | } 138 | } 139 | if (!selectedText) { 140 | const elem = document.activeElement; 141 | if (elem && elem.selectionStart !== elem.selectionEnd) { 142 | selectedText = elem.value.substring(elem.selectionStart, elem.selectionEnd); 143 | console.log('set selectedText from activeElement', selectedText); 144 | } 145 | } 146 | if (selectedText) { 147 | text = selectedText; 148 | } 149 | if (!text) { 150 | text = title; 151 | } 152 | if (!href) { 153 | href = window.location.href; 154 | } 155 | 156 | return formatURL(format, href, title, text, selectedText, newline); 157 | } 158 | -------------------------------------------------------------------------------- /common.js: -------------------------------------------------------------------------------- 1 | const FORMAT_MAX_COUNT = 9; 2 | 3 | const DEFAULT_OPTIONS = { 4 | "defaultFormat": "1", 5 | "title1": "Markdown", 6 | "format1": "[{{text.s(\"\\\\[\",\"\\\\[\").s(\"\\\\]\",\"\\\\]\")}}]({{url.s(\"\\\\(\",\"%28\").s(\"\\\\)\",\"%29\")}})", 7 | "html1": 0, 8 | "title2": "reST", 9 | "format2": "`{{text}} <{{url}}>`_", 10 | "html2": 0, 11 | "title3": "Text", 12 | "format3": "{{text}}\\n{{url}}", 13 | "html3": 0, 14 | "title4": 'HTML', 15 | "format4": "{{text.s(\"<\",\"<\")}}", 16 | "html4": 1, 17 | "title5": "LaTeX", 18 | "format5": "\\\\href\\{{{url}}\\}\\{{{text}}\\}", 19 | "html5": 0, 20 | "title6": "", 21 | "format6": "", 22 | "html6": 0, 23 | "title7": "", 24 | "format7": "", 25 | "html7": 0, 26 | "title8": "", 27 | "format8": "", 28 | "html8": 0, 29 | "title9": "", 30 | "format9": "", 31 | "html9": 0, 32 | "page_url_format": "", 33 | "selected_text_format": "", 34 | "text_format": "", 35 | "title_format": "", 36 | "url_format": "", 37 | "enableContextMenus": true, 38 | "createSubmenus": false 39 | }; 40 | 41 | const VARIABLE_NAMES = [ 42 | "page_url", 43 | "selected_text", 44 | "text", 45 | "title", 46 | "url", 47 | ]; 48 | 49 | async function gettingOptions() { 50 | let options = await browser.storage.sync.get(null); 51 | if (Object.keys(options).length === 0) { 52 | options = DEFAULT_OPTIONS; 53 | } 54 | return options; 55 | } 56 | 57 | function getFormatCount(options) { 58 | let i; 59 | for (i = 1; i <= 9; ++i) { 60 | let optTitle = options['title' + i]; 61 | let optFormat = options['format' + i]; 62 | if (optTitle === '' || optFormat === '') { 63 | break; 64 | } 65 | } 66 | return i - 1; 67 | } 68 | 69 | async function copyLinkToClipboard(format, asHTML, options, linkUrl, linkText) { 70 | try { 71 | const results = await browser.tabs.executeScript({ 72 | code: "typeof FormatLink_formatLink === 'function';", 73 | }); 74 | // The content script's last expression will be true if the function 75 | // has been defined. If this is not the case, then we need to run 76 | // clipboard-helper.js to define functions FormatLink_formatLink 77 | // and FormatLink_copyHTMLToClipboard. 78 | if (!results || results[0] !== true) { 79 | await browser.tabs.executeScript({ 80 | file: "clipboard-helper.js", 81 | }); 82 | } 83 | 84 | const newline = browser.runtime.PlatformOs === 'win' ? '\r\n' : '\n'; 85 | 86 | let code = 'FormatLink_formatLink(' + JSON.stringify(format) + ',' + 87 | JSON.stringify(options) + ',' + 88 | JSON.stringify(newline) + ',' + 89 | (linkUrl ? JSON.stringify(linkUrl) + ',' : '') + 90 | (linkText ? JSON.stringify(linkText) + ',' : '') + 91 | ');'; 92 | const result = await browser.tabs.executeScript({code}); 93 | const data = result[0]; 94 | 95 | console.log('before copying to clipboard, data=', data, ', asHTML=', asHTML); 96 | if (asHTML) { 97 | await browser.tabs.executeScript({ 98 | code: 'FormatLink_copyHTMLToClipboard(' + JSON.stringify(data) + ');' 99 | }); 100 | } else { 101 | await navigator.clipboard.writeText(data); 102 | } 103 | console.log('clipboard successfully set by FormatLink'); 104 | 105 | return data; 106 | } catch (err) { 107 | // This could happen if the extension is not allowed to run code in 108 | // the page, for example if the tab is a privileged page. 109 | console.error('Failed to copy text: ' + err); 110 | } 111 | } 112 | 113 | function creatingContextMenuItem(props) { 114 | return new Promise((resolve, reject) => { 115 | browser.contextMenus.create(props, () => { 116 | const err = browser.runtime.lastError; 117 | if (err) { 118 | reject(err); 119 | } else { 120 | resolve(); 121 | } 122 | }); 123 | }); 124 | } 125 | 126 | async function createContextMenus(options) { 127 | await browser.contextMenus.removeAll(); 128 | if (options.enableContextMenus) { 129 | if (options.createSubmenus) { 130 | const count = getFormatCount(options); 131 | for (let i = 0; i < count; i++) { 132 | let format = options['title' + (i + 1)]; 133 | await creatingContextMenuItem({ 134 | id: "format-link-format" + (i + 1), 135 | title: "as " + format, 136 | contexts: ["all"] 137 | }); 138 | } 139 | } else { 140 | const defaultFormat = options['title' + options['defaultFormat']]; 141 | await creatingContextMenuItem({ 142 | id: "format-link-format-default", 143 | title: "Format Link as " + defaultFormat, 144 | contexts: ["all"] 145 | }); 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnakamur/FormatLink-Firefox/1f462f64114c2cb68148b49e9829216627f20824/icon.png -------------------------------------------------------------------------------- /icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnakamur/FormatLink-Firefox/1f462f64114c2cb68148b49e9829216627f20824/icon128.png -------------------------------------------------------------------------------- /icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnakamur/FormatLink-Firefox/1f462f64114c2cb68148b49e9829216627f20824/icon16.png -------------------------------------------------------------------------------- /icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hnakamur/FormatLink-Firefox/1f462f64114c2cb68148b49e9829216627f20824/icon48.png -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Format Link", 3 | "version": "5.4.1", 4 | "manifest_version": 2, 5 | "description": "Format a link and copy it to the clipboard.", 6 | "icons": { 7 | "16": "icon16.png", 8 | "48": "icon48.png", 9 | "128": "icon128.png" 10 | }, 11 | "applications": { 12 | "gecko": { 13 | "id": "pocemhmkmchpgamlnocemnbhlcjcbjgg@chrome-store-foxified-2931337014" 14 | } 15 | }, 16 | "background": { 17 | "scripts": [ 18 | "common.js", 19 | "background.js" 20 | ] 21 | }, 22 | "browser_action": { 23 | "default_icon": "icon.png", 24 | "default_title": "Format Link", 25 | "default_popup": "popup.html", 26 | "browser_style": true 27 | }, 28 | "options_ui": { 29 | "page": "options.html" 30 | }, 31 | "commands": { 32 | "_execute_browser_action": { 33 | "suggested_key": { "default": "Shift+Alt+C" }, 34 | "description": "Copy a link in the default format" 35 | }, 36 | "copy-link-in-format1": { 37 | "suggested_key": { "default": "Shift+Alt+1" }, 38 | "description": "Copy a link in format 1" 39 | }, 40 | "copy-link-in-format2": { 41 | "suggested_key": { "default": "Shift+Alt+2" }, 42 | "description": "Copy a link in format 2" 43 | }, 44 | "copy-link-in-format3": { 45 | "suggested_key": { "default": "Shift+Alt+3" }, 46 | "description": "Copy a link in format 3" 47 | }, 48 | "copy-link-in-format4": { 49 | "suggested_key": { "default": "Shift+Alt+4" }, 50 | "description": "Copy a link in format 4" 51 | }, 52 | "copy-link-in-format5": { 53 | "suggested_key": { "default": "Shift+Alt+5" }, 54 | "description": "Copy a link in format 5" 55 | }, 56 | "copy-link-in-format6": { 57 | "suggested_key": { "default": "Shift+Alt+6" }, 58 | "description": "Copy a link in format 6" 59 | }, 60 | "copy-link-in-format7": { 61 | "suggested_key": { "default": "Shift+Alt+7" }, 62 | "description": "Copy a link in format 7" 63 | }, 64 | "copy-link-in-format8": { 65 | "suggested_key": { "default": "Shift+Alt+8" }, 66 | "description": "Copy a link in format 8" 67 | }, 68 | "copy-link-in-format9": { 69 | "suggested_key": { "default": "Shift+Alt+9" }, 70 | "description": "Copy a link in format 9" 71 | } 72 | }, 73 | "permissions": [ 74 | "activeTab", 75 | "clipboardWrite", 76 | "contextMenus", 77 | "storage", 78 | "tabs" 79 | ] 80 | } 81 | -------------------------------------------------------------------------------- /options.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 8px; 3 | } 4 | h2 { 5 | margin-top: 24px; 6 | } 7 | 8 | .formats { 9 | display: grid; 10 | grid-template-columns: 12rem 1fr 4rem; 11 | gap: 16px; 12 | } 13 | input { 14 | font-family: "Lucida Console", Monaco, "Courier New", Courier, monospace; 15 | } 16 | .label { 17 | font-weight: bold; 18 | } 19 | .global-replacements { 20 | display: grid; 21 | grid-template-columns: 12rem 1fr; 22 | gap: 16px; 23 | } 24 | .btnGroup { 25 | margin-top: 24px; 26 | } 27 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FormatLink settings 6 | 7 | 8 | 9 |

FormatLink settings

10 |
11 |

Formats

12 |
13 | 14 | Title 15 | Format 16 | HTML 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 |
55 | 56 | 57 |

Global Replacements

58 |

Variable values are preprocessed before applying above formats.

59 |
60 | 61 | Variable 62 | Format 63 | 64 | page_url 65 | 66 | 67 | selected_text 68 | 69 | 70 | text 71 | 72 | 73 | title 74 | 75 | 76 | url 77 | 78 | 79 |
80 | 81 | 82 |

Misc

83 |
84 | 85 | 86 |
87 | 88 |
89 | 90 | 91 |
92 | 93 | 94 |
95 | 96 | 97 |
98 | 99 |
100 | 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | async function restoreOptions() { 2 | const options = await gettingOptions(); 3 | for (let i = 1; i <= 9; ++i) { 4 | document.getElementById('title'+i).value = options['title'+i] || ''; 5 | document.getElementById('format'+i).value = options['format'+i] || ''; 6 | document.getElementById('html'+i).checked = !!options['html'+i]; 7 | } 8 | for (let name of VARIABLE_NAMES) { 9 | document.getElementById(name+'_format').value = options[name+'_format'] || ''; 10 | } 11 | document.getElementById('enableContextMenusCheckbox').checked = options['enableContextMenus']; 12 | document.getElementById('createSubmenusCheckbox').checked = options['createSubmenus']; 13 | } 14 | 15 | async function saveOptions(defaultFormatID) { 16 | let options; 17 | try { 18 | options = defaultFormatID ? 19 | {'defaultFormat': defaultFormatID} : await gettingOptions(); 20 | for (let i = 1; i <= 9; ++i) { 21 | options['title'+i] = document.getElementById('title'+i).value; 22 | options['format'+i] = document.getElementById('format'+i).value; 23 | options['html'+i] = document.getElementById('html'+i).checked ? 1 : 0; 24 | } 25 | for (let name of VARIABLE_NAMES) { 26 | options[name+'_format'] = document.getElementById(name+'_format').value; 27 | } 28 | options['enableContextMenus'] = document.getElementById('enableContextMenusCheckbox').checked; 29 | options['createSubmenus'] = document.getElementById('createSubmenusCheckbox').checked; 30 | } catch (err) { 31 | console.error("failed to get options", err); 32 | } 33 | try { 34 | await browser.storage.sync.set(options); 35 | } catch (err) { 36 | console.error("failed to save options", err); 37 | } 38 | try { 39 | await createContextMenus(options); 40 | } catch (err) { 41 | console.error("failed to update context menu", err); 42 | } 43 | } 44 | 45 | async function restoreDefaults() { 46 | for (let i = 1; i <= 9; ++i) { 47 | document.getElementById('title'+i).value = DEFAULT_OPTIONS['title'+i] || ''; 48 | document.getElementById('format'+i).value = DEFAULT_OPTIONS['format'+i] || ''; 49 | document.getElementById('html'+i).checked = DEFAULT_OPTIONS['html'+i] || 0; 50 | } 51 | for (let name of VARIABLE_NAMES) { 52 | document.getElementById(name+'_format').value = DEFAULT_OPTIONS[name+'_format'] || ''; 53 | } 54 | document.getElementById('enableContextMenusCheckbox').checked = DEFAULT_OPTIONS['enableContextMenus']; 55 | document.getElementById('createSubmenusCheckbox').checked = DEFAULT_OPTIONS['createSubmenus']; 56 | return saveOptions(DEFAULT_OPTIONS['defaultFormat']); 57 | } 58 | 59 | async function init() { 60 | await restoreOptions(); 61 | document.getElementById('enableContextMenusCheckbox'). 62 | addEventListener('click', function(e) { 63 | document.getElementById('createSubmenusCheckbox').disabled = 64 | !document.getElementById('enableContextMenusCheckbox').checked; 65 | }); 66 | document.getElementById('saveButton'). 67 | addEventListener('click', function(e) { 68 | e.preventDefault(); 69 | saveOptions(); 70 | }); 71 | document.getElementById('restoreDefaultsButton'). 72 | addEventListener('click', function(e) { 73 | e.preventDefault(); 74 | restoreDefaults(); 75 | }); 76 | } 77 | document.addEventListener('DOMContentLoaded', init); 78 | -------------------------------------------------------------------------------- /popup.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-width:360px; 3 | overflow-x:hidden; 4 | margin: 4px; 5 | } 6 | textarea { 7 | width: 100%; 8 | } 9 | fieldset { 10 | border: none; 11 | margin: 4px; 12 | padding: 0; 13 | } 14 | label { 15 | font: 90% 'Lucida Grande',Geneva,Helvetica,Arial,sans-serif; 16 | margin: 0 4px 0 4px; 17 | } 18 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | FormatLink Extension's Popup 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | function populateText(formattedText) { 2 | const textElem = document.getElementById('textToCopy'); 3 | textElem.value = formattedText; 4 | textElem.focus(); 5 | textElem.select(); 6 | } 7 | 8 | function populateFormatGroup(options) { 9 | const defaultFormat = options['defaultFormat']; 10 | let radios = []; 11 | const cnt = getFormatCount(options); 12 | let group = document.getElementById('formatGroup'); 13 | while (group.hasChildNodes()) { 14 | group.removeChild(group.childNodes[0]); 15 | } 16 | for (let i = 1; i <= cnt; ++i) { 17 | let radioId = 'format' + i; 18 | 19 | let btn = document.createElement('input'); 20 | btn.setAttribute('type', 'radio'); 21 | btn.setAttribute('name', 'fomrat'); 22 | btn.setAttribute('id', radioId); 23 | btn.setAttribute('value', i); 24 | if (i == defaultFormat) { 25 | btn.setAttribute('checked', 'checked'); 26 | } 27 | btn.addEventListener('click', async e => { 28 | const formatID = e.target.value; 29 | const format = options['format' + formatID]; 30 | const asHTML = options['html' + formatID]; 31 | const formattedText = await copyLinkToClipboard(format, asHTML, options); 32 | populateText(formattedText); 33 | }); 34 | 35 | let optTitle = options['title' + i]; 36 | let text = document.createTextNode(optTitle); 37 | 38 | let label = document.createElement('label'); 39 | label.appendChild(btn); 40 | label.appendChild(text); 41 | 42 | group.appendChild(label); 43 | } 44 | } 45 | 46 | function getSelectedFormatID() { 47 | for (let i = 1; i <= FORMAT_MAX_COUNT; ++i) { 48 | let radio = document.getElementById('format' + i); 49 | if (radio && radio.checked) { 50 | return i; 51 | } 52 | } 53 | return undefined; 54 | } 55 | 56 | async function init() { 57 | document.getElementById('saveDefaultFormatButton').addEventListener('click', async () => { 58 | let formatID = getSelectedFormatID(); 59 | if (formatID) { 60 | await browser.runtime.sendMessage({ 61 | messageID: 'update-default-format', 62 | formatID 63 | }); 64 | } 65 | }); 66 | 67 | document.getElementById('optionsButton').addEventListener('click', () => { 68 | browser.runtime.openOptionsPage(); 69 | }); 70 | 71 | const options = await gettingOptions(); 72 | const format = options['format' + options.defaultFormat]; 73 | const asHTML = options['html' + options.defaultFormat]; 74 | let formattedText = await copyLinkToClipboard(format, asHTML, options); 75 | populateText(formattedText); 76 | populateFormatGroup(options); 77 | } 78 | document.addEventListener('DOMContentLoaded', init); 79 | --------------------------------------------------------------------------------