25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper Extension/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | MarkDownload - Markdown Web Clipper Extension
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | $(PRODUCT_NAME)
17 | CFBundlePackageType
18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
19 | CFBundleShortVersionString
20 | $(MARKETING_VERSION)
21 | CFBundleVersion
22 | $(CURRENT_PROJECT_VERSION)
23 | LSMinimumSystemVersion
24 | $(MACOSX_DEPLOYMENT_TARGET)
25 | NSExtension
26 |
27 | NSExtensionPointIdentifier
28 | com.apple.Safari.web-extension
29 | NSExtensionPrincipalClass
30 | $(PRODUCT_MODULE_NAME).SafariWebExtensionHandler
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/PRIVACY.md:
--------------------------------------------------------------------------------
1 | # Privacy Policy
2 |
3 | This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone
4 | using the MarkDownload browser extension (the Service).
5 |
6 | tl;dr: We don't collect anything. Any information we clip from websites remains on your machine.
7 |
8 | ## Information Collection and Use
9 | This extension reads data on a website you are visiting when you click the button. The URL, metadata and content are clipped and fed
10 | through third-party javascript libraries to transofrm it into the Markdown you see in the preview.
11 |
12 | None of this data is collected or sent back to any server. Any files you download or text you copy from the extension remains on your
13 | machine and none of it is collected by me.
14 |
15 | The settings of the extension get stored in the browser's local storage and likewise are never collected or transmitted by me.
16 |
17 | ## Changes to This Privacy Policy
18 | I may update this Privacy Policy from time to time, for example if new features are developed which require data collection.
19 | Thus, it might be a good idea to review this page periodically for any changes. Any changes are effective immediately, as they are
20 | posted on this page. Editing this page constitutes a notification that the Privacy Policy has changed
21 |
22 | ## Contact Me
23 | If you have any questions or suggestions about this Privacy Policy, do not hesitate to contact me.
24 |
--------------------------------------------------------------------------------
/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "favicon-16x16.png",
5 | "idiom" : "mac",
6 | "scale" : "1x",
7 | "size" : "16x16"
8 | },
9 | {
10 | "filename" : "favicon-32x32.png",
11 | "idiom" : "mac",
12 | "scale" : "2x",
13 | "size" : "16x16"
14 | },
15 | {
16 | "filename" : "favicon-32x32.png",
17 | "idiom" : "mac",
18 | "scale" : "1x",
19 | "size" : "32x32"
20 | },
21 | {
22 | "filename" : "icon64x64.png",
23 | "idiom" : "mac",
24 | "scale" : "2x",
25 | "size" : "32x32"
26 | },
27 | {
28 | "filename" : "appicon-128x128.png",
29 | "idiom" : "mac",
30 | "scale" : "1x",
31 | "size" : "128x128"
32 | },
33 | {
34 | "filename" : "icon256x256-1.png",
35 | "idiom" : "mac",
36 | "scale" : "2x",
37 | "size" : "128x128"
38 | },
39 | {
40 | "filename" : "icon256x256.png",
41 | "idiom" : "mac",
42 | "scale" : "1x",
43 | "size" : "256x256"
44 | },
45 | {
46 | "filename" : "favicon-512x512.png",
47 | "idiom" : "mac",
48 | "scale" : "2x",
49 | "size" : "256x256"
50 | },
51 | {
52 | "filename" : "favicon-512x512.png",
53 | "idiom" : "mac",
54 | "scale" : "1x",
55 | "size" : "512x512"
56 | },
57 | {
58 | "filename" : "icon1024x1024.png",
59 | "idiom" : "mac",
60 | "scale" : "2x",
61 | "size" : "512x512"
62 | }
63 | ],
64 | "info" : {
65 | "author" : "xcode",
66 | "version" : 1
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // MarkDownload - Markdown Web Clipper
4 | //
5 | // Created by Gordon Pedersen on 17/2/21.
6 | //
7 |
8 | import Cocoa
9 | import SafariServices.SFSafariApplication
10 | import SafariServices.SFSafariExtensionManager
11 |
12 | let appName = "MarkDownload - Markdown Web Clipper"
13 | let extensionBundleIdentifier = "au.death.MarkDownload---Markdown-Web-Clipper.Extension"
14 |
15 | class ViewController: NSViewController {
16 |
17 | @IBOutlet var appNameLabel: NSTextField!
18 |
19 | override func viewDidLoad() {
20 | super.viewDidLoad()
21 | self.appNameLabel.stringValue = appName
22 | SFSafariExtensionManager.getStateOfSafariExtension(withIdentifier: extensionBundleIdentifier) { (state, error) in
23 | guard let state = state, error == nil else {
24 | // Insert code to inform the user that something went wrong.
25 | return
26 | }
27 |
28 | DispatchQueue.main.async {
29 | if (state.isEnabled) {
30 | self.appNameLabel.stringValue = "\(appName)'s extension is currently on."
31 | } else {
32 | self.appNameLabel.stringValue = "\(appName)'s extension is currently off. You can turn it on in Safari Extensions preferences."
33 | }
34 | }
35 | }
36 | }
37 |
38 | @IBAction func openSafariExtensionPreferences(_ sender: AnyObject?) {
39 | SFSafariApplication.showPreferencesForExtension(withIdentifier: extensionBundleIdentifier) { error in
40 | guard error == nil else {
41 | // Insert code to inform the user that something went wrong.
42 | return
43 | }
44 |
45 | DispatchQueue.main.async {
46 | NSApplication.shared.terminate(nil)
47 | }
48 | }
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/src/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "manifest_version": 2,
3 | "name": "MarkDownload - Markdown Web Clipper",
4 | "version": "3.0.0",
5 | "author": "Gordon Pedsersen",
6 | "description": "This extension works like a web clipper, but it downloads articles in markdown format.",
7 | "icons": {
8 | "16": "icons/favicon-16x16.png",
9 | "32": "icons/favicon-32x32.png",
10 | "48": "icons/favicon-48x48.png",
11 | "128": "icons/appicon-128x128.png",
12 | "192": "icons/favicon-192x192.png",
13 | "512": "icons/favicon-512x512.png"
14 | },
15 | "permissions": [
16 | "",
17 | "activeTab",
18 | "tabs",
19 | "downloads",
20 | "storage",
21 | "contextMenus",
22 | "clipboardWrite"
23 | ],
24 | "browser_action": {
25 | "default_title": "MarkDownload",
26 | "default_popup": "popup/popup.html",
27 | "default_icon": {
28 | "16": "icons/favicon-16x16.png",
29 | "32": "icons/favicon-32x32.png",
30 | "48": "icons/favicon-48x48.png",
31 | "128": "icons/appicon-128x128.png"
32 | }
33 | },
34 | "background": {
35 | "scripts": [
36 | "browser-polyfill.min.js",
37 | "background/apache-mime-types.js",
38 | "background/moment.min.js",
39 | "background/turndown.js",
40 | "/background/Readability.js",
41 | "background/background.js"
42 | ]
43 | },
44 | "options_ui": {
45 | "page": "options/options.html",
46 | "browser_style": false,
47 | "chrome_style": false,
48 | "open_in_tab": false
49 | },
50 | "commands": {
51 | "_execute_browser_action": {
52 | "suggested_key": {
53 | "default": "Alt+Shift+M"
54 | }
55 | },
56 | "download_tab_as_markdown": {
57 | "suggested_key": {
58 | "default": "Ctrl+Shift+M"
59 | },
60 | "description": "Save current tab as Markdown"
61 | }
62 | },
63 | "browser_specific_settings": {
64 | "gecko": {
65 | "id": "{1c5e4c6f-5530-49a3-b216-31ce7d744db0}"
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/src/popup/lib/xq-light.css:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2011 by MarkLogic Corporation
3 | Author: Mike Brevoort
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | */
23 | .cm-s-xq-light span.cm-keyword { line-height: 1em; font-weight: bold; color: #5A5CAD; }
24 | .cm-s-xq-light span.cm-atom { color: #6C8CD5; }
25 | .cm-s-xq-light span.cm-number { color: #164; }
26 | .cm-s-xq-light span.cm-def { text-decoration:underline; }
27 | .cm-s-xq-light span.cm-variable { color: black; }
28 | .cm-s-xq-light span.cm-variable-2 { color:black; }
29 | .cm-s-xq-light span.cm-variable-3, .cm-s-xq-light span.cm-type { color: black; }
30 | .cm-s-xq-light span.cm-property {}
31 | .cm-s-xq-light span.cm-operator {}
32 | .cm-s-xq-light span.cm-comment { color: #0080FF; font-style: italic; }
33 | .cm-s-xq-light span.cm-string { color: red; }
34 | .cm-s-xq-light span.cm-meta { color: yellow; }
35 | .cm-s-xq-light span.cm-qualifier { color: grey; }
36 | .cm-s-xq-light span.cm-builtin { color: #7EA656; }
37 | .cm-s-xq-light span.cm-bracket { color: #cc7; }
38 | .cm-s-xq-light span.cm-tag { color: #3F7F7F; }
39 | .cm-s-xq-light span.cm-attribute { color: #7F007F; }
40 | .cm-s-xq-light span.cm-error { color: #f00; }
41 |
42 | .cm-s-xq-light .CodeMirror-activeline-background { background: #e8f2ff; }
43 | .cm-s-xq-light .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
44 |
--------------------------------------------------------------------------------
/src/popup/lib/xq-dark.css:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (C) 2011 by MarkLogic Corporation
3 | Author: Mike Brevoort
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 | */
23 | .cm-s-xq-dark.CodeMirror { background: #0a001f; color: #f8f8f8; }
24 | .cm-s-xq-dark div.CodeMirror-selected { background: #27007A; }
25 | .cm-s-xq-dark .CodeMirror-line::selection, .cm-s-xq-dark .CodeMirror-line > span::selection, .cm-s-xq-dark .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); }
26 | .cm-s-xq-dark .CodeMirror-line::-moz-selection, .cm-s-xq-dark .CodeMirror-line > span::-moz-selection, .cm-s-xq-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); }
27 | .cm-s-xq-dark .CodeMirror-gutters { background: #0a001f; border-right: 1px solid #aaa; }
28 | .cm-s-xq-dark .CodeMirror-guttermarker { color: #FFBD40; }
29 | .cm-s-xq-dark .CodeMirror-guttermarker-subtle { color: #f8f8f8; }
30 | .cm-s-xq-dark .CodeMirror-linenumber { color: #f8f8f8; }
31 | .cm-s-xq-dark .CodeMirror-cursor { border-left: 1px solid white; }
32 |
33 | .cm-s-xq-dark span.cm-keyword { color: #FFBD40; }
34 | .cm-s-xq-dark span.cm-atom { color: #6C8CD5; }
35 | .cm-s-xq-dark span.cm-number { color: #164; }
36 | .cm-s-xq-dark span.cm-def { color: #FFF; text-decoration:underline; }
37 | .cm-s-xq-dark span.cm-variable { color: #FFF; }
38 | .cm-s-xq-dark span.cm-variable-2 { color: #EEE; }
39 | .cm-s-xq-dark span.cm-variable-3, .cm-s-xq-dark span.cm-type { color: #DDD; }
40 | .cm-s-xq-dark span.cm-property {}
41 | .cm-s-xq-dark span.cm-operator {}
42 | .cm-s-xq-dark span.cm-comment { color: gray; }
43 | .cm-s-xq-dark span.cm-string { color: #9FEE00; }
44 | .cm-s-xq-dark span.cm-meta { color: yellow; }
45 | .cm-s-xq-dark span.cm-qualifier { color: #FFF700; }
46 | .cm-s-xq-dark span.cm-builtin { color: #30a; }
47 | .cm-s-xq-dark span.cm-bracket { color: #cc7; }
48 | .cm-s-xq-dark span.cm-tag { color: #FFBD40; }
49 | .cm-s-xq-dark span.cm-attribute { color: #FFF700; }
50 | .cm-s-xq-dark span.cm-error { color: #f00; }
51 |
52 | .cm-s-xq-dark .CodeMirror-activeline-background { background: #27282E; }
53 | .cm-s-xq-dark .CodeMirror-matchingbracket { outline:1px solid grey; color:white !important; }
54 |
--------------------------------------------------------------------------------
/src/contentScript/contentScript.js:
--------------------------------------------------------------------------------
1 | function notifyExtension() {
2 | // send a message that the content should be clipped
3 | browser.runtime.sendMessage({ type: "clip", dom: content});
4 | }
5 |
6 | function getHTMLOfDocument() {
7 | // if the document doesn't have a "base" element make one
8 | // this allows the DOM parser in future steps to fix relative uris
9 | let baseEl = document.createElement('base');
10 |
11 | // check for a existing base elements
12 | let baseEls = document.head.getElementsByTagName('base');
13 | if (baseEls.length > 0) {
14 | baseEl = baseEls[0];
15 | }
16 | // if we don't find one, append this new one.
17 | else {
18 | document.head.append(baseEl);
19 | }
20 |
21 | // if the base element doesn't have a href, use the current location
22 | if (!baseEl.getAttribute('href')) {
23 | baseEl.setAttribute('href', window.location.href);
24 | }
25 |
26 | // get the content of the page as a string
27 | return document.documentElement.outerHTML;
28 | }
29 |
30 | // code taken from here: https://stackoverflow.com/a/5084044/304786
31 | function getHTMLOfSelection() {
32 | var range;
33 | if (document.selection && document.selection.createRange) {
34 | range = document.selection.createRange();
35 | return range.htmlText;
36 | } else if (window.getSelection) {
37 | var selection = window.getSelection();
38 | if (selection.rangeCount > 0) {
39 | let content = '';
40 | for (let i = 0; i < selection.rangeCount; i++) {
41 | range = selection.getRangeAt(0);
42 | var clonedSelection = range.cloneContents();
43 | var div = document.createElement('div');
44 | div.appendChild(clonedSelection);
45 | content += div.innerHTML;
46 | }
47 | return content;
48 | } else {
49 | return '';
50 | }
51 | } else {
52 | return '';
53 | }
54 | }
55 |
56 | function getSelectionAndDom() {
57 | return {
58 | selection: getHTMLOfSelection(),
59 | dom: getHTMLOfDocument()
60 | }
61 | }
62 |
63 | // This function must be called in a visible page, such as a browserAction popup
64 | // or a content script. Calling it in a background page has no effect!
65 | function copyToClipboard(text) {
66 | navigator.clipboard.writeText(text);
67 | }
68 |
69 | function downloadMarkdown(filename, text) {
70 | let datauri = `data:text/markdown;base64,${text}`;
71 | var link = document.createElement('a');
72 | link.download = filename;
73 | link.href = datauri;
74 | link.click();
75 | }
76 |
77 | function downloadImage(filename, url) {
78 |
79 | /* Link with a download attribute? CORS says no.
80 | var link = document.createElement('a');
81 | link.download = filename.substring(0, filename.lastIndexOf('.'));
82 | link.href = url;
83 | console.log(link);
84 | link.click();
85 | */
86 |
87 | /* Try via xhr? Blocked by CORS.
88 | var xhr = new XMLHttpRequest();
89 | xhr.open('GET', url, true);
90 | xhr.responseType = 'blob';
91 | xhr.onload = () => {
92 | console.log('onload!')
93 | var file = new Blob([xhr.response], {type: 'application/octet-stream'});
94 | var link = document.createElement('a');
95 | link.download = filename;//.substring(0, filename.lastIndexOf('.'));
96 | link.href = window.URL.createObjectURL(file);
97 | console.log(link);
98 | link.click();
99 | }
100 | xhr.send();
101 | */
102 |
103 | /* draw on canvas? Inscure operation
104 | let img = new Image();
105 | img.src = url;
106 | img.onload = () => {
107 | let canvas = document.createElement("canvas");
108 | let ctx = canvas.getContext("2d");
109 | canvas.width = img.width;
110 | canvas.height = img.height;
111 | ctx.drawImage(img, 0, 0);
112 |
113 | var link = document.createElement('a');
114 | const ext = filename.substring(filename.lastIndexOf('.'));
115 | link.download = filename;
116 | link.href = canvas.toDataURL(`image/png`);
117 | console.log(link);
118 | link.click();
119 | }
120 | */
121 | }
--------------------------------------------------------------------------------
/src/popup/popup.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --focus: orange;
3 | --bg: #fff;/*#0A001F;*/
4 | --fg: #000;
5 | --border: #eee;
6 | --primary: #4A90E2;
7 | --fg-primary: #eee;
8 | --bg-primary: #ECF5FF;
9 | --border-primary: #4A90E2;
10 | --font-size: 14px;
11 | }
12 |
13 | @media(prefers-color-scheme: dark) {
14 | :root {
15 | --focus: orange;
16 | --bg: #2A2A2E;
17 | --fg: #f8f8f8;
18 | --border: #555;
19 | --primary: #4A90E2;
20 | --fg-primary: #eee;
21 | --bg-primary: hsl(212, 80%, 30%);
22 | --border-primary: #4A90E2;
23 | --pre-bg: #474749;
24 | }
25 | .cm-s-xq-dark.CodeMirror {
26 | background-color: var(--bg);
27 | }
28 | }
29 |
30 | /* Works on Firefox */
31 | * {
32 | scrollbar-width: thin;
33 | scrollbar-color: var(--pre-bg) var(--bg);
34 | }
35 |
36 | /* Works on Chrome, Edge, and Safari */
37 | *::-webkit-scrollbar {
38 | width: 12px;
39 | }
40 |
41 | *::-webkit-scrollbar-track {
42 | background: var(--bg);
43 | }
44 |
45 | *::-webkit-scrollbar-thumb {
46 | background-color: var(--border);
47 | border-radius: 20px;
48 | }
49 |
50 | * {
51 | margin: 0;
52 | padding: 0;
53 | box-sizing: border-box;
54 | }
55 |
56 | body {
57 | width: 350px;
58 | height: 500px;
59 | border-radius: 0.5em;
60 | overflow: hidden;
61 | font-family: sans-serif;
62 | background: var(--bg);
63 | font-size: var(--font-size);
64 | }
65 |
66 | @keyframes spinner {
67 | to {
68 | transform: rotate(360deg);
69 | }
70 | }
71 | * {
72 | box-sizing: border-box;
73 | }
74 |
75 | *:focus {
76 | border: 3px dotted var(--focus-color) !important;
77 | }
78 |
79 | #spinner:before {
80 | content: '';
81 | position: absolute;
82 | top: 50%;
83 | left: 50%;
84 | width: 20px;
85 | height: 20px;
86 | margin-top: -10px;
87 | margin-left: -10px;
88 | border-radius: 50%;
89 | border-top: 2px solid var(--primary);
90 | border-right: 2px solid transparent;
91 | animation: spinner .6s linear infinite;
92 | }
93 |
94 | .CodeMirror {
95 | border: none;
96 | padding: 0.5em;
97 | flex: 1;
98 | }
99 |
100 | input#title {
101 | padding: 0.5em;
102 | color: var(--fg);
103 | background-color: var(--bg);
104 | border: 1px solid var(--border);
105 | font-size: 1.01em;
106 | }
107 |
108 | #container {
109 | width: 100%;
110 | height: 100%;
111 | display: none;
112 | flex-direction: column;
113 | }
114 | .row {
115 | flex-direction: row;
116 | display:flex;
117 | }
118 |
119 | a.button {
120 | display: block;
121 | padding: 0.5em 0;
122 | background-color: var(--primary);
123 | color: var(--fg-primary);
124 | text-align: center;
125 | text-decoration: none;
126 | line-height: 1em;
127 | border: 1px solid var(--bg);
128 | border-collapse: collapse;
129 | }
130 |
131 | #downloadSelection {
132 | background-color: var(--bg-primary);
133 | display: none;
134 | }
135 |
136 | #selected, #document {
137 | flex:1;
138 | }
139 |
140 | #clipOption {
141 | display:none;
142 | }
143 |
144 |
145 |
146 | #selected,
147 | #document,
148 | #includeTemplate {
149 | background: var(--bg);
150 | border: 1px solid var(--border);
151 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
152 | position: relative;
153 | color:var(--fg);
154 | cursor: pointer;
155 | padding: 10px 10px 10px 30px;
156 | display: inline-block;
157 | font-weight: 600;
158 | transition: .3s ease all;
159 | }
160 |
161 | #selected:hover,
162 | #document:hover,
163 | #includeTemplate:hover {
164 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
165 | }
166 |
167 | #selected.checked,
168 | #document.checked,
169 | #includeTemplate.checked {
170 | background: var(--bg-primary);
171 | border-color: var(--border-primary);
172 | }
173 |
174 | .check {
175 | background: var(--bg-primary);
176 | border-radius: 50%;
177 | content:'';
178 | height: 20px;
179 | left: 10px;
180 | position: absolute;
181 | top: calc(50% - 10px);
182 | transition: .3s ease background-color;
183 | width: 20px;
184 | }
185 |
186 | .checked .check {
187 | background-color: var(--primary);
188 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNiIgaGVpZ2h0PSIyMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIyLjAyOTY4IC00MC4wOTAzIDI2IDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48IS0tR2VuZXJhdGVkIGJ5IElKU1ZHIChodHRwczovL2dpdGh1Yi5jb20vaWNvbmphci9JSlNWRyktLT48cGF0aCBkPSJNMjcuOTc0MywtMzYuMTI3MmMwLDAuNDQ2NDI4IC0wLjE1NjI1LDAuODI1ODkzIC0wLjQ2ODc1LDEuMTM4MzlsLTEyLjEyMDUsMTIuMTIwNWwtMi4yNzY3OSwyLjI3Njc5Yy0wLjMxMjUsMC4zMTI1IC0wLjY5MTk2NCwwLjQ2ODc1IC0xLjEzODM5LDAuNDY4NzVjLTAuNDQ2NDI4LDAgLTAuODI1ODkzLC0wLjE1NjI1IC0xLjEzODM5LC0wLjQ2ODc1bC0yLjI3Njc5LC0yLjI3Njc5bC02LjA2MDI3LC02LjA2MDI3Yy0wLjMxMjUsLTAuMzEyNSAtMC40Njg3NSwtMC42OTE5NjUgLTAuNDY4NzUsLTEuMTM4MzljMCwtMC40NDY0MjkgMC4xNTYyNSwtMC44MjU4OTMgMC40Njg3NSwtMS4xMzgzOWwyLjI3Njc5LC0yLjI3Njc5YzAuMzEyNSwtMC4zMTI1IDAuNjkxOTY1LC0wLjQ2ODc1IDEuMTM4MzksLTAuNDY4NzVjMC40NDY0MjksMCAwLjgyNTg5MywwLjE1NjI1IDEuMTM4MzksMC40Njg3NWw0LjkyMTg4LDQuOTM4NjJsMTAuOTgyMSwtMTAuOTk4OWMwLjMxMjUsLTAuMzEyNSAwLjY5MTk2NCwtMC40Njg3NSAxLjEzODM5LC0wLjQ2ODc1YzAuNDQ2NDI4LDAgMC44MjU4OTMsMC4xNTYyNSAxLjEzODM5LDAuNDY4NzVsMi4yNzY3OCwyLjI3Njc5YzAuMzEyNSwwLjMxMjUgMC40Njg3NSwwLjY5MTk2NCAwLjQ2ODc1LDEuMTM4MzlaIiB0cmFuc2Zvcm09InNjYWxlKDEuMDAxOTgpIiBmaWxsPSIjZmZmIj48L3BhdGg+PC9zdmc+');
189 | background-repeat: no-repeat;
190 | background-position: center;
191 | background-size: 12px;
192 | }
193 |
--------------------------------------------------------------------------------
/src/popup/popup.js:
--------------------------------------------------------------------------------
1 |
2 | // default variables
3 | var selectedText = null;
4 | var imageList = null;
5 | var mdClipsFolder = '';
6 |
7 | const darkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
8 | // set up event handlers
9 | const cm = CodeMirror.fromTextArea(document.getElementById("md"), {
10 | theme: darkMode ? "xq-dark" : "xq-light",
11 | mode: "markdown",
12 | lineWrapping: true
13 | });
14 | cm.on("cursorActivity", (cm) => {
15 | const somethingSelected = cm.somethingSelected();
16 | var a = document.getElementById("downloadSelection");
17 |
18 | if (somethingSelected) {
19 | if(a.style.display != "block") a.style.display = "block";
20 | }
21 | else {
22 | if(a.style.display != "none") a.style.display = "none";
23 | }
24 | });
25 | document.getElementById("download").addEventListener("click", download);
26 | document.getElementById("downloadSelection").addEventListener("click", downloadSelection);
27 |
28 | const defaultOptions = {
29 | includeTemplate: false,
30 | clipSelection: true
31 | }
32 |
33 | const checkInitialSettings = options => {
34 | if (options.includeTemplate)
35 | document.querySelector("#includeTemplate").classList.add("checked");
36 |
37 | if (options.clipSelection)
38 | document.querySelector("#selected").classList.add("checked");
39 | else
40 | document.querySelector("#document").classList.add("checked");
41 | }
42 |
43 | const toggleClipSelection = options => {
44 | options.clipSelection = !options.clipSelection;
45 | document.querySelector("#selected").classList.toggle("checked");
46 | document.querySelector("#document").classList.toggle("checked");
47 | browser.storage.sync.set(options).then(() => clipSite()).catch((error) => {
48 | console.error(error);
49 | });
50 | }
51 |
52 | const toggleIncludeTemplate = options => {
53 | options.includeTemplate = !options.includeTemplate;
54 | document.querySelector("#includeTemplate").classList.toggle("checked");
55 | browser.storage.sync.set(options).then(() => {
56 | browser.contextMenus.update("toggle-includeTemplate", {
57 | checked: options.includeTemplate
58 | });
59 | try {
60 | browser.contextMenus.update("tabtoggle-includeTemplate", {
61 | checked: options.includeTemplate
62 | });
63 | } catch { }
64 | return clipSite()
65 | }).catch((error) => {
66 | console.error(error);
67 | });
68 | }
69 |
70 | const showOrHideClipOption = selection => {
71 | if (selection) {
72 | document.getElementById("clipOption").style.display = "flex";
73 | }
74 | else {
75 | document.getElementById("clipOption").style.display = "none";
76 | }
77 | }
78 |
79 | const clipSite = id => {
80 | return browser.tabs.executeScript(id, { code: "getSelectionAndDom()" })
81 | .then((result) => {
82 | if (result && result[0]) {
83 | showOrHideClipOption(result[0].selection);
84 | let message = {
85 | type: "clip",
86 | dom: result[0].dom,
87 | selection: result[0].selection
88 | }
89 | return browser.storage.sync.get(defaultOptions).then(options => {
90 | browser.runtime.sendMessage({
91 | ...message,
92 | ...options
93 | });
94 | }).catch(err => {
95 | console.error(err);
96 | browser.runtime.sendMessage({
97 | ...message,
98 | ...defaultOptions
99 | });
100 | });
101 | }
102 | });
103 | }
104 |
105 | // inject the necessary scripts
106 | browser.storage.sync.get(defaultOptions).then(options => {
107 | checkInitialSettings(options);
108 |
109 | document.getElementById("selected").addEventListener("click", (e) => {
110 | e.preventDefault();
111 | toggleClipSelection(options);
112 | });
113 | document.getElementById("document").addEventListener("click", (e) => {
114 | e.preventDefault();
115 | toggleClipSelection(options);
116 | });
117 | document.getElementById("includeTemplate").addEventListener("click", (e) => {
118 | e.preventDefault();
119 | toggleIncludeTemplate(options);
120 | });
121 |
122 | return browser.tabs.query({
123 | currentWindow: true,
124 | active: true
125 | });
126 | }).then((tabs) => {
127 | var id = tabs[0].id;
128 | var url = tabs[0].url;
129 | browser.tabs.executeScript(id, {
130 | file: "/browser-polyfill.min.js"
131 | })
132 | .then(() => {
133 | return browser.tabs.executeScript(id, {
134 | file: "/contentScript/contentScript.js"
135 | });
136 | }).then( () => {
137 | console.info("Successfully injected MarkDownload content script");
138 | return clipSite(id);
139 | }).catch( (error) => {
140 | console.error(error);
141 | });
142 | });
143 |
144 | // listen for notifications from the background page
145 | browser.runtime.onMessage.addListener(notify);
146 |
147 | //function to send the download message to the background page
148 | function sendDownloadMessage(text) {
149 | if (text != null) {
150 |
151 | return browser.tabs.query({
152 | currentWindow: true,
153 | active: true
154 | }).then(tabs => {
155 | var message = {
156 | type: "download",
157 | markdown: text,
158 | title: document.getElementById("title").value,
159 | tab: tabs[0],
160 | imageList: imageList,
161 | mdClipsFolder: mdClipsFolder
162 | };
163 | return browser.runtime.sendMessage(message);
164 | });
165 | }
166 | }
167 |
168 | // event handler for download button
169 | async function download(e) {
170 | e.preventDefault();
171 | await sendDownloadMessage(cm.getValue());
172 | window.close();
173 | }
174 |
175 | // event handler for download selected button
176 | async function downloadSelection(e) {
177 | e.preventDefault();
178 | if (cm.somethingSelected()) {
179 | await sendDownloadMessage(cm.getSelection());
180 | }
181 | }
182 |
183 | //function that handles messages from the injected script into the site
184 | function notify(message) {
185 | // message for displaying markdown
186 | if (message.type == "display.md") {
187 |
188 | // set the values from the message
189 | //document.getElementById("md").value = message.markdown;
190 | cm.setValue(message.markdown);
191 | document.getElementById("title").value = message.article.title;
192 | imageList = message.imageList;
193 | mdClipsFolder = message.mdClipsFolder;
194 |
195 | // show the hidden elements
196 | document.getElementById("container").style.display = 'flex';
197 | document.getElementById("spinner").style.display = 'none';
198 | // focus the download button
199 | document.getElementById("download").focus();
200 | cm.refresh();
201 | }
202 | }
203 |
204 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MarkDownload - Markdown Web Clipper
2 |
3 | [](https://github.com/deathau/markdownload/releases/latest)
4 |
5 | This is an extension to clip websites and download them into a readable markdown file. Please keep in mind that it is not guaranteed to work on all websites.
6 |
7 | To use this add-on, simply click the add-on icon while you are browsing the page you want to save offline. A popup will show the rendered markdown so you can make minor edits or copy the text, or you can click the download button to download an .md file.
8 | Selecting text will allow you to download just the selected text
9 |
10 | See the [Markdownload User Guide](https://death.id.au/30-39+Personal+Dev/33+Browser+Extensions/33.01+MarkDownload/Markdownload+User+Guide) for more details on the functionality of this extension
11 |
12 | # Installation
13 | The extension is available for [Firefox](https://addons.mozilla.org/en-GB/firefox/addon/markdownload/), [Google Chrome](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi), [Microsoft Edge](https://microsoftedge.microsoft.com/addons/detail/hajanaajapkhaabfcofdjgjnlgkdkknm) and [Safari](https://apple.co/3tcU0pD).
14 |
15 | [](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi) [](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi) [](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi)
16 |
17 | [](https://addons.mozilla.org/en-US/firefox/addon/markdownload/) [](https://addons.mozilla.org/en-US/firefox/addon/markdownload/) [](https://addons.mozilla.org/en-US/firefox/addon/markdownload/)
18 |
19 | [](https://microsoftedge.microsoft.com/addons/detail/hajanaajapkhaabfcofdjgjnlgkdkknm) [](https://microsoftedge.microsoft.com/addons/detail/hajanaajapkhaabfcofdjgjnlgkdkknm)
20 |
21 | [](https://apple.co/3tcU0pD)
22 |
23 | # External Libraries
24 | It uses the following libraries:
25 | - [Readability.js](https://github.com/mozilla/readability) by Mozilla in version from commit [52ab9b5c8916c306a47b2119270dcdabebf9d203](https://github.com/mozilla/readability/commit/52ab9b5c8916c306a47b2119270dcdabebf9d203#diff-06d8d22df421dacde90a2268083424ab). This library is also used for the Firefox Reader View and it simplifies the page so that only the important parts are clipped. (Licensed under Apache License Version 2.0)
26 | - [Turndown](https://github.com/domchristie/turndown) by Dom Christie in version 7.0.1 is used to convert the simplified HTML (from Readability.js) into markdown. (Licensed under MIT License)
27 | - [Moment.js](https://momentjs.com) version 2.27.0 used to format dates in template variables
28 |
29 | # Permissions
30 | - Data on all sites: used to enable "Download All Tabs" functionality - no other data is captured or sent online
31 | - Access tabs: used to access the website content when the icon in the browser bar is clicked.
32 | - Manage Downloads: necessary to be able to download the markdown file.
33 | - Storage: used to save extension options
34 | - Clipboard: used to copy Markdown to clipboard
35 |
36 | ---
37 | The Common Mark icon courtesy of https://github.com/dcurtis/markdown-mark
38 |
39 | ## Pricing
40 | This is an open-source extension I made *for fun*. It's intention is to be completely free.
41 | It's free on Firefox, Edge and Chrome (and other Chromium browsers),
42 | but unfortunately for Safari there is a yearly developer fee, so I've decided to
43 | charge a small price for the Safari version to help cover that cost.
44 | Alternately, you can become a GitHub Sponsor for as little as $2 per month and
45 | you can request a key for the Safari version.
46 | Also, even if you're using the free version and you absolutely *have* to
47 | send me money because you like it that much, feel free to throw some coins
48 | in my hat via the following:
49 |
50 | [](https://github.com/sponsors/deathau)
51 | [](https://paypal.me/deathau)
52 |
53 | # Version History
54 | ## 3.0.0
55 | - Theme revamp
56 | - Utilizing CodeMirror for the Markdown Editor
57 | - Strip Disallowed characters on title and image filenames during text replacement
58 | - Add "Download Type" option, to attempt to resolve conflicts with other Download extensions (and to help support Safari!)
59 | - Add options for stripping images and links
60 | - Fixes around downloading images and getting correct urls in the markdown
61 | - Added meta keywords support for the text replace
62 | - Added text replace support for meta tags in general
63 | - Add option to disable turndown escaping
64 | - Strip out 'red dot' special characters
65 | - Added an option to specify a download path (within the downloads folder). Thanks to Nikita Lukianets!
66 |
67 | ## 2.4.1
68 | - Add option for Obsidian-style image links (when downloading images with the markdown file)
69 | - Downloaded images should download relative to the markdown file in the case where you specify a subfolder in your title template
70 | - Front- and back-matter template will no longer put in extra lines on Opera
71 | - Adjusted the way text is copied to the clipboard
72 |
73 | ## 2.4.0
74 | - Fixed typo on options page (thanks Dean Cook)
75 | - Added option to download images alongside the markdown file
76 | - Also added the ability to add a prefix to the images you download, so you can, for example, save them in a subfolder
77 | - If your browser has the option to always show a save as dialog enabled, you might get a dialog for every image. Sorry about that 😬
78 | - Updated turndown to 7.0.1 and allowed iframes to be kept in the markdown
79 | - Added a new `{pageTitle}` option for template replacement (there are many websites where the `{title}` and `{pageTitle}` actually differ)
80 | - Added a context menu option to copy a tab URL as a markdown link, using the title configured in settings as the link title (i.e. `[]()`)
81 | - Added custom disallowed characters to strip from titles (set to `[]#^` by default for maximum compatibility with Obsidian)
82 | - Added some focus styling so you can tell what is focused
83 | - Auto-focus the download button (you can now `ctrl`+`shift`+`M`, Enter to quickly download a file)
84 | - Template title (and image prefixes) now allow forward slashes (`/`) so that files get saved to a subfolder
85 |
86 | ## 2.3.1
87 | - Added template toggle to Firefox's tab context menu
88 |
89 | ## 2.3.0
90 | - Added contexy menus for copying markdown
91 | - Added options to clip selected text
92 | - Include front-matter/back-matter templates in popup
93 | - Add title templating
94 | - Added keyboard shortcut to show the popup
95 | - Added option to always show Save As
96 | - Added context menus to download all tabs as markdown
97 |
98 | ## 2.2.0
99 | - Added extension options
100 | - Turndown (markdown generation) options
101 | - Front-matter/back-matter templates with replacement variables from page metadata (and date)
102 |
103 | ## 2.1.6
104 | - Replace non-breaking spaces in filenames
105 |
106 | ## 2.1.5
107 | - Fixed an issue with sites with invalid `` tags
108 |
109 | ## 2.1.4
110 | - Fixed issue with relative links [#1](https://github.com/deathau/markdownload/issues/1)
111 |
112 | ## 2.1.3
113 | - Fist change, forked from [enrico-kaack/markdown-clipper](https://github.com/enrico-kaack/markdown-clipper)
114 | - Added URL to markdown output ([#5](https://github.com/deathau/markdownload/issues/5))
115 |
--------------------------------------------------------------------------------
/src/options/options.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --focus: orange;
3 | --bg: #fff;
4 | /*#0A001F;*/
5 | --fg: #000;
6 | --border: #eee;
7 | --primary: #4A90E2;
8 | --fg-primary: #eee;
9 | --bg-primary: #ECF5FF;
10 | --border-primary: #4A90E2;
11 | --pre-bg: #eee;
12 | }
13 |
14 | @media(prefers-color-scheme: dark) {
15 | :root {
16 | --focus: orange;
17 | --bg: #202023;
18 | --fg: #f8f8f8;
19 | --border: #555;
20 | --primary: #4A90E2;
21 | --fg-primary: #eee;
22 | --bg-primary: hsl(212, 80%, 30%);
23 | --border-primary: #4A90E2;
24 | --pre-bg: #474749;
25 | }
26 | }
27 |
28 | /* Works on Firefox */
29 | * {
30 | scrollbar-width: thin;
31 | scrollbar-color: var(--pre-bg) var(--bg);
32 | }
33 |
34 | /* Works on Chrome, Edge, and Safari */
35 | *::-webkit-scrollbar {
36 | width: 12px;
37 | }
38 |
39 | *::-webkit-scrollbar-track {
40 | background: var(--bg);
41 | }
42 |
43 | *::-webkit-scrollbar-thumb {
44 | background-color: var(--border);
45 | border-radius: 20px;
46 | }
47 |
48 | body {
49 | margin: auto;
50 | padding: 8px;
51 | font-size: 16px;
52 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, "Apple Color Emoji", Arial, sans-serif, "Segoe UI Emoji", "Segoe UI Symbol";
53 | background-color: var(--bg);
54 | color: var(--fg)
55 | }
56 |
57 | a {
58 | color:var(--primary);
59 | }
60 |
61 | pre,
62 | code {
63 | border-radius: 5px;
64 | background: var(--pre-bg);
65 | color: var(--fg);
66 | padding: 0.5em;
67 | border: 1px solid var(--border);
68 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
69 | font-weight: 400;
70 | }
71 |
72 | code {
73 | padding-top: 0.2em;
74 | padding-bottom: 0.2em;
75 | display: inline-block;
76 | }
77 |
78 | input[type="text"] {
79 | width: calc(100% - 56px);
80 | display: block;
81 | border: 1px solid var(--border);
82 | font-family: inherit;
83 | font-size: inherit;
84 | padding: 2px 6px;
85 | font-family: monospace;
86 | }
87 |
88 | .save {
89 | display:block;
90 | width:100%;
91 | padding: 1em 0;
92 | margin: 0;
93 | background-color: var(--primary);
94 | color: var(--fg-primary);
95 | text-align: center;
96 | text-decoration: none;
97 | line-height: 1em;
98 | border:none;
99 | }
100 |
101 | .status {
102 | display:block;
103 | width: 100%;
104 | padding: 1em 0;
105 | margin: 0.5em 0;
106 | text-align: center;
107 | text-decoration: none;
108 | line-height: 1em;
109 | background-color: transparent;
110 | }
111 | .status.success{
112 | background-color: #d4edda;
113 | color:#155724;
114 | }
115 | .status.error{
116 | background-color: #f8d7da;
117 | color:#721c24;
118 | }
119 | .input-sizer {
120 | position: relative;
121 | width: calc(100% - 56px);
122 | display: block;
123 | resize: both;
124 | min-height: 40px;
125 | font-family: monospace;
126 | font-size: inherit;
127 | padding: 0;
128 | }
129 | .input-sizer textarea {
130 | position: absolute;
131 | top:0;
132 | left:0;
133 | width:100%;
134 | height:100%;
135 | font-family: inherit;
136 | font-size: inherit;
137 | padding: 1px 6px;
138 | margin:0;
139 | border: 1px solid var(--border);
140 | }
141 | .input-sizer::after {
142 | content: attr(data-value) ' ';
143 | visibility: hidden;
144 | white-space: pre;
145 | font-family: inherit;
146 | font-size: inherit;
147 | }
148 |
149 | .radio-container>label,
150 | .checkbox-container>label,
151 | .textbox-container,
152 | .instructions {
153 | background: var(--bg);
154 | border: 1px solid var(--border);
155 | border-radius: 5px;
156 | box-shadow: 0 2px 4px rgba(0,0,0,0.05);
157 | position: relative;
158 | }
159 |
160 | .instructions {
161 | padding:20px;
162 | display:block;
163 | }
164 |
165 | .radio-container>label,
166 | .checkbox-container>label {
167 | cursor: pointer;
168 | padding: 20px 20px 20px 65px;
169 | max-width: calc(100% - 100px);
170 | }
171 |
172 | label p{
173 | word-break: normal;
174 | white-space: normal;
175 | font-weight: normal;
176 | }
177 |
178 | .textbox-container {
179 | box-sizing: border-box;
180 | border-color: var(--border-primary);
181 | overflow:hidden;
182 | transition:all 0.3s ease-out;
183 | }
184 |
185 | .textbox-container > label {
186 | margin: 8px 20px;
187 | }
188 |
189 | .textbox-container > input[type="text"],
190 | .textbox-container > textarea,
191 | .textbox-container > .input-sizer {
192 | margin:0 20px 20px 20px;
193 | }
194 |
195 | .radio-container>label,
196 | .checkbox-container>label,
197 | .textbox-container>label {
198 | display: inline-block;
199 | font-weight: 600;
200 | transition: .3s ease all;
201 | flex: 1 0 auto;
202 | }
203 | .textbox-container
204 | .radio-container>label:hover,
205 | .checkbox-container>label:hover {
206 | box-shadow: 0 4px 8px rgba(0,0,0,0.05);
207 | }
208 | .radio-container>label::before,
209 | .checkbox-container>label::before {
210 | background: var(--bg-primary);
211 | border-radius: 50%;
212 | content:'';
213 | height: 30px;
214 | left: 20px;
215 | position: absolute;
216 | top: calc(50% - 15px);
217 | transition: .3s ease background-color;
218 | width: 30px;
219 | }
220 | .radio-container>input[type="radio"],
221 | .checkbox-container>input[type="checkbox"] {
222 | position: absolute;
223 | visibility: hidden;
224 | }
225 | .radio-container>input[type="radio"]:checked+label,
226 | .checkbox-container>input[type="checkbox"]:checked+label {
227 | background: var(--bg-primary);
228 | border-color: var(--primary);
229 | }
230 | .radio-container>input[type="radio"]:checked+label::before,
231 | .checkbox-container>input[type="checkbox"]:checked+label::before {
232 | background-color: var(--primary);
233 | background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHdpZHRoPSIyNiIgaGVpZ2h0PSIyMCIgdmVyc2lvbj0iMS4xIiB2aWV3Qm94PSIyLjAyOTY4IC00MC4wOTAzIDI2IDIwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48IS0tR2VuZXJhdGVkIGJ5IElKU1ZHIChodHRwczovL2dpdGh1Yi5jb20vaWNvbmphci9JSlNWRyktLT48cGF0aCBkPSJNMjcuOTc0MywtMzYuMTI3MmMwLDAuNDQ2NDI4IC0wLjE1NjI1LDAuODI1ODkzIC0wLjQ2ODc1LDEuMTM4MzlsLTEyLjEyMDUsMTIuMTIwNWwtMi4yNzY3OSwyLjI3Njc5Yy0wLjMxMjUsMC4zMTI1IC0wLjY5MTk2NCwwLjQ2ODc1IC0xLjEzODM5LDAuNDY4NzVjLTAuNDQ2NDI4LDAgLTAuODI1ODkzLC0wLjE1NjI1IC0xLjEzODM5LC0wLjQ2ODc1bC0yLjI3Njc5LC0yLjI3Njc5bC02LjA2MDI3LC02LjA2MDI3Yy0wLjMxMjUsLTAuMzEyNSAtMC40Njg3NSwtMC42OTE5NjUgLTAuNDY4NzUsLTEuMTM4MzljMCwtMC40NDY0MjkgMC4xNTYyNSwtMC44MjU4OTMgMC40Njg3NSwtMS4xMzgzOWwyLjI3Njc5LC0yLjI3Njc5YzAuMzEyNSwtMC4zMTI1IDAuNjkxOTY1LC0wLjQ2ODc1IDEuMTM4MzksLTAuNDY4NzVjMC40NDY0MjksMCAwLjgyNTg5MywwLjE1NjI1IDEuMTM4MzksMC40Njg3NWw0LjkyMTg4LDQuOTM4NjJsMTAuOTgyMSwtMTAuOTk4OWMwLjMxMjUsLTAuMzEyNSAwLjY5MTk2NCwtMC40Njg3NSAxLjEzODM5LC0wLjQ2ODc1YzAuNDQ2NDI4LDAgMC44MjU4OTMsMC4xNTYyNSAxLjEzODM5LDAuNDY4NzVsMi4yNzY3OCwyLjI3Njc5YzAuMzEyNSwwLjMxMjUgMC40Njg3NSwwLjY5MTk2NCAwLjQ2ODc1LDEuMTM4MzlaIiB0cmFuc2Zvcm09InNjYWxlKDEuMDAxOTgpIiBmaWxsPSIjZmZmIj48L3BhdGg+PC9zdmc+');
234 | background-repeat: no-repeat;
235 | background-position: center;
236 | background-size: 15px;
237 | }
238 |
239 | .radio-container>input[type="radio"]:disabled+label,
240 | .checkbox-container>input[type="checkbox"]:disabled+label {
241 | background-color: var(--bg);
242 | border-color: var(--pre-bg);
243 | cursor: not-allowed;
244 | opacity: 0.7;
245 | }
246 |
247 | .radio-container>input[type="radio"]:disabled+label::before,
248 | .checkbox-container>input[type="checkbox"]:disabled+label::before {
249 | background-color: var(--pre-bg);
250 | }
251 |
252 | .radio-container, .checkbox-container {
253 | display:flex;
254 | flex-direction: row;
255 | flex-wrap: wrap;
256 | align-items: stretch;
257 |
258 | --gap: 12px;
259 | margin: calc(-1 * var(--gap)) 0 calc(var(--gap) * 2) calc(-1 * var(--gap));
260 | width: calc(100% + var(--gap));
261 |
262 | overflow:hidden;
263 | transition:all 0.3s ease-out;
264 | }
265 |
266 | .checkbox-container{
267 | margin-top:0;
268 | margin-bottom: 0;
269 | }
270 |
271 | .radio-container > *,
272 | .checkbox-container > * {
273 | margin: var(--gap) 0 0 var(--gap);
274 | -webkit-user-select: none;
275 | -moz-user-select: none;
276 | user-select: none;
277 | }
278 |
279 | .radio-container > h3,
280 | .checkbox-container > h3,
281 | .radio-container>p,
282 | .checkbox-container>p {
283 | width: 100%;
284 | }
285 |
286 | @keyframes spinner {
287 | to {
288 | transform: rotate(360deg);
289 | }
290 | }
291 |
292 | #spinner:before {
293 | content: '';
294 | position: fixed;
295 | top: 20px;
296 | right: 20px;
297 | width: 20px;
298 | height: 20px;
299 | margin-top: -10px;
300 | margin-left: -10px;
301 | border-radius: 50%;
302 | border-top: 2px solid var(--primary);
303 | border-right: 2px solid transparent;
304 | animation: spinner .6s linear infinite;
305 | }
306 |
307 | input[type="text"], textarea {
308 | background-color: var(--bg);
309 | color: var(--fg);
310 | }
311 |
312 | #downloadMode p{
313 | font-size: 0.85em;
314 | }
--------------------------------------------------------------------------------
/src/popup/lib/codemirror.css:
--------------------------------------------------------------------------------
1 | /* BASICS */
2 |
3 | .CodeMirror {
4 | /* Set height, width, borders, and global font properties here */
5 | font-family: monospace;
6 | height: 300px;
7 | color: black;
8 | direction: ltr;
9 | }
10 |
11 | /* PADDING */
12 |
13 | .CodeMirror-lines {
14 | padding: 4px 0; /* Vertical padding around content */
15 | }
16 | .CodeMirror pre.CodeMirror-line,
17 | .CodeMirror pre.CodeMirror-line-like {
18 | padding: 0 4px; /* Horizontal padding of content */
19 | }
20 |
21 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
22 | background-color: transparent; /* The little square between H and V scrollbars */
23 | }
24 |
25 | /* GUTTER */
26 |
27 | .CodeMirror-gutters {
28 | border-right: 1px solid #ddd;
29 | background-color: #f7f7f7;
30 | white-space: nowrap;
31 | }
32 | .CodeMirror-linenumbers {}
33 | .CodeMirror-linenumber {
34 | padding: 0 3px 0 5px;
35 | min-width: 20px;
36 | text-align: right;
37 | color: #999;
38 | white-space: nowrap;
39 | }
40 |
41 | .CodeMirror-guttermarker { color: black; }
42 | .CodeMirror-guttermarker-subtle { color: #999; }
43 |
44 | /* CURSOR */
45 |
46 | .CodeMirror-cursor {
47 | border-left: 1px solid black;
48 | border-right: none;
49 | width: 0;
50 | }
51 | /* Shown when moving in bi-directional text */
52 | .CodeMirror div.CodeMirror-secondarycursor {
53 | border-left: 1px solid silver;
54 | }
55 | .cm-fat-cursor .CodeMirror-cursor {
56 | width: auto;
57 | border: 0 !important;
58 | background: #7e7;
59 | }
60 | .cm-fat-cursor div.CodeMirror-cursors {
61 | z-index: 1;
62 | }
63 | .cm-fat-cursor-mark {
64 | background-color: rgba(20, 255, 20, 0.5);
65 | -webkit-animation: blink 1.06s steps(1) infinite;
66 | -moz-animation: blink 1.06s steps(1) infinite;
67 | animation: blink 1.06s steps(1) infinite;
68 | }
69 | .cm-animate-fat-cursor {
70 | width: auto;
71 | border: 0;
72 | -webkit-animation: blink 1.06s steps(1) infinite;
73 | -moz-animation: blink 1.06s steps(1) infinite;
74 | animation: blink 1.06s steps(1) infinite;
75 | background-color: #7e7;
76 | }
77 | @-moz-keyframes blink {
78 | 0% {}
79 | 50% { background-color: transparent; }
80 | 100% {}
81 | }
82 | @-webkit-keyframes blink {
83 | 0% {}
84 | 50% { background-color: transparent; }
85 | 100% {}
86 | }
87 | @keyframes blink {
88 | 0% {}
89 | 50% { background-color: transparent; }
90 | 100% {}
91 | }
92 |
93 | /* Can style cursor different in overwrite (non-insert) mode */
94 | .CodeMirror-overwrite .CodeMirror-cursor {}
95 |
96 | .cm-tab { display: inline-block; text-decoration: inherit; }
97 |
98 | .CodeMirror-rulers {
99 | position: absolute;
100 | left: 0; right: 0; top: -50px; bottom: 0;
101 | overflow: hidden;
102 | }
103 | .CodeMirror-ruler {
104 | border-left: 1px solid #ccc;
105 | top: 0; bottom: 0;
106 | position: absolute;
107 | }
108 |
109 | /* DEFAULT THEME */
110 |
111 | .cm-s-default .cm-header {color: blue;}
112 | .cm-s-default .cm-quote {color: #090;}
113 | .cm-negative {color: #d44;}
114 | .cm-positive {color: #292;}
115 | .cm-header, .cm-strong {font-weight: bold;}
116 | .cm-em {font-style: italic;}
117 | .cm-link {text-decoration: underline;}
118 | .cm-strikethrough {text-decoration: line-through;}
119 |
120 | .cm-s-default .cm-keyword {color: #708;}
121 | .cm-s-default .cm-atom {color: #219;}
122 | .cm-s-default .cm-number {color: #164;}
123 | .cm-s-default .cm-def {color: #00f;}
124 | .cm-s-default .cm-variable,
125 | .cm-s-default .cm-punctuation,
126 | .cm-s-default .cm-property,
127 | .cm-s-default .cm-operator {}
128 | .cm-s-default .cm-variable-2 {color: #05a;}
129 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
130 | .cm-s-default .cm-comment {color: #a50;}
131 | .cm-s-default .cm-string {color: #a11;}
132 | .cm-s-default .cm-string-2 {color: #f50;}
133 | .cm-s-default .cm-meta {color: #555;}
134 | .cm-s-default .cm-qualifier {color: #555;}
135 | .cm-s-default .cm-builtin {color: #30a;}
136 | .cm-s-default .cm-bracket {color: #997;}
137 | .cm-s-default .cm-tag {color: #170;}
138 | .cm-s-default .cm-attribute {color: #00c;}
139 | .cm-s-default .cm-hr {color: #999;}
140 | .cm-s-default .cm-link {color: #00c;}
141 |
142 | .cm-s-default .cm-error {color: #f00;}
143 | .cm-invalidchar {color: #f00;}
144 |
145 | .CodeMirror-composing { border-bottom: 2px solid; }
146 |
147 | /* Default styles for common addons */
148 |
149 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;}
150 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;}
151 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
152 | .CodeMirror-activeline-background {background: #e8f2ff;}
153 |
154 | /* STOP */
155 |
156 | /* The rest of this file contains styles related to the mechanics of
157 | the editor. You probably shouldn't touch them. */
158 |
159 | .CodeMirror {
160 | position: relative;
161 | overflow: hidden;
162 | background: white;
163 | }
164 |
165 | .CodeMirror-scroll {
166 | overflow: scroll !important; /* Things will break if this is overridden */
167 | /* 50px is the magic margin used to hide the element's real scrollbars */
168 | /* See overflow: hidden in .CodeMirror */
169 | margin-bottom: -50px; margin-right: -50px;
170 | padding-bottom: 50px;
171 | height: 100%;
172 | outline: none; /* Prevent dragging from highlighting the element */
173 | position: relative;
174 | }
175 | .CodeMirror-sizer {
176 | position: relative;
177 | border-right: 50px solid transparent;
178 | }
179 |
180 | /* The fake, visible scrollbars. Used to force redraw during scrolling
181 | before actual scrolling happens, thus preventing shaking and
182 | flickering artifacts. */
183 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
184 | position: absolute;
185 | z-index: 6;
186 | display: none;
187 | outline: none;
188 | }
189 | .CodeMirror-vscrollbar {
190 | right: 0; top: 0;
191 | overflow-x: hidden;
192 | overflow-y: scroll;
193 | }
194 | .CodeMirror-hscrollbar {
195 | bottom: 0; left: 0;
196 | overflow-y: hidden;
197 | overflow-x: scroll;
198 | }
199 | .CodeMirror-scrollbar-filler {
200 | right: 0; bottom: 0;
201 | }
202 | .CodeMirror-gutter-filler {
203 | left: 0; bottom: 0;
204 | }
205 |
206 | .CodeMirror-gutters {
207 | position: absolute; left: 0; top: 0;
208 | min-height: 100%;
209 | z-index: 3;
210 | }
211 | .CodeMirror-gutter {
212 | white-space: normal;
213 | height: 100%;
214 | display: inline-block;
215 | vertical-align: top;
216 | margin-bottom: -50px;
217 | }
218 | .CodeMirror-gutter-wrapper {
219 | position: absolute;
220 | z-index: 4;
221 | background: none !important;
222 | border: none !important;
223 | }
224 | .CodeMirror-gutter-background {
225 | position: absolute;
226 | top: 0; bottom: 0;
227 | z-index: 4;
228 | }
229 | .CodeMirror-gutter-elt {
230 | position: absolute;
231 | cursor: default;
232 | z-index: 4;
233 | }
234 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent }
235 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
236 |
237 | .CodeMirror-lines {
238 | cursor: text;
239 | min-height: 1px; /* prevents collapsing before first draw */
240 | }
241 | .CodeMirror pre.CodeMirror-line,
242 | .CodeMirror pre.CodeMirror-line-like {
243 | /* Reset some styles that the rest of the page might have set */
244 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
245 | border-width: 0;
246 | background: transparent;
247 | font-family: inherit;
248 | font-size: inherit;
249 | margin: 0;
250 | white-space: pre;
251 | word-wrap: normal;
252 | line-height: inherit;
253 | color: inherit;
254 | z-index: 2;
255 | position: relative;
256 | overflow: visible;
257 | -webkit-tap-highlight-color: transparent;
258 | -webkit-font-variant-ligatures: contextual;
259 | font-variant-ligatures: contextual;
260 | }
261 | .CodeMirror-wrap pre.CodeMirror-line,
262 | .CodeMirror-wrap pre.CodeMirror-line-like {
263 | word-wrap: break-word;
264 | white-space: pre-wrap;
265 | word-break: normal;
266 | }
267 |
268 | .CodeMirror-linebackground {
269 | position: absolute;
270 | left: 0; right: 0; top: 0; bottom: 0;
271 | z-index: 0;
272 | }
273 |
274 | .CodeMirror-linewidget {
275 | position: relative;
276 | z-index: 2;
277 | padding: 0.1px; /* Force widget margins to stay inside of the container */
278 | }
279 |
280 | .CodeMirror-widget {}
281 |
282 | .CodeMirror-rtl pre { direction: rtl; }
283 |
284 | .CodeMirror-code {
285 | outline: none;
286 | }
287 |
288 | /* Force content-box sizing for the elements where we expect it */
289 | .CodeMirror-scroll,
290 | .CodeMirror-sizer,
291 | .CodeMirror-gutter,
292 | .CodeMirror-gutters,
293 | .CodeMirror-linenumber {
294 | -moz-box-sizing: content-box;
295 | box-sizing: content-box;
296 | }
297 |
298 | .CodeMirror-measure {
299 | position: absolute;
300 | width: 100%;
301 | height: 0;
302 | overflow: hidden;
303 | visibility: hidden;
304 | }
305 |
306 | .CodeMirror-cursor {
307 | position: absolute;
308 | pointer-events: none;
309 | }
310 | .CodeMirror-measure pre { position: static; }
311 |
312 | div.CodeMirror-cursors {
313 | visibility: hidden;
314 | position: relative;
315 | z-index: 3;
316 | }
317 | div.CodeMirror-dragcursors {
318 | visibility: visible;
319 | }
320 |
321 | .CodeMirror-focused div.CodeMirror-cursors {
322 | visibility: visible;
323 | }
324 |
325 | .CodeMirror-selected { background: #d9d9d9; }
326 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
327 | .CodeMirror-crosshair { cursor: crosshair; }
328 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
329 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
330 |
331 | .cm-searching {
332 | background-color: #ffa;
333 | background-color: rgba(255, 255, 0, .4);
334 | }
335 |
336 | /* Used to force a border model for a node */
337 | .cm-force-border { padding-right: .1px; }
338 |
339 | @media print {
340 | /* Hide the cursor when printing */
341 | .CodeMirror div.CodeMirror-cursors {
342 | visibility: hidden;
343 | }
344 | }
345 |
346 | /* See issue #2901 */
347 | .cm-tab-wrap-hack:after { content: ''; }
348 |
349 | /* Help users use markselection to safely style text background */
350 | span.CodeMirror-selectedtext { background: none; }
351 |
--------------------------------------------------------------------------------
/user-guide.md:
--------------------------------------------------------------------------------
1 | # MarkDownload User Guide
2 | ## Basic Usage
3 | Simply click the  Markdown icon in the browser's extension area to show a popup containing the current website as Markdown. Here you can make quick edits to the content or the title before clicking the Download button at the bottom of the popup to download the content as a Markdown file. The file will be named according to the text in the title box at the top of the popover.
4 |
5 | Because the website is first passed through a readability process, you won't get extra content such as website navigation, footers and advertisements. However, please note that not all websites are created equal and as such some sites may not clip the content you expect.
6 |
7 | ### Clipping Selected Text
8 | If you select text on the page *and then* click the  Markdown icon, you will have the option of clipping just the selected text, or the entire document. This is great for capturing small snippets of a website, or for websites whose main content may not clip properly.
9 |
10 | Furthermore, if you select text *within* the popup, a "Download Selected" button will appear, allowing you to download just the selected section of text in the popup
11 |
12 | ### Include Front/Back Template
13 | The popup also includes the option to "Include front/back template". This is extra text and metadata that can appear at the start and/or end of the clipping. You can customize the templates for these in the [Front/Back Templates](#Front%2FBack%20Templates)
14 |
15 | ## Context Menu
16 | A couple of options are available in the context menu by right clicking on a page and hovering over the MarkDownload option.
17 |
18 | ### Download Tab as Markdown
19 | Select this option to download the current tab as a Markdown file, without having to open the popup. You can also set up a shortcut key for this functionality in your browser's settings (Alt+Shift+M by default)
20 |
21 | ### Download Selection as Markdown
22 | Select this option to download the currently selected section of the web page as a Markdown file, without having to open the popup
23 |
24 | ### Download All Tabs as Markdown
25 | Selecting this option will download all open tabs in the current window as Markdown files.
26 |
27 | ### Copy Tab as Markdown
28 | This converts the current tab's content as Markdown and copies it to the clipboard, so you can paste it in another program
29 |
30 | ### Copy Selection as Markdown
31 | This converts the currently selected section of the web page as Markdown and copies it to the clipboard, so you can paste it in another program
32 |
33 | ### Copy Tab URL as Markdown Link
34 | Copys the current tab's url and title as a Markdown link to be pasted into another Markdown document
35 |
36 | ### Copy Link as Markdown
37 | **Only when right-clicking on a link**
38 | Copies the selected link as a Markdown link to be pasted into another Markdown document
39 |
40 | ### Copy Image as Markdown
41 | **Only when right-clicking on an image**
42 | Copies the selected image as a Markdown image embed to be pasted into another Markdown document
43 |
44 | ### Include Front/Back Template
45 | This allows you to toggle the setting via the context menu — for if you would like the templates to be included without using the popup. As mentioned above, you can customize the templates in the [Front/Back Templates](#Front%2FBack%20Templates)
46 |
47 | ## Extension Options
48 | One of the best features of MarkDownload is that it is highly customizable. Open the extension's options and tweak things to work the way *you* want them to.
49 |
50 | ### Title Template
51 | This is what will be displayed in the popup as the file's title, and what will be the resulting markdown file's filename. Utilizes [Custom Text Substitutions](#Custom%20Text%20Substitutions)
52 |
53 | **Default value:** `{title}`
54 |
55 | ### Subfolder
56 | **Only available if [Download Mode](#Download%20Mode) is set to "Downloads API" (Not supported in Safari)**
57 | This specifies a subfolder within your downloads folder to save downloaded files. A security limitation of modern browsers prevents this folder being outside the browser's specified downloads folder. Utilizes [Custom Text Substitutions](#Custom%20Text%20Substitutions)
58 |
59 | **Default value:** (none)
60 |
61 | ### Disallowed Caracters
62 | There are specific characters that are automatically stripped from filenames, as they are invalid on certain operating systems. This setting allows you to add more characters to strip out, in case they are not supported in other programs you use.
63 |
64 | **Default value:** `[]#^` (these characters can cause issues with Obsidian)
65 |
66 | ### Front/Back Templates
67 | This is the text you would like to appear at the start or end of any downloaded Markdown files. Useful for supplying metadata. Utilizes [Custom Text Substitutions](#Custom%20Text%20Substitutions).
68 |
69 | **Default value (front):**
70 | ```
71 | ---
72 | created: {date:YYYY-MM-DDTHH:mm:ss} (UTC {date:Z})
73 | tags: [{keywords}]
74 | source: {baseURI}
75 | author: {byline}
76 | ---
77 |
78 | # {pageTitle}
79 |
80 | > ## Excerpt
81 | > {excerpt}
82 |
83 | ---
84 | ```
85 | **Default value (back):** (none)
86 |
87 | ### Download Mode
88 | **The Downloads API is recommended, but is not supported in the current version of Safari**
89 | This specifies the method to use for downloading Markdown files. Set to "Content Link" if you're having trouble with the Downloads API (Sometimes conflicts can occur with other download extensions, leading to randomly generated filenames and other symptoms).
90 |
91 | **Note:** "Content Link" mode is more limited and thus disables some functionality such as downloading images or using subfolders.
92 |
93 | ### Show Save As Dialog
94 | **Only available if [Download Mode](#Download%20Mode) is set to "Downloads API" (Not supported in Safari)**
95 | When this is on, the Save As popup will show when downloading a markdown file through the extension, regardless of the browser's current settings. Note that this is *not* recommended if [Download Images](#Download%20Images) is on.
96 |
97 | ### Download Images
98 | **Only available if [Download Mode](#Download%20Mode) is set to "Downloads API" (Not supported in Safari)**
99 | Turning this on will download images alongside Markdown files you download. The Markdown can even be adjusted to link to these local images, rather than the online ones (depending on your [Image Format](#Image Format) setting).
100 |
101 | ### Image Filename Prefix
102 | **Only available if [Download Images](#Download%20Images) is on (Not supported in Safari)**
103 | This allows you to provide a prefix and/or subfolder for images downloaded alongside markdown files. Including a forward slash (`/`) will specify a subfolder.
104 |
105 | **Default value:** `{title}/` — this means images will download in a folder with the same name as the Markdown file
106 |
107 | ### Heading Style
108 | - Setext Style headers:
109 | ```markdown
110 | All About Dogs
111 | ==============
112 | ```
113 | - Atx-Style Headers
114 | ```markdown
115 | # All About Dogs
116 | ```
117 |
118 | ### Horizontal Rule Style
119 | - `***`
120 | - `---`
121 | - `___`
122 |
123 | ### Bullet List Marker
124 | - `*`
125 | - `-`
126 | - `+`
127 |
128 | ### Code Block Style
129 | - Indented
130 | ```markdown
131 | ····const helloWorld = () => {
132 | ········console.log("Hello World");
133 | ····}
134 | ```
135 | - Fenced
136 | ~~~markdown
137 | ```
138 | const helloWorld = () => {
139 | ····console.log("Hello World");
140 | }
141 | ```
142 | ~~~
143 |
144 | ### Code Block Fence
145 | **If [Code Block Style](#Code%20Block%20Style) is "Fenced"**
146 | - ```
147 | - `~~~`
148 |
149 | ### Emphesis (italics) Delimiter
150 | - `_italics_`
151 | - `*italics*`
152 |
153 | ### Strong (bold) Delimiter
154 | - `**bold**`
155 | - `__bold__`
156 |
157 | ### Link Style
158 | - Inlined
159 | ```markdown
160 | Link to [Google](http://google.com)
161 | ```
162 | - Referenced
163 | ```markdown
164 | Link to [Google]
165 |
166 | [Google]: http://google.com
167 | ```
168 | - Strip links
169 | ```markdown
170 | Link to Google
171 | ```
172 |
173 | ### Link Reference Style
174 | **If [Link Style](#Link%20Style) is "Referenced"**
175 | - Full
176 | ```markdown
177 | Link to [Google][1]
178 |
179 | [1]: http://google.com
180 | ```
181 | - Collapsed
182 | ```markdown
183 | Link to [Google][]
184 |
185 | [Google]: http://google.com
186 | ```
187 | - Shortcut
188 | ```markdown
189 | Link to [Google]
190 |
191 | [Google]: http://google.com
192 | ```
193 |
194 | ### Image Style
195 | - Original Source
196 | ```markdown
197 | Figure 1: 
198 | ```
199 | - Strip Images
200 | ```markdown
201 | Figure 1:
202 | ```
203 | **The following options only apply if [Download Images](#Download%20Images) is on (Not supported in Safari)**
204 | - Pure Markdown
205 | ```markdown
206 | 
207 | ```
208 | - Obsidian internal embed
209 | ```markdown
210 | ![[folder/image.jpg]]
211 | ```
212 | - Obsidian internal embed (no folder prefix)
213 | ```markdown
214 | ![[image.jpg]]
215 | ```
216 |
217 | ### Escape Markdown Characters
218 | By default, backslashes (`\`) are used to escape Markdown characters in the HTML input. This ensures that these characters are not interpreted as Markdown. For example, the contents of `
1. Hello world
` needs to be escaped to `1\. Hello world`, otherwise it will be interpreted as a list item rather than a heading.
219 |
220 | Disabling this option disables this escaping.
221 |
222 | ## Custom Text Substitutions
223 | For options such as the [Title Template](#Title%20Template), [Subfolder](#Subfolder), [Front/Back Templates](#Front%2FBack%20Templates) and [Image Filename Prefix](#Image%20Filename%20Prefix), you can specify text substitutions, based on the website metadata and/or the current date. The following options are available:
224 |
225 | - `{title}` \- Article Title
226 | - `{pageTitle}` \- Title of the actual page
227 | - `{length}` \- Length of the article, in characters
228 | - `{excerpt}` \- Article description or short excerpt from the content
229 | - `{byline}` \- Author metadata
230 | - `{dir}` \- Content direction
231 | - `{baseURI}` \- The url of the article
232 | - `{date:FORMAT}` \- The current date and time. Check the [format reference](https://momentjs.com/docs/#/displaying/format/)
233 | - `{keywords}` \- Meta keywords (if present). Comma separated by default.
234 | - `{keywords:SEPARATOR}` \- Meta keywords (if present) separated by SEPARATOR. For example, to separate by space, use `{keywords: }`
235 |
236 | There is also support for all meta tags not mentioned above, should the page you are clipping support them. For example, try `{og:image}` or any other widely supported meta tags.
237 |
238 | Note that not all websites will provide all values.
239 |
--------------------------------------------------------------------------------
/src/browser-polyfill.min.js:
--------------------------------------------------------------------------------
1 | (function (a, b) { if ("function" == typeof define && define.amd) define("webextension-polyfill", ["module"], b); else if ("undefined" != typeof exports) b(module); else { var c = { exports: {} }; b(c), a.browser = c.exports } })("undefined" == typeof globalThis ? "undefined" == typeof self ? this : self : globalThis, function (a) { "use strict"; if ("undefined" == typeof browser || Object.getPrototypeOf(browser) !== Object.prototype) { if ("object" != typeof chrome || !chrome || !chrome.runtime || !chrome.runtime.id) throw new Error("This script should only be loaded in a browser extension."); a.exports = (a => { const b = { alarms: { clear: { minArgs: 0, maxArgs: 1 }, clearAll: { minArgs: 0, maxArgs: 0 }, get: { minArgs: 0, maxArgs: 1 }, getAll: { minArgs: 0, maxArgs: 0 } }, bookmarks: { create: { minArgs: 1, maxArgs: 1 }, get: { minArgs: 1, maxArgs: 1 }, getChildren: { minArgs: 1, maxArgs: 1 }, getRecent: { minArgs: 1, maxArgs: 1 }, getSubTree: { minArgs: 1, maxArgs: 1 }, getTree: { minArgs: 0, maxArgs: 0 }, move: { minArgs: 2, maxArgs: 2 }, remove: { minArgs: 1, maxArgs: 1 }, removeTree: { minArgs: 1, maxArgs: 1 }, search: { minArgs: 1, maxArgs: 1 }, update: { minArgs: 2, maxArgs: 2 } }, browserAction: { disable: { minArgs: 0, maxArgs: 1, fallbackToNoCallback: !0 }, enable: { minArgs: 0, maxArgs: 1, fallbackToNoCallback: !0 }, getBadgeBackgroundColor: { minArgs: 1, maxArgs: 1 }, getBadgeText: { minArgs: 1, maxArgs: 1 }, getPopup: { minArgs: 1, maxArgs: 1 }, getTitle: { minArgs: 1, maxArgs: 1 }, openPopup: { minArgs: 0, maxArgs: 0 }, setBadgeBackgroundColor: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, setBadgeText: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, setIcon: { minArgs: 1, maxArgs: 1 }, setPopup: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, setTitle: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 } }, browsingData: { remove: { minArgs: 2, maxArgs: 2 }, removeCache: { minArgs: 1, maxArgs: 1 }, removeCookies: { minArgs: 1, maxArgs: 1 }, removeDownloads: { minArgs: 1, maxArgs: 1 }, removeFormData: { minArgs: 1, maxArgs: 1 }, removeHistory: { minArgs: 1, maxArgs: 1 }, removeLocalStorage: { minArgs: 1, maxArgs: 1 }, removePasswords: { minArgs: 1, maxArgs: 1 }, removePluginData: { minArgs: 1, maxArgs: 1 }, settings: { minArgs: 0, maxArgs: 0 } }, commands: { getAll: { minArgs: 0, maxArgs: 0 } }, contextMenus: { remove: { minArgs: 1, maxArgs: 1 }, removeAll: { minArgs: 0, maxArgs: 0 }, update: { minArgs: 2, maxArgs: 2 } }, cookies: { get: { minArgs: 1, maxArgs: 1 }, getAll: { minArgs: 1, maxArgs: 1 }, getAllCookieStores: { minArgs: 0, maxArgs: 0 }, remove: { minArgs: 1, maxArgs: 1 }, set: { minArgs: 1, maxArgs: 1 } }, devtools: { inspectedWindow: { eval: { minArgs: 1, maxArgs: 2, singleCallbackArg: !1 } }, panels: { create: { minArgs: 3, maxArgs: 3, singleCallbackArg: !0 } } }, downloads: { cancel: { minArgs: 1, maxArgs: 1 }, download: { minArgs: 1, maxArgs: 1 }, erase: { minArgs: 1, maxArgs: 1 }, getFileIcon: { minArgs: 1, maxArgs: 2 }, open: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, pause: { minArgs: 1, maxArgs: 1 }, removeFile: { minArgs: 1, maxArgs: 1 }, resume: { minArgs: 1, maxArgs: 1 }, search: { minArgs: 1, maxArgs: 1 }, show: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 } }, extension: { isAllowedFileSchemeAccess: { minArgs: 0, maxArgs: 0 }, isAllowedIncognitoAccess: { minArgs: 0, maxArgs: 0 } }, history: { addUrl: { minArgs: 1, maxArgs: 1 }, deleteAll: { minArgs: 0, maxArgs: 0 }, deleteRange: { minArgs: 1, maxArgs: 1 }, deleteUrl: { minArgs: 1, maxArgs: 1 }, getVisits: { minArgs: 1, maxArgs: 1 }, search: { minArgs: 1, maxArgs: 1 } }, i18n: { detectLanguage: { minArgs: 1, maxArgs: 1 }, getAcceptLanguages: { minArgs: 0, maxArgs: 0 } }, identity: { launchWebAuthFlow: { minArgs: 1, maxArgs: 1 } }, idle: { queryState: { minArgs: 1, maxArgs: 1 } }, management: { get: { minArgs: 1, maxArgs: 1 }, getAll: { minArgs: 0, maxArgs: 0 }, getSelf: { minArgs: 0, maxArgs: 0 }, setEnabled: { minArgs: 2, maxArgs: 2 }, uninstallSelf: { minArgs: 0, maxArgs: 1 } }, notifications: { clear: { minArgs: 1, maxArgs: 1 }, create: { minArgs: 1, maxArgs: 2 }, getAll: { minArgs: 0, maxArgs: 0 }, getPermissionLevel: { minArgs: 0, maxArgs: 0 }, update: { minArgs: 2, maxArgs: 2 } }, pageAction: { getPopup: { minArgs: 1, maxArgs: 1 }, getTitle: { minArgs: 1, maxArgs: 1 }, hide: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, setIcon: { minArgs: 1, maxArgs: 1 }, setPopup: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, setTitle: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 }, show: { minArgs: 1, maxArgs: 1, fallbackToNoCallback: !0 } }, permissions: { contains: { minArgs: 1, maxArgs: 1 }, getAll: { minArgs: 0, maxArgs: 0 }, remove: { minArgs: 1, maxArgs: 1 }, request: { minArgs: 1, maxArgs: 1 } }, runtime: { getBackgroundPage: { minArgs: 0, maxArgs: 0 }, getPlatformInfo: { minArgs: 0, maxArgs: 0 }, openOptionsPage: { minArgs: 0, maxArgs: 0 }, requestUpdateCheck: { minArgs: 0, maxArgs: 0 }, sendMessage: { minArgs: 1, maxArgs: 3 }, sendNativeMessage: { minArgs: 2, maxArgs: 2 }, setUninstallURL: { minArgs: 1, maxArgs: 1 } }, sessions: { getDevices: { minArgs: 0, maxArgs: 1 }, getRecentlyClosed: { minArgs: 0, maxArgs: 1 }, restore: { minArgs: 0, maxArgs: 1 } }, storage: { local: { clear: { minArgs: 0, maxArgs: 0 }, get: { minArgs: 0, maxArgs: 1 }, getBytesInUse: { minArgs: 0, maxArgs: 1 }, remove: { minArgs: 1, maxArgs: 1 }, set: { minArgs: 1, maxArgs: 1 } }, managed: { get: { minArgs: 0, maxArgs: 1 }, getBytesInUse: { minArgs: 0, maxArgs: 1 } }, sync: { clear: { minArgs: 0, maxArgs: 0 }, get: { minArgs: 0, maxArgs: 1 }, getBytesInUse: { minArgs: 0, maxArgs: 1 }, remove: { minArgs: 1, maxArgs: 1 }, set: { minArgs: 1, maxArgs: 1 } } }, tabs: { captureVisibleTab: { minArgs: 0, maxArgs: 2 }, create: { minArgs: 1, maxArgs: 1 }, detectLanguage: { minArgs: 0, maxArgs: 1 }, discard: { minArgs: 0, maxArgs: 1 }, duplicate: { minArgs: 1, maxArgs: 1 }, executeScript: { minArgs: 1, maxArgs: 2 }, get: { minArgs: 1, maxArgs: 1 }, getCurrent: { minArgs: 0, maxArgs: 0 }, getZoom: { minArgs: 0, maxArgs: 1 }, getZoomSettings: { minArgs: 0, maxArgs: 1 }, highlight: { minArgs: 1, maxArgs: 1 }, insertCSS: { minArgs: 1, maxArgs: 2 }, move: { minArgs: 2, maxArgs: 2 }, query: { minArgs: 1, maxArgs: 1 }, reload: { minArgs: 0, maxArgs: 2 }, remove: { minArgs: 1, maxArgs: 1 }, removeCSS: { minArgs: 1, maxArgs: 2 }, sendMessage: { minArgs: 2, maxArgs: 3 }, setZoom: { minArgs: 1, maxArgs: 2 }, setZoomSettings: { minArgs: 1, maxArgs: 2 }, update: { minArgs: 1, maxArgs: 2 } }, topSites: { get: { minArgs: 0, maxArgs: 0 } }, webNavigation: { getAllFrames: { minArgs: 1, maxArgs: 1 }, getFrame: { minArgs: 1, maxArgs: 1 } }, webRequest: { handlerBehaviorChanged: { minArgs: 0, maxArgs: 0 } }, windows: { create: { minArgs: 0, maxArgs: 1 }, get: { minArgs: 1, maxArgs: 2 }, getAll: { minArgs: 0, maxArgs: 1 }, getCurrent: { minArgs: 0, maxArgs: 1 }, getLastFocused: { minArgs: 0, maxArgs: 1 }, remove: { minArgs: 1, maxArgs: 1 }, update: { minArgs: 2, maxArgs: 2 } } }; if (0 === Object.keys(b).length) throw new Error("api-metadata.json has not been included in browser-polyfill"); class c extends WeakMap { constructor(a, b = void 0) { super(b), this.createItem = a } get(a) { return this.has(a) || this.set(a, this.createItem(a)), super.get(a) } } const d = a => a && "object" == typeof a && "function" == typeof a.then, e = (b, c) => (...d) => { a.runtime.lastError ? b.reject(a.runtime.lastError) : c.singleCallbackArg || 1 >= d.length && !1 !== c.singleCallbackArg ? b.resolve(d[0]) : b.resolve(d) }, f = a => 1 == a ? "argument" : "arguments", g = (a, b) => function (c, ...d) { if (d.length < b.minArgs) throw new Error(`Expected at least ${b.minArgs} ${f(b.minArgs)} for ${a}(), got ${d.length}`); if (d.length > b.maxArgs) throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`); return new Promise((f, g) => { if (b.fallbackToNoCallback) try { c[a](...d, e({ resolve: f, reject: g }, b)) } catch (e) { console.warn(`${a} API method doesn't seem to support the callback parameter, ` + "falling back to call it without a callback: ", e), c[a](...d), b.fallbackToNoCallback = !1, b.noCallback = !0, f() } else b.noCallback ? (c[a](...d), f()) : c[a](...d, e({ resolve: f, reject: g }, b)) }) }, h = (a, b, c) => new Proxy(b, { apply(b, d, e) { return c.call(d, a, ...e) } }); let i = Function.call.bind(Object.prototype.hasOwnProperty); const j = (a, b = {}, c = {}) => { let d = Object.create(null), e = { has(b, c) { return c in a || c in d }, get(e, f, k) { if (f in d) return d[f]; if (!(f in a)) return; let l = a[f]; if ("function" == typeof l) { if ("function" == typeof b[f]) l = h(a, a[f], b[f]); else if (i(c, f)) { let b = g(f, c[f]); l = h(a, a[f], b) } else l = l.bind(a); } else if ("object" == typeof l && null !== l && (i(b, f) || i(c, f))) l = j(l, b[f], c[f]); else if (i(c, "*")) l = j(l, b[f], c["*"]); else return Object.defineProperty(d, f, { configurable: !0, enumerable: !0, get() { return a[f] }, set(b) { a[f] = b } }), l; return d[f] = l, l }, set(b, c, e, f) { return c in d ? d[c] = e : a[c] = e, !0 }, defineProperty(a, b, c) { return Reflect.defineProperty(d, b, c) }, deleteProperty(a, b) { return Reflect.deleteProperty(d, b) } }, f = Object.create(a); return new Proxy(f, e) }, k = a => ({ addListener(b, c, ...d) { b.addListener(a.get(c), ...d) }, hasListener(b, c) { return b.hasListener(a.get(c)) }, removeListener(b, c) { b.removeListener(a.get(c)) } }); let l = !1; const m = new c(a => "function" == typeof a ? function (b, c, e) { let f, g, h = !1, i = new Promise(a => { f = function (b) { l || (console.warn("Returning a Promise is the preferred way to send a reply from an onMessage/onMessageExternal listener, as the sendResponse will be removed from the specs (See https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/onMessage)", new Error().stack), l = !0), h = !0, a(b) } }); try { g = a(b, c, f) } catch (a) { g = Promise.reject(a) } const j = !0 !== g && d(g); if (!0 !== g && !j && !h) return !1; const k = a => { a.then(a => { e(a) }, a => { let b; b = a && (a instanceof Error || "string" == typeof a.message) ? a.message : "An unexpected error occurred", e({ __mozWebExtensionPolyfillReject__: !0, message: b }) }).catch(a => { console.error("Failed to send onMessage rejected reply", a) }) }; return j ? k(g) : k(i), !0 } : a), n = ({ reject: b, resolve: c }, d) => { a.runtime.lastError ? a.runtime.lastError.message === "The message port closed before a response was received." ? c() : b(a.runtime.lastError) : d && d.__mozWebExtensionPolyfillReject__ ? b(new Error(d.message)) : c(d) }, o = (a, b, c, ...d) => { if (d.length < b.minArgs) throw new Error(`Expected at least ${b.minArgs} ${f(b.minArgs)} for ${a}(), got ${d.length}`); if (d.length > b.maxArgs) throw new Error(`Expected at most ${b.maxArgs} ${f(b.maxArgs)} for ${a}(), got ${d.length}`); return new Promise((a, b) => { const e = n.bind(null, { resolve: a, reject: b }); d.push(e), c.sendMessage(...d) }) }, p = { runtime: { onMessage: k(m), onMessageExternal: k(m), sendMessage: o.bind(null, "sendMessage", { minArgs: 1, maxArgs: 3 }) }, tabs: { sendMessage: o.bind(null, "sendMessage", { minArgs: 2, maxArgs: 3 }) } }, q = { clear: { minArgs: 1, maxArgs: 1 }, get: { minArgs: 1, maxArgs: 1 }, set: { minArgs: 1, maxArgs: 1 } }; return b.privacy = { network: { "*": q }, services: { "*": q }, websites: { "*": q } }, j(a, p, b) })(chrome) } else a.exports = browser });
2 | //# sourceMappingURL=browser-polyfill.min.js.map
3 |
4 | // webextension-polyfill v.0.6.0 (https://github.com/mozilla/webextension-polyfill)
5 |
6 | /* This Source Code Form is subject to the terms of the Mozilla Public
7 | * License, v. 2.0. If a copy of the MPL was not distributed with this
8 | * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
--------------------------------------------------------------------------------
/src/options/options.js:
--------------------------------------------------------------------------------
1 | // these are the default options
2 | const defaultOptions = {
3 | headingStyle: "atx",
4 | hr: "___",
5 | bulletListMarker: "-",
6 | codeBlockStyle: "fenced",
7 | fence: "```",
8 | emDelimiter: "_",
9 | strongDelimiter: "**",
10 | linkStyle: "inlined",
11 | linkReferenceStyle: "full",
12 | imageStyle: "markdown",
13 | frontmatter: "---\ncreated: {date:YYYY-MM-DDTHH:mm:ss} (UTC {date:Z})\ntags: [{keywords}]\nsource: {baseURI}\nauthor: {byline}\n---\n\n# {pageTitle}\n\n> ## Excerpt\n> {excerpt}\n\n---",
14 | backmatter: "",
15 | title: "{title}",
16 | includeTemplate: false,
17 | saveAs: false,
18 | downloadImages: false,
19 | imagePrefix: '{title}/',
20 | mdClipsFolder: null,
21 | disallowedChars: '[]#^',
22 | downloadMode: 'downloadsApi',
23 | turndownEscape: true,
24 | // obsidianVault: null,
25 | // obsidianPathType: 'name'
26 | }
27 |
28 | let options = defaultOptions;
29 | let keyupTimeout = null;
30 |
31 |
32 | const saveOptions = e => {
33 | e.preventDefault();
34 |
35 | options = {
36 | frontmatter: document.querySelector("[name='frontmatter']").value,
37 | backmatter: document.querySelector("[name='backmatter']").value,
38 | title: document.querySelector("[name='title']").value,
39 | disallowedChars: document.querySelector("[name='disallowedChars']").value,
40 | includeTemplate: document.querySelector("[name='includeTemplate']").checked,
41 | saveAs: document.querySelector("[name='saveAs']").checked,
42 | downloadImages: document.querySelector("[name='downloadImages']").checked,
43 | imagePrefix: document.querySelector("[name='imagePrefix']").value,
44 | mdClipsFolder: document.querySelector("[name='mdClipsFolder']").value,
45 | turndownEscape: document.querySelector("[name='turndownEscape']").checked,
46 | // obsidianVault: document.querySelector("[name='obsidianVault']").value,
47 |
48 | headingStyle: getCheckedValue(document.querySelectorAll("input[name='headingStyle']")),
49 | hr: getCheckedValue(document.querySelectorAll("input[name='hr']")),
50 | bulletListMarker: getCheckedValue(document.querySelectorAll("input[name='bulletListMarker']")),
51 | codeBlockStyle: getCheckedValue(document.querySelectorAll("input[name='codeBlockStyle']")),
52 | fence: getCheckedValue(document.querySelectorAll("input[name='fence']")),
53 | emDelimiter: getCheckedValue(document.querySelectorAll("input[name='emDelimiter']")),
54 | strongDelimiter: getCheckedValue(document.querySelectorAll("input[name='strongDelimiter']")),
55 | linkStyle: getCheckedValue(document.querySelectorAll("input[name='linkStyle']")),
56 | linkReferenceStyle: getCheckedValue(document.querySelectorAll("input[name='linkReferenceStyle']")),
57 | imageStyle: getCheckedValue(document.querySelectorAll("input[name='imageStyle']")),
58 | downloadMode: getCheckedValue(document.querySelectorAll("input[name='downloadMode']")),
59 | // obsidianPathType: getCheckedValue(document.querySelectorAll("input[name='obsidianPathType']")),
60 | }
61 |
62 | save();
63 | }
64 |
65 | const save = () => {
66 | const spinner = document.getElementById("spinner");
67 | spinner.style.display = "block";
68 | browser.storage.sync.set(options)
69 | .then(() => {
70 | browser.contextMenus.update("toggle-includeTemplate", {
71 | checked: options.includeTemplate
72 | });
73 | try {
74 | browser.contextMenus.update("tabtoggle-includeTemplate", {
75 | checked: options.includeTemplate
76 | });
77 | } catch { }
78 | })
79 | .then(() => {
80 | spinner.style.display = "none";
81 | })
82 | .catch(err => {
83 | document.querySelectorAll(".status").forEach(statusEl => {
84 | statusEl.textContent = err;
85 | statusEl.classList.remove('success');
86 | statusEl.classList.add('error');
87 | });
88 | spinner.style.display = "none";
89 | });
90 | }
91 |
92 | const restoreOptions = () => {
93 | const setCurrentChoice = result => {
94 | options = result;
95 |
96 | // if browser doesn't support the download api (i.e. Safari)
97 | // we have to use contentLink download mode
98 | if (!browser.downloads) {
99 | options.downloadMode = 'contentLink';
100 | document.querySelectorAll("[name='downloadMode']").forEach(el => el.disabled = true)
101 | document.querySelector('#downloadMode p').innerText = "The Downloas API is unavailable in this browser."
102 | }
103 |
104 | const downloadImages = options.downloadImages && options.downloadMode == 'downloadsApi';
105 |
106 | if(!downloadImages && (options.imageStyle == 'markdown' || options.imageStyle.startsWith('obsidian'))) {
107 | options.imageStyle = 'originalSource';
108 | }
109 |
110 | document.querySelector("[name='frontmatter']").value = options.frontmatter;
111 | textareaInput.bind(document.querySelector("[name='frontmatter']"))();
112 | document.querySelector("[name='backmatter']").value = options.backmatter;
113 | textareaInput.bind(document.querySelector("[name='backmatter']"))();
114 | document.querySelector("[name='title']").value = options.title;
115 | document.querySelector("[name='disallowedChars']").value = options.disallowedChars;
116 | document.querySelector("[name='includeTemplate']").checked = options.includeTemplate;
117 | document.querySelector("[name='saveAs']").checked = options.saveAs;
118 | document.querySelector("[name='downloadImages']").checked = options.downloadImages;
119 | document.querySelector("[name='imagePrefix']").value = options.imagePrefix;
120 | document.querySelector("[name='mdClipsFolder']").value = result.mdClipsFolder;
121 | document.querySelector("[name='turndownEscape']").checked = options.turndownEscape;
122 | // document.querySelector("[name='obsidianVault']").value = options.obsidianVault;
123 |
124 | setCheckedValue(document.querySelectorAll("[name='headingStyle']"), options.headingStyle);
125 | setCheckedValue(document.querySelectorAll("[name='hr']"), options.hr);
126 | setCheckedValue(document.querySelectorAll("[name='bulletListMarker']"), options.bulletListMarker);
127 | setCheckedValue(document.querySelectorAll("[name='codeBlockStyle']"), options.codeBlockStyle);
128 | setCheckedValue(document.querySelectorAll("[name='fence']"), options.fence);
129 | setCheckedValue(document.querySelectorAll("[name='emDelimiter']"), options.emDelimiter);
130 | setCheckedValue(document.querySelectorAll("[name='strongDelimiter']"), options.strongDelimiter);
131 | setCheckedValue(document.querySelectorAll("[name='linkStyle']"), options.linkStyle);
132 | setCheckedValue(document.querySelectorAll("[name='linkReferenceStyle']"), options.linkReferenceStyle);
133 | setCheckedValue(document.querySelectorAll("[name='imageStyle']"), options.imageStyle);
134 | setCheckedValue(document.querySelectorAll("[name='downloadMode']"), options.downloadMode);
135 | // setCheckedValue(document.querySelectorAll("[name='obsidianPathType']"), options.obsidianPathType);
136 |
137 | refereshElements();
138 | }
139 |
140 | const onError = error => {
141 | console.error(error);
142 | }
143 |
144 | browser.storage.sync.get(defaultOptions).then(setCurrentChoice, onError);
145 | }
146 |
147 | function textareaInput(){
148 | this.parentNode.dataset.value = this.value;
149 | }
150 |
151 | const show = (el, show) => {
152 | el.style.height = show ? el.dataset.height + 'px' : "0";
153 | el.style.opacity = show ? "1" : "0";
154 | }
155 |
156 | const refereshElements = () => {
157 | document.getElementById("downloadModeGroup").querySelectorAll('.radio-container,.checkbox-container,.textbox-container').forEach(container => {
158 | show(container, options.downloadMode == 'downloadsApi')
159 | });
160 |
161 | // document.getElementById("obsidianUriGroup").querySelectorAll('.radio-container,.checkbox-container,.textbox-container').forEach(container => {
162 | // show(container, options.downloadMode == 'obsidianUri')
163 | // });
164 | show(document.getElementById("mdClipsFolder"), options.downloadMode == 'downloadsApi');
165 |
166 | show(document.getElementById("linkReferenceStyle"), (options.linkStyle == "referenced"));
167 |
168 | show(document.getElementById("fence"), (options.codeBlockStyle == "fenced"));
169 |
170 | const downloadImages = options.downloadImages && options.downloadMode == 'downloadsApi';
171 |
172 | show(document.getElementById("imagePrefix"), downloadImages);
173 |
174 | document.getElementById('markdown').disabled = !downloadImages;
175 | document.getElementById('obsidian').disabled = !downloadImages;
176 | document.getElementById('obsidian-nofolder').disabled = !downloadImages;
177 |
178 |
179 | }
180 |
181 | const inputChange = e => {
182 | console.log('inputChange');
183 |
184 | if (e) {
185 | let key = e.target.name;
186 | let value = e.target.value;
187 | if (e.target.type == "checkbox") value = e.target.checked;
188 | options[key] = value;
189 | }
190 |
191 | save();
192 |
193 | refereshElements();
194 | }
195 |
196 | const inputKeyup = (e) => {
197 | if (keyupTimeout) clearTimeout(keyupTimeout);
198 | keyupTimeout = setTimeout(inputChange, 500, e);
199 | }
200 |
201 | const loaded = () => {
202 | document.querySelectorAll('.radio-container,.checkbox-container,.textbox-container').forEach(container => {
203 | container.dataset.height = container.clientHeight;
204 | });
205 |
206 | restoreOptions();
207 |
208 | document.querySelectorAll('input,textarea').forEach(input => {
209 | if (input.tagName == "TEXTAREA" || input.type == "text") {
210 | input.addEventListener('keyup', inputKeyup);
211 | }
212 | else input.addEventListener('change', inputChange);
213 | })
214 | }
215 |
216 | document.addEventListener("DOMContentLoaded", loaded);
217 | document.querySelectorAll(".save").forEach(el => el.addEventListener("click", saveOptions));
218 | document.querySelectorAll(".input-sizer > textarea").forEach(el => el.addEventListener("input", textareaInput));
219 |
220 | /// https://www.somacon.com/p143.php
221 | // return the value of the radio button that is checked
222 | // return an empty string if none are checked, or
223 | // there are no radio buttons
224 | function getCheckedValue(radioObj) {
225 | if (!radioObj)
226 | return "";
227 | var radioLength = radioObj.length;
228 | if (radioLength == undefined)
229 | if (radioObj.checked)
230 | return radioObj.value;
231 | else
232 | return "";
233 | for (var i = 0; i < radioLength; i++) {
234 | if (radioObj[i].checked) {
235 | return radioObj[i].value;
236 | }
237 | }
238 | return "";
239 | }
240 |
241 | // set the radio button with the given value as being checked
242 | // do nothing if there are no radio buttons
243 | // if the given value does not exist, all the radio buttons
244 | // are reset to unchecked
245 | function setCheckedValue(radioObj, newValue) {
246 | if (!radioObj)
247 | return;
248 | var radioLength = radioObj.length;
249 | if (radioLength == undefined) {
250 | radioObj.checked = (radioObj.value == newValue.toString());
251 | return;
252 | }
253 | for (var i = 0; i < radioLength; i++) {
254 | radioObj[i].checked = false;
255 | if (radioObj[i].value == newValue.toString()) {
256 | radioObj[i].checked = true;
257 | }
258 | }
259 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2019 Enrico Kaack
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
--------------------------------------------------------------------------------
/src/options/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | MarkDownload Options
7 |
8 |
9 |
10 |
11 |
Custom text
12 | For the title, as well as the front- and back-matter custom text, you can use the following text replacement values.
13 | Please note that not all websites will provide all values
14 |
15 |
{title} - Article Title
16 |
{pageTitle} - Title of the actual page
17 |
{length} - Length of the article, in characters
18 |
{excerpt} - Article description or short excerpt from the content
19 |
{byline} - Author metadata
20 |
{dir} - Content direction
21 |
{baseURI} - The url of the article
22 |
{date:FORMAT} - The current date and time. Check the format reference
23 |
{keywords} - Meta keywords (if present). Comma separated by default.
24 |
{keywords:SEPARATOR} - Meta keywords (if present) separated by SEPARATOR. For example, to separate by space, use {keywords: }
25 |
26 | There is also support for all meta tags not mentioned above, should the page you are clipping support them.
27 | For example, try {og:image} or any other widely supported meta tags
28 |
29 |
30 |
Title template
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
49 |
50 |
51 |
52 |
Front-matter template
53 |
54 |
55 |
56 |
57 |
58 |
Back-matter template
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
Other options
73 |
74 |
75 |
Download Mode
76 |
77 | Method to use for downloading Markdown files. Set to "Content Link" if you're having trouble with the Downloads API
78 | (Sometimes conflicts can occur with other download extensions, leading to randomly generated filenames and other symptoms).
79 |
80 |
Note: "Content Link" mode disables some functionality such as downloading images or using subfolders in the filename.
81 |
82 |
83 |
84 |
85 |
87 |
88 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
Markdown conversion options
127 |
128 |
129 |
Heading Style
130 |
131 |
135 |
136 |
139 |
140 |
141 |
142 |
Horizontal Rule style
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
Bullet List Marker
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
Code Block Style
163 |
164 |
169 |
170 |
177 |
178 |
179 |
180 |
Code Block Fence
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
Emphesis (italics) Delimiter
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
Strong (bold) Delimiter
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
Link Style
205 |
206 |
209 |
210 |
215 |
216 |
219 |
220 |
221 |
222 |
Link Reference Style
223 |
224 |
229 |
230 |
235 |
236 |
241 |
242 |
243 |
244 |
Image Style
245 |
246 |
249 |
250 |
253 |
Note: The following only apply if Download Images is on.