├── logo-128.png ├── logo-947.png ├── screenshot.png ├── pack.sh ├── manifest.json ├── readme.md ├── options.html ├── formatters.js ├── options.js ├── popup.html └── popup.js /logo-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/refgen/HEAD/logo-128.png -------------------------------------------------------------------------------- /logo-947.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/refgen/HEAD/logo-947.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reorx/refgen/HEAD/screenshot.png -------------------------------------------------------------------------------- /pack.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | : ${outdir:="$HOME/Downloads"} 4 | 5 | version=$(grep '"version' manifest.json | grep -Eo '\d.\d.\d') 6 | if [ -z "$version" ]; then 7 | echo "cannot get version" 8 | exit 1 9 | fi 10 | filename="refgen-$version.zip" 11 | 12 | zip $filename * -vr -x '*.git*' 13 | mv $filename $outdir 14 | 15 | echo "Result:" 16 | ls -l $outdir/$filename 17 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Refgen", 3 | "version": "1.5.0", 4 | "manifest_version": 3, 5 | "description": "Generate referrer for current page.", 6 | "author": "Reorx ", 7 | "icons": { 8 | "16": "logo-947.png", 9 | "48": "logo-947.png", 10 | "128": "logo-947.png" 11 | }, 12 | "action": { 13 | "default_popup": "popup.html" 14 | }, 15 | "options_ui": { 16 | "page": "options.html", 17 | "open_in_tab": false 18 | }, 19 | "commands": { 20 | "_execute_action": { 21 | "suggested_key": { 22 | "default": "Alt+U", 23 | "mac": "Command+U" 24 | } 25 | } 26 | }, 27 | "permissions": [ 28 | "tabs", 29 | "activeTab", 30 | "storage", 31 | "scripting" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Refgen 2 | 3 | An extension to generate markdown style link reference for current page. 4 | 5 | **🚀 New feature in 2023: clean URL tracking parameters and remember the preference for the site.** 6 | 7 | Get it on [Chrome Web Store](https://chrome.google.com/webstore/detail/refgen/ceknnceiglebkdhimphmkbgjbbcnhiib) 8 | 9 | ![](screenshot.png) 10 | 11 | 12 | ## TODO 13 | 14 | - [x] Copy as HTML 15 | - [x] Support other markup formats 16 | - AsciiDoc 17 | - reStructuredText 18 | - MediaWiki 19 | - HTML 20 | - [x] support using canonical url and save the preference for the site 21 | - [x] title text remove \n 22 | - [x] markdown title text normalize (add \ before _ and * etc.) 23 | - [x] purge URL (manually and auto) 24 | - [x] show parameters list, check the ones that need to be removed and save the preference for the site 25 | - [ ] right-click menu to copy selected text (for getting text from websites that have copy restrictions) 26 | - [ ] each time on activation, capture and store the selection for current page, 27 | selection can then be toggled, sorted, nested; and choose the usage from: 28 | subtitle, quote, list item 29 | - [ ] fork to a new plugin as document quoting editor 30 | -------------------------------------------------------------------------------- /options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Refgen Options 6 | 44 | 45 | 46 |

Settings JSON

47 |
48 | 49 |
50 | 51 |
52 | 53 |
54 |
55 | 56 | 57 |
58 |
Import message
59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /formatters.js: -------------------------------------------------------------------------------- 1 | class MarkdownFormatter { 2 | renderLink(title, url) { 3 | // escape special characters in title: \`*_#+-{}[]() 4 | const escapedTitle = title.replace(/([\\`*_\{\}\[\]])/g, '\\$1'); 5 | 6 | return `[${escapedTitle}](${url})` 7 | } 8 | 9 | parseLink(text) { 10 | // regex parse [title](url) from text 11 | const regex = /\[([^\]]+)\]\(([^\)]+)\)/g; 12 | 13 | let match = regex.exec(text); 14 | // if match, return first and second group 15 | if (match) { 16 | return { 17 | title: match[1], 18 | url: match[2], 19 | }; 20 | } 21 | } 22 | } 23 | 24 | class AsciidocFormatter { 25 | renderLink(title, url) { 26 | return `${url}[${title}]` 27 | } 28 | 29 | parseLink(text) { 30 | // regex parse url[title] from text 31 | const regex = /([^\)]+)\[([^\]]+)\]/g; 32 | 33 | let match = regex.exec(text); 34 | // if match, return first and second group 35 | if (match) { 36 | return { 37 | title: match[2], 38 | url: match[1], 39 | }; 40 | } 41 | } 42 | } 43 | 44 | class RSTFormatter { 45 | renderLink(title, url) { 46 | return `\`${title} <${url}>\`_` 47 | } 48 | 49 | parseLink(text) { 50 | // regex parse `url `_ from text 51 | const regex = /\`([^<]+)\s<([^>]+)>\`_/g; 52 | 53 | let match = regex.exec(text); 54 | // if match, return first and second group 55 | if (match) { 56 | return { 57 | title: match[1], 58 | url: match[2], 59 | }; 60 | } 61 | } 62 | } 63 | 64 | class MediaWikiFormatter { 65 | renderLink(title, url) { 66 | return `[${url} ${title}]` 67 | } 68 | 69 | parseLink(text) { 70 | // regex parse [url title] from text 71 | const regex = /\[([^\]\s]+)\s([^\]]+)\]/g; 72 | 73 | let match = regex.exec(text); 74 | // if match, return first and second group 75 | if (match) { 76 | return { 77 | title: match[2], 78 | url: match[1], 79 | }; 80 | } 81 | } 82 | } 83 | 84 | class HTMLFormatter { 85 | renderLink(title, url) { 86 | return `<a href="${url}">${title}</a>` 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | const QS = document.querySelector.bind(document) 2 | const STORAGE_KEYS = ['settings', 'siteSettingsMap'] 3 | const store = {} 4 | 5 | function loadSettingsJSON() { 6 | return chrome.storage.sync.get(STORAGE_KEYS).then((data) => { 7 | const elJSON = QS('textarea') 8 | store.settingsJSON = JSON.stringify(data, null, 2) 9 | elJSON.value = store.settingsJSON 10 | }) 11 | } 12 | 13 | loadSettingsJSON() 14 | 15 | const exportBth = QS('#f-export') 16 | const importBth = QS('#f-import') 17 | const importMessage = QS('#import-message') 18 | 19 | exportBth.addEventListener('click', () => { 20 | // get datetime in format YYYYMMDD 21 | const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '') 22 | downloadContent(`refgen-settings-export-${dateStr}.json`, store.settingsJSON) 23 | }) 24 | 25 | importBth.addEventListener('click', () => { 26 | // get content from file input: input[name="import-file"] 27 | const elFile = QS('input[name="import-file"]') 28 | if (elFile.files.length === 0) { 29 | importMessage.innerHTML = 'No file chosen' 30 | importMessage.classList.add('error') 31 | return 32 | } 33 | const file = elFile.files[0] 34 | const reader = new FileReader() 35 | reader.onload = (e) => { 36 | let data 37 | try { 38 | data = JSON.parse(e.target.result) 39 | } catch (e) { 40 | importMessage.innerHTML = 'Invalid JSON file: ' + new String(e).toString() 41 | importMessage.classList.add('error') 42 | throw e 43 | } 44 | console.log('import data', data) 45 | for (const key of STORAGE_KEYS) { 46 | if (data[key]) { 47 | chrome.storage.sync.set({ [key]: data[key] }) 48 | } 49 | } 50 | loadSettingsJSON() 51 | importMessage.innerHTML = 'Import success!' 52 | importMessage.classList.add('success') 53 | } 54 | reader.readAsText(file) 55 | }) 56 | 57 | function downloadContent(filename, text) { 58 | var element = document.createElement('a'); 59 | element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(text)); 60 | element.setAttribute('download', filename); 61 | 62 | element.style.display = 'none'; 63 | document.body.appendChild(element); 64 | 65 | element.click(); 66 | 67 | document.body.removeChild(element); 68 | } 69 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title> 6 | 180 | 181 | 182 |
183 |
184 |
Ref:
185 |
186 | 187 |
188 |
189 | 190 |
191 | 192 | 193 |
194 |
195 | 196 | 197 |
198 |
199 | 200 | 201 | 202 |
203 |
204 |
205 |
HTML Preview
206 |
207 |
208 | 209 |
210 | 223 | 224 | 229 | 230 |
231 |
Formatter:
232 |
233 | 240 | 241 |
242 |
243 |
244 |
245 | 246 | 247 | 267 | 268 | 269 | 270 |
271 | 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /popup.js: -------------------------------------------------------------------------------- 1 | const QS = document.querySelector.bind(document) 2 | const QSA = document.querySelectorAll.bind(document) 3 | const STORAGE_KEYS = ['settings', 'siteSettingsMap'] 4 | const S_KEY = { 5 | // global settings 6 | defaultFormat: "defaultFormat", 7 | // site settings 8 | alwaysUseCanonicalUrl: "canonicalUrl", 9 | urlParams: "urlParams", 10 | } 11 | 12 | 13 | /* Class definition */ 14 | 15 | class App { 16 | constructor() { 17 | this.data = { 18 | title: null, 19 | url: null, 20 | canonicalUrl: null, 21 | displayUrl: null, 22 | } 23 | this.domain = null 24 | this.settings = {} 25 | this.siteSettingsMap = {} 26 | 27 | /* Content pane */ 28 | 29 | this.elRef = QS('#d-ref') 30 | this.elRef.addEventListener("keydown", (e) => { 31 | console.log('keydown') 32 | if (e.ctrlKey && e.metaKey && e.key === "c") { 33 | console.log('key combo: ctrl+meta+c') 34 | this.copyRefAsHTML() 35 | this.elCopyHTMLBtn.focus() 36 | return 37 | } else if (e.ctrlKey && e.metaKey && e.key === "u") { 38 | console.log('key combo: ctrl+meta+l') 39 | this.copyURL() 40 | this.elCopyURLBtn.focus() 41 | return 42 | } 43 | }); 44 | this.elRef.addEventListener("keyup", () => { 45 | console.log('keyup') 46 | this.renderPreview() 47 | }); 48 | this.elRef.addEventListener("focus", () => { 49 | this.elRef.select() 50 | }) 51 | const selectRefOnEnter = (e) => { 52 | if (e.key === 'Enter') { 53 | e.preventDefault() 54 | this.elRef.select() 55 | } 56 | } 57 | 58 | this.elTitle = QS('#d-title') 59 | this.elTitle.addEventListener('input', () => { 60 | this.data.title = this.elTitle.value 61 | this.renderRef() 62 | this.renderPreview() 63 | }) 64 | this.elTitle.addEventListener('keydown', selectRefOnEnter) 65 | this.elUrl = QS('#d-url') 66 | this.elUrl.addEventListener('input', () => { 67 | this.data.displayUrl = this.elUrl.value 68 | this.renderRef() 69 | }) 70 | this.elUrl.addEventListener('keydown', selectRefOnEnter) 71 | 72 | /* Action pane */ 73 | 74 | this.elCanonicalUrl = QS('#d-canonical-url') 75 | 76 | this.elCanonicalUrlBtn = QS('#f-canonical-url') 77 | this.elCanonicalUrlBtn.addEventListener('click', () => { 78 | this.data.displayUrl = this.data.canonicalUrl 79 | this.renderRef() 80 | }) 81 | this.elCanonicalUrlCheckbox = QS('#f-canonical-url-always') 82 | this.elCanonicalUrlCheckbox.addEventListener('change', () => { 83 | console.log('elCanonicalUrlCheckbox change') 84 | this.updateSiteSettings(S_KEY.alwaysUseCanonicalUrl, this.elCanonicalUrlCheckbox.checked) 85 | }) 86 | 87 | this.elParamsContent = QS('#section-params .content') 88 | 89 | this.elFormat = QS('#d-format') 90 | this.elFormat.addEventListener('change', () => { 91 | this.renderRef() 92 | }) 93 | 94 | this.elCopyBtn = QS('#f-copy') 95 | this.elCopyBtn.addEventListener('click', () => { 96 | this.copyRef() 97 | }); 98 | 99 | this.elCopyHTMLBtn = QS("#f-copy-html") 100 | this.elCopyHTMLBtn.addEventListener('click', () => { 101 | this.copyRefAsHTML() 102 | }); 103 | 104 | this.elCopyURLBtn = QS("#f-copy-url") 105 | this.elCopyURLBtn.addEventListener('click', () => { 106 | this.copyURL() 107 | }); 108 | 109 | /* Settings pane */ 110 | 111 | // when click #f-settings, toggle #settings-pane display 112 | QS('#f-settings').addEventListener('click', () => { 113 | const el = QS('#settings-pane') 114 | console.log('click #f-settings', el.style.display) 115 | if (el.offsetParent === null) { 116 | el.style.display = 'block' 117 | } else { 118 | el.style.display = 'none' 119 | } 120 | }) 121 | 122 | QSA('input[name="default-format"]').forEach(el => { 123 | el.addEventListener('change', (e) => { 124 | console.log('change input default-format', e) 125 | const defaultFormat = e.target.value 126 | this.elFormat.value = defaultFormat 127 | // dispatch change event on elFormat 128 | this.elFormat.dispatchEvent(new Event('change')) 129 | 130 | // save settings 131 | const d = {} 132 | d[S_KEY.defaultFormat] = defaultFormat 133 | this.saveSettings(d).then(() => { 134 | console.log('saveSettings done') 135 | }) 136 | }) 137 | }) 138 | 139 | /* Preview pane */ 140 | 141 | this.elPreview = QS('#html-preview') 142 | } 143 | 144 | getDefaultFormat() { 145 | return QS('input[name="default-format":checked]').value 146 | } 147 | 148 | getFormat() { 149 | // get the option of