├── .github └── FUNDING.yml ├── media ├── Edge Logo.png ├── SmallPromo.png ├── large promo.jpg ├── small Promo.jpg ├── Marquee Promo.jpg ├── Marquee Promo.png ├── Chrome.Screenshot.1.png ├── Safari.Screenshot.1.png ├── Firefox.Screenshot.1.png ├── Chrome.Screenshot.Settings1.png ├── Chrome.Screenshot.Settings2.png ├── Chrome.Screenshot.Settings3.png ├── Safari.Screenshot.Settings1.png ├── Safari.Screenshot.Settings2.png ├── Firefox.Screenshot.Settings1.png ├── Firefox.Screenshot.Settings2.png ├── Firefox.Screenshot.Settings3.png ├── Safari.Screenshot.DownloadAll.png ├── Chrome.Screenshot.ClipSelection.png ├── Chrome.Screenshot.DownloadAll1.png ├── Firefox.Screenshot.ClipSelection.png ├── Firefox.Screenshot.DownloadAll1.png ├── Firefox.Screenshot.DownloadAll2.png └── Safari.Screenshot.ClipSelection.png ├── src ├── icons │ ├── icon64x64.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-48x48.png │ ├── icon1024x1024.png │ ├── icon256x256.png │ ├── appicon-128x128.png │ ├── favicon-192x192.png │ └── favicon-512x512.png ├── popup │ ├── popup.html │ ├── lib │ │ ├── xq-light.css │ │ ├── xq-dark.css │ │ ├── codemirror.css │ │ └── modes │ │ │ └── markdown │ │ │ └── index.html │ ├── popup.css │ └── popup.js ├── manifest.json ├── contentScript │ └── contentScript.js ├── options │ ├── options.css │ ├── options.js │ └── options.html ├── browser-polyfill.min.js └── background │ └── turndown.js ├── xcode └── MarkDownload - Markdown Web Clipper │ ├── MarkDownload - Markdown Web Clipper │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── icon64x64.png │ │ │ ├── icon256x256.png │ │ │ ├── appicon-128x128.png │ │ │ ├── favicon-16x16.png │ │ │ ├── favicon-32x32.png │ │ │ ├── favicon-512x512.png │ │ │ ├── icon1024x1024.png │ │ │ ├── icon256x256-1.png │ │ │ └── Contents.json │ ├── MarkDownload___Markdown_Web_Clipper.entitlements │ ├── AppDelegate.swift │ ├── Info.plist │ ├── ViewController.swift │ └── Base.lproj │ │ └── Main.storyboard │ ├── MarkDownload - Markdown Web Clipper Extension │ ├── MarkDownload___Markdown_Web_Clipper_Extension.entitlements │ ├── SafariWebExtensionHandler.swift │ └── Info.plist │ └── MarkDownload - Markdown Web Clipper.xcodeproj │ └── project.pbxproj ├── .gitignore ├── .vscode ├── tasks.json └── extensions.json ├── PRIVACY.md ├── README.md ├── user-guide.md └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: deathau 2 | custom: ["https://www.paypal.me/deathau"] 3 | -------------------------------------------------------------------------------- /media/Edge Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Edge Logo.png -------------------------------------------------------------------------------- /media/SmallPromo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/SmallPromo.png -------------------------------------------------------------------------------- /media/large promo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/large promo.jpg -------------------------------------------------------------------------------- /media/small Promo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/small Promo.jpg -------------------------------------------------------------------------------- /media/Marquee Promo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Marquee Promo.jpg -------------------------------------------------------------------------------- /media/Marquee Promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Marquee Promo.png -------------------------------------------------------------------------------- /src/icons/icon64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/icon64x64.png -------------------------------------------------------------------------------- /src/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/icons/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/favicon-48x48.png -------------------------------------------------------------------------------- /src/icons/icon1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/icon1024x1024.png -------------------------------------------------------------------------------- /src/icons/icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/icon256x256.png -------------------------------------------------------------------------------- /media/Chrome.Screenshot.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Chrome.Screenshot.1.png -------------------------------------------------------------------------------- /media/Safari.Screenshot.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Safari.Screenshot.1.png -------------------------------------------------------------------------------- /src/icons/appicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/appicon-128x128.png -------------------------------------------------------------------------------- /src/icons/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/favicon-192x192.png -------------------------------------------------------------------------------- /src/icons/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/src/icons/favicon-512x512.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.1.png -------------------------------------------------------------------------------- /media/Chrome.Screenshot.Settings1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Chrome.Screenshot.Settings1.png -------------------------------------------------------------------------------- /media/Chrome.Screenshot.Settings2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Chrome.Screenshot.Settings2.png -------------------------------------------------------------------------------- /media/Chrome.Screenshot.Settings3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Chrome.Screenshot.Settings3.png -------------------------------------------------------------------------------- /media/Safari.Screenshot.Settings1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Safari.Screenshot.Settings1.png -------------------------------------------------------------------------------- /media/Safari.Screenshot.Settings2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Safari.Screenshot.Settings2.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.Settings1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.Settings1.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.Settings2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.Settings2.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.Settings3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.Settings3.png -------------------------------------------------------------------------------- /media/Safari.Screenshot.DownloadAll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Safari.Screenshot.DownloadAll.png -------------------------------------------------------------------------------- /media/Chrome.Screenshot.ClipSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Chrome.Screenshot.ClipSelection.png -------------------------------------------------------------------------------- /media/Chrome.Screenshot.DownloadAll1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Chrome.Screenshot.DownloadAll1.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.ClipSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.ClipSelection.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.DownloadAll1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.DownloadAll1.png -------------------------------------------------------------------------------- /media/Firefox.Screenshot.DownloadAll2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Firefox.Screenshot.DownloadAll2.png -------------------------------------------------------------------------------- /media/Safari.Screenshot.ClipSelection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/media/Safari.Screenshot.ClipSelection.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | web-ext-artifacts 2 | xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/project.xcworkspace 3 | xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/xcuserdata 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon64x64.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon256x256.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/appicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/appicon-128x128.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/favicon-16x16.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/favicon-32x32.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/favicon-512x512.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon1024x1024.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon256x256-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/darrylcauldwell/markdownload/HEAD/xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Assets.xcassets/AppIcon.appiconset/icon256x256-1.png -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/MarkDownload___Markdown_Web_Clipper.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "web-ext build -s ./src" 10 | }, 11 | { 12 | "label": "run", 13 | "type": "shell", 14 | "command": "web-ext run -s ./src" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper Extension/MarkDownload___Markdown_Web_Clipper_Extension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.files.user-selected.read-only 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "svsool.markdown-memo" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [ 11 | 12 | ] 13 | } -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // MarkDownload - Markdown Web Clipper 4 | // 5 | // Created by Gordon Pedersen on 17/2/21. 6 | // 7 | 8 | import Cocoa 9 | 10 | @main 11 | class AppDelegate: NSObject, NSApplicationDelegate { 12 | 13 | func applicationDidFinishLaunching(_ notification: Notification) { 14 | // Insert code here to initialize your application 15 | } 16 | 17 | func applicationWillTerminate(_ notification: Notification) { 18 | // Insert code here to tear down your application 19 | } 20 | 21 | func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 22 | return true 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper Extension/SafariWebExtensionHandler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SafariWebExtensionHandler.swift 3 | // MarkDownload - Markdown Web Clipper Extension 4 | // 5 | // Created by Gordon Pedersen on 17/2/21. 6 | // 7 | 8 | import SafariServices 9 | import os.log 10 | 11 | let SFExtensionMessageKey = "message" 12 | 13 | class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { 14 | 15 | func beginRequest(with context: NSExtensionContext) { 16 | let item = context.inputItems[0] as! NSExtensionItem 17 | let message = item.userInfo?[SFExtensionMessageKey] 18 | os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@", message as! CVarArg) 19 | 20 | let response = NSExtensionItem() 21 | response.userInfo = [ SFExtensionMessageKey: [ "Response to": message ] ] 22 | 23 | context.completeRequest(returningItems: [response], completionHandler: nil) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 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 | LSApplicationCategoryType 24 | public.app-category.productivity 25 | LSMinimumSystemVersion 26 | $(MACOSX_DEPLOYMENT_TARGET) 27 | NSMainStoryboardFile 28 | Main 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/popup/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | Selected Text 17 | Entire Document 18 |
19 | Include front/back template 20 | 21 | 22 | Download 23 | Download selected 24 |
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 | [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/deathau/markdownload?style=for-the-badge&sort=semver)](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://img.shields.io/chrome-web-store/v/pcmpcfapbekmbjjkdalcgopdkipoggdi.svg?logo=google-chrome&style=flat)](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi) [![](https://img.shields.io/chrome-web-store/rating/pcmpcfapbekmbjjkdalcgopdkipoggdi.svg?logo=google-chrome&style=flat)](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi) [![](https://img.shields.io/chrome-web-store/users/pcmpcfapbekmbjjkdalcgopdkipoggdi.svg?logo=google-chrome&style=flat)](https://chrome.google.com/webstore/detail/markdownload-markdown-web/pcmpcfapbekmbjjkdalcgopdkipoggdi) 16 | 17 | [![](https://img.shields.io/amo/v/markdownload.svg?logo=firefox&style=flat)](https://addons.mozilla.org/en-US/firefox/addon/markdownload/) [![](https://img.shields.io/amo/rating/markdownload.svg?logo=firefox&style=flat)](https://addons.mozilla.org/en-US/firefox/addon/markdownload/) [![](https://img.shields.io/amo/users/markdownload.svg?logo=firefox&style=flat)](https://addons.mozilla.org/en-US/firefox/addon/markdownload/) 18 | 19 | [![](https://img.shields.io/badge/dynamic/json?label=edge%20add-on&prefix=v&query=%24.version&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fhajanaajapkhaabfcofdjgjnlgkdkknm&style=flat&logo=microsoft-edge)](https://microsoftedge.microsoft.com/addons/detail/hajanaajapkhaabfcofdjgjnlgkdkknm) [![](https://img.shields.io/badge/dynamic/json?label=rating&suffix=/5&query=%24.averageRating&url=https%3A%2F%2Fmicrosoftedge.microsoft.com%2Faddons%2Fgetproductdetailsbycrxid%2Fhajanaajapkhaabfcofdjgjnlgkdkknm&style=flat&logo=microsoft-edge)](https://microsoftedge.microsoft.com/addons/detail/hajanaajapkhaabfcofdjgjnlgkdkknm) 20 | 21 | [![iTunes App Store](https://img.shields.io/itunes/v/1554029832?label=Safari&logo=safari&style=flat)](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 | [![GitHub Sponsors](https://img.shields.io/github/sponsors/deathau?style=social)](https://github.com/sponsors/deathau) 51 | [![Paypal](https://img.shields.io/badge/paypal-deathau-yellow?style=social&logo=paypal)](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 ![](icons/favicon-16x16.png) 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 ![](icons/favicon-16x16.png) 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: ![](http://example.com/img/image.jpg) 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 | ![](folder/image.jpg) 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 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 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.

254 | 255 | 258 | 259 | 262 | 263 | 266 |
267 | 268 |
269 | 270 | 281 |
282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /src/popup/lib/modes/markdown/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | CodeMirror: Markdown mode 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 32 | 33 |
34 |

Markdown mode

35 |
353 | 354 | 362 | 363 |

If you also want support strikethrough, emoji and few other goodies, check out Github-Flavored Markdown mode.

364 | 365 |

Optionally depends on other modes for properly highlighted code blocks, 366 | and XML mode for properly highlighted inline XML blocks.

367 | 368 |

Markdown mode supports these options:

369 |
    370 |
  • 371 | 372 |
    highlightFormatting: boolean
    373 |
    Whether to separately highlight markdown meta characterts (*[]()etc.) (default: false).
    374 |
    375 |
  • 376 |
  • 377 | 378 |
    maxBlockquoteDepth: boolean
    379 |
    Maximum allowed blockquote nesting (default: 0 - infinite nesting).
    380 |
    381 |
  • 382 |
  • 383 | 384 |
    xml: boolean
    385 |
    Whether to highlight inline XML (default: true).
    386 |
    387 |
  • 388 |
  • 389 | 390 |
    fencedCodeBlockHighlighting: boolean
    391 |
    Whether to syntax-highlight fenced code blocks, if given mode is included, or fencedCodeBlockDefaultMode is set (default: true).
    392 |
    393 |
  • 394 |
  • 395 | 396 |
    fencedCodeBlockDefaultMode: string
    397 |
    Mode to use for fencedCodeBlockHighlighting, if given mode is not included.
    398 |
    399 |
  • 400 |
  • 401 | 402 |
    tokenTypeOverrides: Object
    403 |
    When you want to override default token type names (e.g. {code: "code"}).
    404 |
    405 |
  • 406 |
  • 407 | 408 |
    allowAtxHeaderWithoutSpace: boolean
    409 |
    Allow lazy headers without whitespace between hashtag and text (default: false).
    410 |
    411 |
  • 412 |
413 | 414 |

MIME types defined: text/x-markdown.

415 | 416 |

Parsing/Highlighting Tests: normal, verbose.

417 | 418 |
419 | -------------------------------------------------------------------------------- /xcode/MarkDownload - Markdown Web Clipper/MarkDownload - Markdown Web Clipper.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | F88D6F4425DDEF08001F90AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88D6F4325DDEF08001F90AF /* AppDelegate.swift */; }; 11 | F88D6F4725DDEF08001F90AF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F4525DDEF08001F90AF /* Main.storyboard */; }; 12 | F88D6F4925DDEF08001F90AF /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88D6F4825DDEF08001F90AF /* ViewController.swift */; }; 13 | F88D6F4B25DDEF0A001F90AF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F4A25DDEF0A001F90AF /* Assets.xcassets */; }; 14 | F88D6F5225DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = F88D6F5125DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 15 | F88D6F5725DDEF0A001F90AF /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F88D6F5625DDEF0A001F90AF /* Cocoa.framework */; }; 16 | F88D6F5A25DDEF0A001F90AF /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88D6F5925DDEF0A001F90AF /* SafariWebExtensionHandler.swift */; }; 17 | F88D6F6E25DDEF0B001F90AF /* background in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6725DDEF0B001F90AF /* background */; }; 18 | F88D6F6F25DDEF0B001F90AF /* options in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6825DDEF0B001F90AF /* options */; }; 19 | F88D6F7025DDEF0B001F90AF /* popup in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6925DDEF0B001F90AF /* popup */; }; 20 | F88D6F7125DDEF0B001F90AF /* browser-polyfill.min.js in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6A25DDEF0B001F90AF /* browser-polyfill.min.js */; }; 21 | F88D6F7225DDEF0B001F90AF /* icons in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6B25DDEF0B001F90AF /* icons */; }; 22 | F88D6F7325DDEF0B001F90AF /* manifest.json in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6C25DDEF0B001F90AF /* manifest.json */; }; 23 | F88D6F7425DDEF0B001F90AF /* contentScript in Resources */ = {isa = PBXBuildFile; fileRef = F88D6F6D25DDEF0B001F90AF /* contentScript */; }; 24 | /* End PBXBuildFile section */ 25 | 26 | /* Begin PBXContainerItemProxy section */ 27 | F88D6F5325DDEF0A001F90AF /* PBXContainerItemProxy */ = { 28 | isa = PBXContainerItemProxy; 29 | containerPortal = F88D6F3725DDEF07001F90AF /* Project object */; 30 | proxyType = 1; 31 | remoteGlobalIDString = F88D6F5025DDEF0A001F90AF; 32 | remoteInfo = "MarkDownload - Markdown Web Clipper Extension"; 33 | }; 34 | /* End PBXContainerItemProxy section */ 35 | 36 | /* Begin PBXCopyFilesBuildPhase section */ 37 | F88D6F6225DDEF0A001F90AF /* Embed App Extensions */ = { 38 | isa = PBXCopyFilesBuildPhase; 39 | buildActionMask = 2147483647; 40 | dstPath = ""; 41 | dstSubfolderSpec = 13; 42 | files = ( 43 | F88D6F5225DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension.appex in Embed App Extensions */, 44 | ); 45 | name = "Embed App Extensions"; 46 | runOnlyForDeploymentPostprocessing = 0; 47 | }; 48 | /* End PBXCopyFilesBuildPhase section */ 49 | 50 | /* Begin PBXFileReference section */ 51 | F88D6F3F25DDEF08001F90AF /* MarkDownload - Markdown Web Clipper.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "MarkDownload - Markdown Web Clipper.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | F88D6F4225DDEF08001F90AF /* MarkDownload___Markdown_Web_Clipper.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "MarkDownload___Markdown_Web_Clipper.entitlements"; sourceTree = ""; }; 53 | F88D6F4325DDEF08001F90AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 54 | F88D6F4625DDEF08001F90AF /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 55 | F88D6F4825DDEF08001F90AF /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 56 | F88D6F4A25DDEF0A001F90AF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 57 | F88D6F4C25DDEF0A001F90AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 58 | F88D6F5125DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "MarkDownload - Markdown Web Clipper Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 59 | F88D6F5625DDEF0A001F90AF /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 60 | F88D6F5925DDEF0A001F90AF /* SafariWebExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariWebExtensionHandler.swift; sourceTree = ""; }; 61 | F88D6F5B25DDEF0A001F90AF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 62 | F88D6F5C25DDEF0A001F90AF /* MarkDownload___Markdown_Web_Clipper_Extension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "MarkDownload___Markdown_Web_Clipper_Extension.entitlements"; sourceTree = ""; }; 63 | F88D6F6725DDEF0B001F90AF /* background */ = {isa = PBXFileReference; lastKnownFileType = folder; name = background; path = ../../../src/background; sourceTree = ""; }; 64 | F88D6F6825DDEF0B001F90AF /* options */ = {isa = PBXFileReference; lastKnownFileType = folder; name = options; path = ../../../src/options; sourceTree = ""; }; 65 | F88D6F6925DDEF0B001F90AF /* popup */ = {isa = PBXFileReference; lastKnownFileType = folder; name = popup; path = ../../../src/popup; sourceTree = ""; }; 66 | F88D6F6A25DDEF0B001F90AF /* browser-polyfill.min.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; name = "browser-polyfill.min.js"; path = "../../../src/browser-polyfill.min.js"; sourceTree = ""; }; 67 | F88D6F6B25DDEF0B001F90AF /* icons */ = {isa = PBXFileReference; lastKnownFileType = folder; name = icons; path = ../../../src/icons; sourceTree = ""; }; 68 | F88D6F6C25DDEF0B001F90AF /* manifest.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; name = manifest.json; path = ../../../src/manifest.json; sourceTree = ""; }; 69 | F88D6F6D25DDEF0B001F90AF /* contentScript */ = {isa = PBXFileReference; lastKnownFileType = folder; name = contentScript; path = ../../../src/contentScript; sourceTree = ""; }; 70 | /* End PBXFileReference section */ 71 | 72 | /* Begin PBXFrameworksBuildPhase section */ 73 | F88D6F3C25DDEF07001F90AF /* Frameworks */ = { 74 | isa = PBXFrameworksBuildPhase; 75 | buildActionMask = 2147483647; 76 | files = ( 77 | ); 78 | runOnlyForDeploymentPostprocessing = 0; 79 | }; 80 | F88D6F4E25DDEF0A001F90AF /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | F88D6F5725DDEF0A001F90AF /* Cocoa.framework in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | F88D6F3625DDEF07001F90AF = { 92 | isa = PBXGroup; 93 | children = ( 94 | F88D6F4125DDEF08001F90AF /* MarkDownload - Markdown Web Clipper */, 95 | F88D6F5825DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension */, 96 | F88D6F5525DDEF0A001F90AF /* Frameworks */, 97 | F88D6F4025DDEF08001F90AF /* Products */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | F88D6F4025DDEF08001F90AF /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | F88D6F3F25DDEF08001F90AF /* MarkDownload - Markdown Web Clipper.app */, 105 | F88D6F5125DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension.appex */, 106 | ); 107 | name = Products; 108 | sourceTree = ""; 109 | }; 110 | F88D6F4125DDEF08001F90AF /* MarkDownload - Markdown Web Clipper */ = { 111 | isa = PBXGroup; 112 | children = ( 113 | F88D6F4225DDEF08001F90AF /* MarkDownload___Markdown_Web_Clipper.entitlements */, 114 | F88D6F4325DDEF08001F90AF /* AppDelegate.swift */, 115 | F88D6F4525DDEF08001F90AF /* Main.storyboard */, 116 | F88D6F4825DDEF08001F90AF /* ViewController.swift */, 117 | F88D6F4A25DDEF0A001F90AF /* Assets.xcassets */, 118 | F88D6F4C25DDEF0A001F90AF /* Info.plist */, 119 | ); 120 | path = "MarkDownload - Markdown Web Clipper"; 121 | sourceTree = ""; 122 | }; 123 | F88D6F5525DDEF0A001F90AF /* Frameworks */ = { 124 | isa = PBXGroup; 125 | children = ( 126 | F88D6F5625DDEF0A001F90AF /* Cocoa.framework */, 127 | ); 128 | name = Frameworks; 129 | sourceTree = ""; 130 | }; 131 | F88D6F5825DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | F88D6F6625DDEF0B001F90AF /* Resources */, 135 | F88D6F5925DDEF0A001F90AF /* SafariWebExtensionHandler.swift */, 136 | F88D6F5B25DDEF0A001F90AF /* Info.plist */, 137 | F88D6F5C25DDEF0A001F90AF /* MarkDownload___Markdown_Web_Clipper_Extension.entitlements */, 138 | ); 139 | path = "MarkDownload - Markdown Web Clipper Extension"; 140 | sourceTree = ""; 141 | }; 142 | F88D6F6625DDEF0B001F90AF /* Resources */ = { 143 | isa = PBXGroup; 144 | children = ( 145 | F88D6F6725DDEF0B001F90AF /* background */, 146 | F88D6F6825DDEF0B001F90AF /* options */, 147 | F88D6F6925DDEF0B001F90AF /* popup */, 148 | F88D6F6A25DDEF0B001F90AF /* browser-polyfill.min.js */, 149 | F88D6F6B25DDEF0B001F90AF /* icons */, 150 | F88D6F6C25DDEF0B001F90AF /* manifest.json */, 151 | F88D6F6D25DDEF0B001F90AF /* contentScript */, 152 | ); 153 | name = Resources; 154 | path = "MarkDownload - Markdown Web Clipper Extension"; 155 | sourceTree = SOURCE_ROOT; 156 | }; 157 | /* End PBXGroup section */ 158 | 159 | /* Begin PBXNativeTarget section */ 160 | F88D6F3E25DDEF07001F90AF /* MarkDownload - Markdown Web Clipper */ = { 161 | isa = PBXNativeTarget; 162 | buildConfigurationList = F88D6F6325DDEF0A001F90AF /* Build configuration list for PBXNativeTarget "MarkDownload - Markdown Web Clipper" */; 163 | buildPhases = ( 164 | F88D6F3B25DDEF07001F90AF /* Sources */, 165 | F88D6F3C25DDEF07001F90AF /* Frameworks */, 166 | F88D6F3D25DDEF07001F90AF /* Resources */, 167 | F88D6F6225DDEF0A001F90AF /* Embed App Extensions */, 168 | ); 169 | buildRules = ( 170 | ); 171 | dependencies = ( 172 | F88D6F5425DDEF0A001F90AF /* PBXTargetDependency */, 173 | ); 174 | name = "MarkDownload - Markdown Web Clipper"; 175 | productName = "MarkDownload - Markdown Web Clipper"; 176 | productReference = F88D6F3F25DDEF08001F90AF /* MarkDownload - Markdown Web Clipper.app */; 177 | productType = "com.apple.product-type.application"; 178 | }; 179 | F88D6F5025DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension */ = { 180 | isa = PBXNativeTarget; 181 | buildConfigurationList = F88D6F5F25DDEF0A001F90AF /* Build configuration list for PBXNativeTarget "MarkDownload - Markdown Web Clipper Extension" */; 182 | buildPhases = ( 183 | F88D6F4D25DDEF0A001F90AF /* Sources */, 184 | F88D6F4E25DDEF0A001F90AF /* Frameworks */, 185 | F88D6F4F25DDEF0A001F90AF /* Resources */, 186 | ); 187 | buildRules = ( 188 | ); 189 | dependencies = ( 190 | ); 191 | name = "MarkDownload - Markdown Web Clipper Extension"; 192 | productName = "MarkDownload - Markdown Web Clipper Extension"; 193 | productReference = F88D6F5125DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension.appex */; 194 | productType = "com.apple.product-type.app-extension"; 195 | }; 196 | /* End PBXNativeTarget section */ 197 | 198 | /* Begin PBXProject section */ 199 | F88D6F3725DDEF07001F90AF /* Project object */ = { 200 | isa = PBXProject; 201 | attributes = { 202 | LastSwiftUpdateCheck = 1240; 203 | LastUpgradeCheck = 1240; 204 | TargetAttributes = { 205 | F88D6F3E25DDEF07001F90AF = { 206 | CreatedOnToolsVersion = 12.4; 207 | }; 208 | F88D6F5025DDEF0A001F90AF = { 209 | CreatedOnToolsVersion = 12.4; 210 | }; 211 | }; 212 | }; 213 | buildConfigurationList = F88D6F3A25DDEF07001F90AF /* Build configuration list for PBXProject "MarkDownload - Markdown Web Clipper" */; 214 | compatibilityVersion = "Xcode 9.3"; 215 | developmentRegion = en; 216 | hasScannedForEncodings = 0; 217 | knownRegions = ( 218 | en, 219 | Base, 220 | ); 221 | mainGroup = F88D6F3625DDEF07001F90AF; 222 | productRefGroup = F88D6F4025DDEF08001F90AF /* Products */; 223 | projectDirPath = ""; 224 | projectRoot = ""; 225 | targets = ( 226 | F88D6F3E25DDEF07001F90AF /* MarkDownload - Markdown Web Clipper */, 227 | F88D6F5025DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension */, 228 | ); 229 | }; 230 | /* End PBXProject section */ 231 | 232 | /* Begin PBXResourcesBuildPhase section */ 233 | F88D6F3D25DDEF07001F90AF /* Resources */ = { 234 | isa = PBXResourcesBuildPhase; 235 | buildActionMask = 2147483647; 236 | files = ( 237 | F88D6F4B25DDEF0A001F90AF /* Assets.xcassets in Resources */, 238 | F88D6F4725DDEF08001F90AF /* Main.storyboard in Resources */, 239 | ); 240 | runOnlyForDeploymentPostprocessing = 0; 241 | }; 242 | F88D6F4F25DDEF0A001F90AF /* Resources */ = { 243 | isa = PBXResourcesBuildPhase; 244 | buildActionMask = 2147483647; 245 | files = ( 246 | F88D6F7125DDEF0B001F90AF /* browser-polyfill.min.js in Resources */, 247 | F88D6F7025DDEF0B001F90AF /* popup in Resources */, 248 | F88D6F6E25DDEF0B001F90AF /* background in Resources */, 249 | F88D6F7425DDEF0B001F90AF /* contentScript in Resources */, 250 | F88D6F7225DDEF0B001F90AF /* icons in Resources */, 251 | F88D6F7325DDEF0B001F90AF /* manifest.json in Resources */, 252 | F88D6F6F25DDEF0B001F90AF /* options in Resources */, 253 | ); 254 | runOnlyForDeploymentPostprocessing = 0; 255 | }; 256 | /* End PBXResourcesBuildPhase section */ 257 | 258 | /* Begin PBXSourcesBuildPhase section */ 259 | F88D6F3B25DDEF07001F90AF /* Sources */ = { 260 | isa = PBXSourcesBuildPhase; 261 | buildActionMask = 2147483647; 262 | files = ( 263 | F88D6F4925DDEF08001F90AF /* ViewController.swift in Sources */, 264 | F88D6F4425DDEF08001F90AF /* AppDelegate.swift in Sources */, 265 | ); 266 | runOnlyForDeploymentPostprocessing = 0; 267 | }; 268 | F88D6F4D25DDEF0A001F90AF /* Sources */ = { 269 | isa = PBXSourcesBuildPhase; 270 | buildActionMask = 2147483647; 271 | files = ( 272 | F88D6F5A25DDEF0A001F90AF /* SafariWebExtensionHandler.swift in Sources */, 273 | ); 274 | runOnlyForDeploymentPostprocessing = 0; 275 | }; 276 | /* End PBXSourcesBuildPhase section */ 277 | 278 | /* Begin PBXTargetDependency section */ 279 | F88D6F5425DDEF0A001F90AF /* PBXTargetDependency */ = { 280 | isa = PBXTargetDependency; 281 | target = F88D6F5025DDEF0A001F90AF /* MarkDownload - Markdown Web Clipper Extension */; 282 | targetProxy = F88D6F5325DDEF0A001F90AF /* PBXContainerItemProxy */; 283 | }; 284 | /* End PBXTargetDependency section */ 285 | 286 | /* Begin PBXVariantGroup section */ 287 | F88D6F4525DDEF08001F90AF /* Main.storyboard */ = { 288 | isa = PBXVariantGroup; 289 | children = ( 290 | F88D6F4625DDEF08001F90AF /* Base */, 291 | ); 292 | name = Main.storyboard; 293 | sourceTree = ""; 294 | }; 295 | /* End PBXVariantGroup section */ 296 | 297 | /* Begin XCBuildConfiguration section */ 298 | F88D6F5D25DDEF0A001F90AF /* Debug */ = { 299 | isa = XCBuildConfiguration; 300 | buildSettings = { 301 | ALWAYS_SEARCH_USER_PATHS = NO; 302 | CLANG_ANALYZER_NONNULL = YES; 303 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 304 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 305 | CLANG_CXX_LIBRARY = "libc++"; 306 | CLANG_ENABLE_MODULES = YES; 307 | CLANG_ENABLE_OBJC_ARC = YES; 308 | CLANG_ENABLE_OBJC_WEAK = YES; 309 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 310 | CLANG_WARN_BOOL_CONVERSION = YES; 311 | CLANG_WARN_COMMA = YES; 312 | CLANG_WARN_CONSTANT_CONVERSION = YES; 313 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 314 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 315 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 316 | CLANG_WARN_EMPTY_BODY = YES; 317 | CLANG_WARN_ENUM_CONVERSION = YES; 318 | CLANG_WARN_INFINITE_RECURSION = YES; 319 | CLANG_WARN_INT_CONVERSION = YES; 320 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 321 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 322 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 323 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 324 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 325 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 326 | CLANG_WARN_STRICT_PROTOTYPES = YES; 327 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 328 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 329 | CLANG_WARN_UNREACHABLE_CODE = YES; 330 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 331 | COPY_PHASE_STRIP = NO; 332 | DEBUG_INFORMATION_FORMAT = dwarf; 333 | ENABLE_STRICT_OBJC_MSGSEND = YES; 334 | ENABLE_TESTABILITY = YES; 335 | GCC_C_LANGUAGE_STANDARD = gnu11; 336 | GCC_DYNAMIC_NO_PIC = NO; 337 | GCC_NO_COMMON_BLOCKS = YES; 338 | GCC_OPTIMIZATION_LEVEL = 0; 339 | GCC_PREPROCESSOR_DEFINITIONS = ( 340 | "DEBUG=1", 341 | "$(inherited)", 342 | ); 343 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 344 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 345 | GCC_WARN_UNDECLARED_SELECTOR = YES; 346 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 347 | GCC_WARN_UNUSED_FUNCTION = YES; 348 | GCC_WARN_UNUSED_VARIABLE = YES; 349 | MACOSX_DEPLOYMENT_TARGET = 10.15; 350 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 351 | MTL_FAST_MATH = YES; 352 | ONLY_ACTIVE_ARCH = YES; 353 | SDKROOT = macosx; 354 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 355 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 356 | }; 357 | name = Debug; 358 | }; 359 | F88D6F5E25DDEF0A001F90AF /* Release */ = { 360 | isa = XCBuildConfiguration; 361 | buildSettings = { 362 | ALWAYS_SEARCH_USER_PATHS = NO; 363 | CLANG_ANALYZER_NONNULL = YES; 364 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 366 | CLANG_CXX_LIBRARY = "libc++"; 367 | CLANG_ENABLE_MODULES = YES; 368 | CLANG_ENABLE_OBJC_ARC = YES; 369 | CLANG_ENABLE_OBJC_WEAK = YES; 370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 371 | CLANG_WARN_BOOL_CONVERSION = YES; 372 | CLANG_WARN_COMMA = YES; 373 | CLANG_WARN_CONSTANT_CONVERSION = YES; 374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 377 | CLANG_WARN_EMPTY_BODY = YES; 378 | CLANG_WARN_ENUM_CONVERSION = YES; 379 | CLANG_WARN_INFINITE_RECURSION = YES; 380 | CLANG_WARN_INT_CONVERSION = YES; 381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 385 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 390 | CLANG_WARN_UNREACHABLE_CODE = YES; 391 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 394 | ENABLE_NS_ASSERTIONS = NO; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu11; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | MACOSX_DEPLOYMENT_TARGET = 10.15; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | MTL_FAST_MATH = YES; 407 | SDKROOT = macosx; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 410 | }; 411 | name = Release; 412 | }; 413 | F88D6F6025DDEF0A001F90AF /* Debug */ = { 414 | isa = XCBuildConfiguration; 415 | buildSettings = { 416 | CODE_SIGN_ENTITLEMENTS = "MarkDownload - Markdown Web Clipper Extension/MarkDownload___Markdown_Web_Clipper_Extension.entitlements"; 417 | CODE_SIGN_STYLE = Automatic; 418 | CURRENT_PROJECT_VERSION = 2; 419 | DEVELOPMENT_TEAM = 4JYKNQ5WRZ; 420 | INFOPLIST_FILE = "MarkDownload - Markdown Web Clipper Extension/Info.plist"; 421 | LD_RUNPATH_SEARCH_PATHS = ( 422 | "$(inherited)", 423 | "@executable_path/../Frameworks", 424 | "@executable_path/../../../../Frameworks", 425 | ); 426 | MACOSX_DEPLOYMENT_TARGET = 10.14; 427 | MARKETING_VERSION = 3.0.0; 428 | PRODUCT_BUNDLE_IDENTIFIER = au.death.MarkDownload.Extension; 429 | PRODUCT_NAME = "$(TARGET_NAME)"; 430 | SKIP_INSTALL = YES; 431 | SWIFT_VERSION = 5.0; 432 | }; 433 | name = Debug; 434 | }; 435 | F88D6F6125DDEF0A001F90AF /* Release */ = { 436 | isa = XCBuildConfiguration; 437 | buildSettings = { 438 | CODE_SIGN_ENTITLEMENTS = "MarkDownload - Markdown Web Clipper Extension/MarkDownload___Markdown_Web_Clipper_Extension.entitlements"; 439 | CODE_SIGN_STYLE = Automatic; 440 | CURRENT_PROJECT_VERSION = 2; 441 | DEVELOPMENT_TEAM = 4JYKNQ5WRZ; 442 | INFOPLIST_FILE = "MarkDownload - Markdown Web Clipper Extension/Info.plist"; 443 | LD_RUNPATH_SEARCH_PATHS = ( 444 | "$(inherited)", 445 | "@executable_path/../Frameworks", 446 | "@executable_path/../../../../Frameworks", 447 | ); 448 | MACOSX_DEPLOYMENT_TARGET = 10.14; 449 | MARKETING_VERSION = 3.0.0; 450 | PRODUCT_BUNDLE_IDENTIFIER = au.death.MarkDownload.Extension; 451 | PRODUCT_NAME = "$(TARGET_NAME)"; 452 | SKIP_INSTALL = YES; 453 | SWIFT_VERSION = 5.0; 454 | }; 455 | name = Release; 456 | }; 457 | F88D6F6425DDEF0A001F90AF /* Debug */ = { 458 | isa = XCBuildConfiguration; 459 | buildSettings = { 460 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 461 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 462 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 463 | CODE_SIGN_ENTITLEMENTS = "MarkDownload - Markdown Web Clipper/MarkDownload___Markdown_Web_Clipper.entitlements"; 464 | CODE_SIGN_STYLE = Automatic; 465 | COMBINE_HIDPI_IMAGES = YES; 466 | CURRENT_PROJECT_VERSION = 2; 467 | DEVELOPMENT_TEAM = 4JYKNQ5WRZ; 468 | INFOPLIST_FILE = "MarkDownload - Markdown Web Clipper/Info.plist"; 469 | LD_RUNPATH_SEARCH_PATHS = ( 470 | "$(inherited)", 471 | "@executable_path/../Frameworks", 472 | ); 473 | MACOSX_DEPLOYMENT_TARGET = 10.14; 474 | MARKETING_VERSION = 3.0.0; 475 | PRODUCT_BUNDLE_IDENTIFIER = au.death.MarkDownload; 476 | PRODUCT_NAME = "$(TARGET_NAME)"; 477 | SWIFT_VERSION = 5.0; 478 | }; 479 | name = Debug; 480 | }; 481 | F88D6F6525DDEF0A001F90AF /* Release */ = { 482 | isa = XCBuildConfiguration; 483 | buildSettings = { 484 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 485 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 486 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 487 | CODE_SIGN_ENTITLEMENTS = "MarkDownload - Markdown Web Clipper/MarkDownload___Markdown_Web_Clipper.entitlements"; 488 | CODE_SIGN_STYLE = Automatic; 489 | COMBINE_HIDPI_IMAGES = YES; 490 | CURRENT_PROJECT_VERSION = 2; 491 | DEVELOPMENT_TEAM = 4JYKNQ5WRZ; 492 | INFOPLIST_FILE = "MarkDownload - Markdown Web Clipper/Info.plist"; 493 | LD_RUNPATH_SEARCH_PATHS = ( 494 | "$(inherited)", 495 | "@executable_path/../Frameworks", 496 | ); 497 | MACOSX_DEPLOYMENT_TARGET = 10.14; 498 | MARKETING_VERSION = 3.0.0; 499 | PRODUCT_BUNDLE_IDENTIFIER = au.death.MarkDownload; 500 | PRODUCT_NAME = "$(TARGET_NAME)"; 501 | SWIFT_VERSION = 5.0; 502 | }; 503 | name = Release; 504 | }; 505 | /* End XCBuildConfiguration section */ 506 | 507 | /* Begin XCConfigurationList section */ 508 | F88D6F3A25DDEF07001F90AF /* Build configuration list for PBXProject "MarkDownload - Markdown Web Clipper" */ = { 509 | isa = XCConfigurationList; 510 | buildConfigurations = ( 511 | F88D6F5D25DDEF0A001F90AF /* Debug */, 512 | F88D6F5E25DDEF0A001F90AF /* Release */, 513 | ); 514 | defaultConfigurationIsVisible = 0; 515 | defaultConfigurationName = Release; 516 | }; 517 | F88D6F5F25DDEF0A001F90AF /* Build configuration list for PBXNativeTarget "MarkDownload - Markdown Web Clipper Extension" */ = { 518 | isa = XCConfigurationList; 519 | buildConfigurations = ( 520 | F88D6F6025DDEF0A001F90AF /* Debug */, 521 | F88D6F6125DDEF0A001F90AF /* Release */, 522 | ); 523 | defaultConfigurationIsVisible = 0; 524 | defaultConfigurationName = Release; 525 | }; 526 | F88D6F6325DDEF0A001F90AF /* Build configuration list for PBXNativeTarget "MarkDownload - Markdown Web Clipper" */ = { 527 | isa = XCConfigurationList; 528 | buildConfigurations = ( 529 | F88D6F6425DDEF0A001F90AF /* Debug */, 530 | F88D6F6525DDEF0A001F90AF /* Release */, 531 | ); 532 | defaultConfigurationIsVisible = 0; 533 | defaultConfigurationName = Release; 534 | }; 535 | /* End XCConfigurationList section */ 536 | }; 537 | rootObject = F88D6F3725DDEF07001F90AF /* Project object */; 538 | } 539 | -------------------------------------------------------------------------------- /src/background/turndown.js: -------------------------------------------------------------------------------- 1 | var TurndownService = (function () { 2 | 'use strict'; 3 | 4 | function extend (destination) { 5 | for (var i = 1; i < arguments.length; i++) { 6 | var source = arguments[i]; 7 | for (var key in source) { 8 | if (source.hasOwnProperty(key)) destination[key] = source[key]; 9 | } 10 | } 11 | return destination 12 | } 13 | 14 | function repeat (character, count) { 15 | return Array(count + 1).join(character) 16 | } 17 | 18 | var blockElements = [ 19 | 'ADDRESS', 'ARTICLE', 'ASIDE', 'AUDIO', 'BLOCKQUOTE', 'BODY', 'CANVAS', 20 | 'CENTER', 'DD', 'DIR', 'DIV', 'DL', 'DT', 'FIELDSET', 'FIGCAPTION', 'FIGURE', 21 | 'FOOTER', 'FORM', 'FRAMESET', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HEADER', 22 | 'HGROUP', 'HR', 'HTML', 'ISINDEX', 'LI', 'MAIN', 'MENU', 'NAV', 'NOFRAMES', 23 | 'NOSCRIPT', 'OL', 'OUTPUT', 'P', 'PRE', 'SECTION', 'TABLE', 'TBODY', 'TD', 24 | 'TFOOT', 'TH', 'THEAD', 'TR', 'UL' 25 | ]; 26 | 27 | function isBlock (node) { 28 | return is(node, blockElements) 29 | } 30 | 31 | var voidElements = [ 32 | 'AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 33 | 'KEYGEN', 'LINK', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR' 34 | ]; 35 | 36 | function isVoid (node) { 37 | return is(node, voidElements) 38 | } 39 | 40 | function hasVoid (node) { 41 | return has(node, voidElements) 42 | } 43 | 44 | var meaningfulWhenBlankElements = [ 45 | 'A', 'TABLE', 'THEAD', 'TBODY', 'TFOOT', 'TH', 'TD', 'IFRAME', 'SCRIPT', 46 | 'AUDIO', 'VIDEO' 47 | ]; 48 | 49 | function isMeaningfulWhenBlank (node) { 50 | return is(node, meaningfulWhenBlankElements) 51 | } 52 | 53 | function hasMeaningfulWhenBlank (node) { 54 | return has(node, meaningfulWhenBlankElements) 55 | } 56 | 57 | function is (node, tagNames) { 58 | return tagNames.indexOf(node.nodeName) >= 0 59 | } 60 | 61 | function has (node, tagNames) { 62 | return ( 63 | node.getElementsByTagName && 64 | tagNames.some(function (tagName) { 65 | return node.getElementsByTagName(tagName).length 66 | }) 67 | ) 68 | } 69 | 70 | var rules = {}; 71 | 72 | rules.paragraph = { 73 | filter: 'p', 74 | 75 | replacement: function (content) { 76 | return '\n\n' + content + '\n\n' 77 | } 78 | }; 79 | 80 | rules.lineBreak = { 81 | filter: 'br', 82 | 83 | replacement: function (content, node, options) { 84 | return options.br + '\n' 85 | } 86 | }; 87 | 88 | rules.heading = { 89 | filter: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], 90 | 91 | replacement: function (content, node, options) { 92 | var hLevel = Number(node.nodeName.charAt(1)); 93 | 94 | if (options.headingStyle === 'setext' && hLevel < 3) { 95 | var underline = repeat((hLevel === 1 ? '=' : '-'), content.length); 96 | return ( 97 | '\n\n' + content + '\n' + underline + '\n\n' 98 | ) 99 | } else { 100 | return '\n\n' + repeat('#', hLevel) + ' ' + content + '\n\n' 101 | } 102 | } 103 | }; 104 | 105 | rules.blockquote = { 106 | filter: 'blockquote', 107 | 108 | replacement: function (content) { 109 | content = content.replace(/^\n+|\n+$/g, ''); 110 | content = content.replace(/^/gm, '> '); 111 | return '\n\n' + content + '\n\n' 112 | } 113 | }; 114 | 115 | rules.list = { 116 | filter: ['ul', 'ol'], 117 | 118 | replacement: function (content, node) { 119 | var parent = node.parentNode; 120 | if (parent.nodeName === 'LI' && parent.lastElementChild === node) { 121 | return '\n' + content 122 | } else { 123 | return '\n\n' + content + '\n\n' 124 | } 125 | } 126 | }; 127 | 128 | rules.listItem = { 129 | filter: 'li', 130 | 131 | replacement: function (content, node, options) { 132 | content = content 133 | .replace(/^\n+/, '') // remove leading newlines 134 | .replace(/\n+$/, '\n') // replace trailing newlines with just a single one 135 | .replace(/\n/gm, '\n '); // indent 136 | var prefix = options.bulletListMarker + ' '; 137 | var parent = node.parentNode; 138 | if (parent.nodeName === 'OL') { 139 | var start = parent.getAttribute('start'); 140 | var index = Array.prototype.indexOf.call(parent.children, node); 141 | prefix = (start ? Number(start) + index : index + 1) + '. '; 142 | } 143 | return ( 144 | prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '') 145 | ) 146 | } 147 | }; 148 | 149 | rules.indentedCodeBlock = { 150 | filter: function (node, options) { 151 | return ( 152 | options.codeBlockStyle === 'indented' && 153 | node.nodeName === 'PRE' && 154 | node.firstChild && 155 | node.firstChild.nodeName === 'CODE' 156 | ) 157 | }, 158 | 159 | replacement: function (content, node, options) { 160 | return ( 161 | '\n\n ' + 162 | node.firstChild.textContent.replace(/\n/g, '\n ') + 163 | '\n\n' 164 | ) 165 | } 166 | }; 167 | 168 | rules.fencedCodeBlock = { 169 | filter: function (node, options) { 170 | return ( 171 | options.codeBlockStyle === 'fenced' && 172 | node.nodeName === 'PRE' && 173 | node.firstChild && 174 | node.firstChild.nodeName === 'CODE' 175 | ) 176 | }, 177 | 178 | replacement: function (content, node, options) { 179 | var className = node.firstChild.getAttribute('class') || ''; 180 | var language = (className.match(/language-(\S+)/) || [null, ''])[1]; 181 | var code = node.firstChild.textContent; 182 | 183 | var fenceChar = options.fence.charAt(0); 184 | var fenceSize = 3; 185 | var fenceInCodeRegex = new RegExp('^' + fenceChar + '{3,}', 'gm'); 186 | 187 | var match; 188 | while ((match = fenceInCodeRegex.exec(code))) { 189 | if (match[0].length >= fenceSize) { 190 | fenceSize = match[0].length + 1; 191 | } 192 | } 193 | 194 | var fence = repeat(fenceChar, fenceSize); 195 | 196 | return ( 197 | '\n\n' + fence + language + '\n' + 198 | code.replace(/\n$/, '') + 199 | '\n' + fence + '\n\n' 200 | ) 201 | } 202 | }; 203 | 204 | rules.horizontalRule = { 205 | filter: 'hr', 206 | 207 | replacement: function (content, node, options) { 208 | return '\n\n' + options.hr + '\n\n' 209 | } 210 | }; 211 | 212 | rules.inlineLink = { 213 | filter: function (node, options) { 214 | return ( 215 | options.linkStyle === 'inlined' && 216 | node.nodeName === 'A' && 217 | node.getAttribute('href') 218 | ) 219 | }, 220 | 221 | replacement: function (content, node) { 222 | var href = node.getAttribute('href'); 223 | var title = cleanAttribute(node.getAttribute('title')); 224 | if (title) title = ' "' + title + '"'; 225 | return '[' + content + '](' + href + title + ')' 226 | } 227 | }; 228 | 229 | rules.referenceLink = { 230 | filter: function (node, options) { 231 | return ( 232 | options.linkStyle === 'referenced' && 233 | node.nodeName === 'A' && 234 | node.getAttribute('href') 235 | ) 236 | }, 237 | 238 | replacement: function (content, node, options) { 239 | var href = node.getAttribute('href'); 240 | var title = cleanAttribute(node.getAttribute('title')); 241 | if (title) title = ' "' + title + '"'; 242 | var replacement; 243 | var reference; 244 | 245 | switch (options.linkReferenceStyle) { 246 | case 'collapsed': 247 | replacement = '[' + content + '][]'; 248 | reference = '[' + content + ']: ' + href + title; 249 | break 250 | case 'shortcut': 251 | replacement = '[' + content + ']'; 252 | reference = '[' + content + ']: ' + href + title; 253 | break 254 | default: 255 | var id = this.references.length + 1; 256 | replacement = '[' + content + '][' + id + ']'; 257 | reference = '[' + id + ']: ' + href + title; 258 | } 259 | 260 | this.references.push(reference); 261 | return replacement 262 | }, 263 | 264 | references: [], 265 | 266 | append: function (options) { 267 | var references = ''; 268 | if (this.references.length) { 269 | references = '\n\n' + this.references.join('\n') + '\n\n'; 270 | this.references = []; // Reset references 271 | } 272 | return references 273 | } 274 | }; 275 | 276 | rules.emphasis = { 277 | filter: ['em', 'i'], 278 | 279 | replacement: function (content, node, options) { 280 | if (!content.trim()) return '' 281 | return options.emDelimiter + content + options.emDelimiter 282 | } 283 | }; 284 | 285 | rules.strong = { 286 | filter: ['strong', 'b'], 287 | 288 | replacement: function (content, node, options) { 289 | if (!content.trim()) return '' 290 | return options.strongDelimiter + content + options.strongDelimiter 291 | } 292 | }; 293 | 294 | rules.code = { 295 | filter: function (node) { 296 | var hasSiblings = node.previousSibling || node.nextSibling; 297 | var isCodeBlock = node.parentNode.nodeName === 'PRE' && !hasSiblings; 298 | 299 | return node.nodeName === 'CODE' && !isCodeBlock 300 | }, 301 | 302 | replacement: function (content) { 303 | if (!content.trim()) return '' 304 | 305 | var delimiter = '`'; 306 | var leadingSpace = ''; 307 | var trailingSpace = ''; 308 | var matches = content.match(/`+/gm); 309 | if (matches) { 310 | if (/^`/.test(content)) leadingSpace = ' '; 311 | if (/`$/.test(content)) trailingSpace = ' '; 312 | while (matches.indexOf(delimiter) !== -1) delimiter = delimiter + '`'; 313 | } 314 | 315 | return delimiter + leadingSpace + content + trailingSpace + delimiter 316 | } 317 | }; 318 | 319 | rules.image = { 320 | filter: 'img', 321 | 322 | replacement: function (content, node) { 323 | var alt = cleanAttribute(node.getAttribute('alt')); 324 | var src = node.getAttribute('src') || ''; 325 | var title = cleanAttribute(node.getAttribute('title')); 326 | var titlePart = title ? ' "' + title + '"' : ''; 327 | return src ? '![' + alt + ']' + '(' + src + titlePart + ')' : '' 328 | } 329 | }; 330 | 331 | function cleanAttribute (attribute) { 332 | return attribute ? attribute.replace(/(\n+\s*)+/g, '\n') : '' 333 | } 334 | 335 | /** 336 | * Manages a collection of rules used to convert HTML to Markdown 337 | */ 338 | 339 | function Rules (options) { 340 | this.options = options; 341 | this._keep = []; 342 | this._remove = []; 343 | 344 | this.blankRule = { 345 | replacement: options.blankReplacement 346 | }; 347 | 348 | this.keepReplacement = options.keepReplacement; 349 | 350 | this.defaultRule = { 351 | replacement: options.defaultReplacement 352 | }; 353 | 354 | this.array = []; 355 | for (var key in options.rules) this.array.push(options.rules[key]); 356 | } 357 | 358 | Rules.prototype = { 359 | add: function (key, rule) { 360 | this.array.unshift(rule); 361 | }, 362 | 363 | keep: function (filter) { 364 | this._keep.unshift({ 365 | filter: filter, 366 | replacement: this.keepReplacement 367 | }); 368 | }, 369 | 370 | remove: function (filter) { 371 | this._remove.unshift({ 372 | filter: filter, 373 | replacement: function () { 374 | return '' 375 | } 376 | }); 377 | }, 378 | 379 | forNode: function (node) { 380 | if (node.isBlank) return this.blankRule 381 | var rule; 382 | 383 | if ((rule = findRule(this.array, node, this.options))) return rule 384 | if ((rule = findRule(this._keep, node, this.options))) return rule 385 | if ((rule = findRule(this._remove, node, this.options))) return rule 386 | 387 | return this.defaultRule 388 | }, 389 | 390 | forEach: function (fn) { 391 | for (var i = 0; i < this.array.length; i++) fn(this.array[i], i); 392 | } 393 | }; 394 | 395 | function findRule (rules, node, options) { 396 | for (var i = 0; i < rules.length; i++) { 397 | var rule = rules[i]; 398 | if (filterValue(rule, node, options)) return rule 399 | } 400 | return void 0 401 | } 402 | 403 | function filterValue (rule, node, options) { 404 | var filter = rule.filter; 405 | if (typeof filter === 'string') { 406 | if (filter === node.nodeName.toLowerCase()) return true 407 | } else if (Array.isArray(filter)) { 408 | if (filter.indexOf(node.nodeName.toLowerCase()) > -1) return true 409 | } else if (typeof filter === 'function') { 410 | if (filter.call(rule, node, options)) return true 411 | } else { 412 | throw new TypeError('`filter` needs to be a string, array, or function') 413 | } 414 | } 415 | 416 | /** 417 | * The collapseWhitespace function is adapted from collapse-whitespace 418 | * by Luc Thevenard. 419 | * 420 | * The MIT License (MIT) 421 | * 422 | * Copyright (c) 2014 Luc Thevenard 423 | * 424 | * Permission is hereby granted, free of charge, to any person obtaining a copy 425 | * of this software and associated documentation files (the "Software"), to deal 426 | * in the Software without restriction, including without limitation the rights 427 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 428 | * copies of the Software, and to permit persons to whom the Software is 429 | * furnished to do so, subject to the following conditions: 430 | * 431 | * The above copyright notice and this permission notice shall be included in 432 | * all copies or substantial portions of the Software. 433 | * 434 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 435 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 436 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 437 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 438 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 439 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 440 | * THE SOFTWARE. 441 | */ 442 | 443 | /** 444 | * collapseWhitespace(options) removes extraneous whitespace from an the given element. 445 | * 446 | * @param {Object} options 447 | */ 448 | function collapseWhitespace (options) { 449 | var element = options.element; 450 | var isBlock = options.isBlock; 451 | var isVoid = options.isVoid; 452 | var isPre = options.isPre || function (node) { 453 | return node.nodeName === 'PRE' 454 | }; 455 | 456 | if (!element.firstChild || isPre(element)) return 457 | 458 | var prevText = null; 459 | var prevVoid = false; 460 | 461 | var prev = null; 462 | var node = next(prev, element, isPre); 463 | 464 | while (node !== element) { 465 | if (node.nodeType === 3 || node.nodeType === 4) { // Node.TEXT_NODE or Node.CDATA_SECTION_NODE 466 | var text = node.data.replace(/[ \r\n\t]+/g, ' '); 467 | 468 | if ((!prevText || / $/.test(prevText.data)) && 469 | !prevVoid && text[0] === ' ') { 470 | text = text.substr(1); 471 | } 472 | 473 | // `text` might be empty at this point. 474 | if (!text) { 475 | node = remove(node); 476 | continue 477 | } 478 | 479 | node.data = text; 480 | 481 | prevText = node; 482 | } else if (node.nodeType === 1) { // Node.ELEMENT_NODE 483 | if (isBlock(node) || node.nodeName === 'BR') { 484 | if (prevText) { 485 | prevText.data = prevText.data.replace(/ $/, ''); 486 | } 487 | 488 | prevText = null; 489 | prevVoid = false; 490 | } else if (isVoid(node)) { 491 | // Avoid trimming space around non-block, non-BR void elements. 492 | prevText = null; 493 | prevVoid = true; 494 | } 495 | } else { 496 | node = remove(node); 497 | continue 498 | } 499 | 500 | var nextNode = next(prev, node, isPre); 501 | prev = node; 502 | node = nextNode; 503 | } 504 | 505 | if (prevText) { 506 | prevText.data = prevText.data.replace(/ $/, ''); 507 | if (!prevText.data) { 508 | remove(prevText); 509 | } 510 | } 511 | } 512 | 513 | /** 514 | * remove(node) removes the given node from the DOM and returns the 515 | * next node in the sequence. 516 | * 517 | * @param {Node} node 518 | * @return {Node} node 519 | */ 520 | function remove (node) { 521 | var next = node.nextSibling || node.parentNode; 522 | 523 | node.parentNode.removeChild(node); 524 | 525 | return next 526 | } 527 | 528 | /** 529 | * next(prev, current, isPre) returns the next node in the sequence, given the 530 | * current and previous nodes. 531 | * 532 | * @param {Node} prev 533 | * @param {Node} current 534 | * @param {Function} isPre 535 | * @return {Node} 536 | */ 537 | function next (prev, current, isPre) { 538 | if ((prev && prev.parentNode === current) || isPre(current)) { 539 | return current.nextSibling || current.parentNode 540 | } 541 | 542 | return current.firstChild || current.nextSibling || current.parentNode 543 | } 544 | 545 | /* 546 | * Set up window for Node.js 547 | */ 548 | 549 | var root = (typeof window !== 'undefined' ? window : {}); 550 | 551 | /* 552 | * Parsing HTML strings 553 | */ 554 | 555 | function canParseHTMLNatively () { 556 | var Parser = root.DOMParser; 557 | var canParse = false; 558 | 559 | // Adapted from https://gist.github.com/1129031 560 | // Firefox/Opera/IE throw errors on unsupported types 561 | try { 562 | // WebKit returns null on unsupported types 563 | if (new Parser().parseFromString('', 'text/html')) { 564 | canParse = true; 565 | } 566 | } catch (e) {} 567 | 568 | return canParse 569 | } 570 | 571 | function createHTMLParser () { 572 | var Parser = function () {}; 573 | 574 | { 575 | if (shouldUseActiveX()) { 576 | Parser.prototype.parseFromString = function (string) { 577 | var doc = new window.ActiveXObject('htmlfile'); 578 | doc.designMode = 'on'; // disable on-page scripts 579 | doc.open(); 580 | doc.write(string); 581 | doc.close(); 582 | return doc 583 | }; 584 | } else { 585 | Parser.prototype.parseFromString = function (string) { 586 | var doc = document.implementation.createHTMLDocument(''); 587 | doc.open(); 588 | doc.write(string); 589 | doc.close(); 590 | return doc 591 | }; 592 | } 593 | } 594 | return Parser 595 | } 596 | 597 | function shouldUseActiveX () { 598 | var useActiveX = false; 599 | try { 600 | document.implementation.createHTMLDocument('').open(); 601 | } catch (e) { 602 | if (window.ActiveXObject) useActiveX = true; 603 | } 604 | return useActiveX 605 | } 606 | 607 | var HTMLParser = canParseHTMLNatively() ? root.DOMParser : createHTMLParser(); 608 | 609 | function RootNode (input) { 610 | var root; 611 | if (typeof input === 'string') { 612 | var doc = htmlParser().parseFromString( 613 | // DOM parsers arrange elements in the and . 614 | // Wrapping in a custom element ensures elements are reliably arranged in 615 | // a single element. 616 | '' + input + '', 617 | 'text/html' 618 | ); 619 | root = doc.getElementById('turndown-root'); 620 | } else { 621 | root = input.cloneNode(true); 622 | } 623 | collapseWhitespace({ 624 | element: root, 625 | isBlock: isBlock, 626 | isVoid: isVoid 627 | }); 628 | 629 | return root 630 | } 631 | 632 | var _htmlParser; 633 | function htmlParser () { 634 | _htmlParser = _htmlParser || new HTMLParser(); 635 | return _htmlParser 636 | } 637 | 638 | function Node (node) { 639 | node.isBlock = isBlock(node); 640 | node.isCode = node.nodeName.toLowerCase() === 'code' || node.parentNode.isCode; 641 | node.isBlank = isBlank(node); 642 | node.flankingWhitespace = flankingWhitespace(node); 643 | return node 644 | } 645 | 646 | function isBlank (node) { 647 | return ( 648 | !isVoid(node) && 649 | !isMeaningfulWhenBlank(node) && 650 | /^\s*$/i.test(node.textContent) && 651 | !hasVoid(node) && 652 | !hasMeaningfulWhenBlank(node) 653 | ) 654 | } 655 | 656 | function flankingWhitespace (node) { 657 | var leading = ''; 658 | var trailing = ''; 659 | 660 | if (!node.isBlock) { 661 | var hasLeading = /^\s/.test(node.textContent); 662 | var hasTrailing = /\s$/.test(node.textContent); 663 | var blankWithSpaces = node.isBlank && hasLeading && hasTrailing; 664 | 665 | if (hasLeading && !isFlankedByWhitespace('left', node)) { 666 | leading = ' '; 667 | } 668 | 669 | if (!blankWithSpaces && hasTrailing && !isFlankedByWhitespace('right', node)) { 670 | trailing = ' '; 671 | } 672 | } 673 | 674 | return { leading: leading, trailing: trailing } 675 | } 676 | 677 | function isFlankedByWhitespace (side, node) { 678 | var sibling; 679 | var regExp; 680 | var isFlanked; 681 | 682 | if (side === 'left') { 683 | sibling = node.previousSibling; 684 | regExp = / $/; 685 | } else { 686 | sibling = node.nextSibling; 687 | regExp = /^ /; 688 | } 689 | 690 | if (sibling) { 691 | if (sibling.nodeType === 3) { 692 | isFlanked = regExp.test(sibling.nodeValue); 693 | } else if (sibling.nodeType === 1 && !isBlock(sibling)) { 694 | isFlanked = regExp.test(sibling.textContent); 695 | } 696 | } 697 | return isFlanked 698 | } 699 | 700 | var reduce = Array.prototype.reduce; 701 | var leadingNewLinesRegExp = /^\n*/; 702 | var trailingNewLinesRegExp = /\n*$/; 703 | var escapes = [ 704 | [/\\/g, '\\\\'], 705 | [/\*/g, '\\*'], 706 | [/^-/g, '\\-'], 707 | [/^\+ /g, '\\+ '], 708 | [/^(=+)/g, '\\$1'], 709 | [/^(#{1,6}) /g, '\\$1 '], 710 | [/`/g, '\\`'], 711 | [/^~~~/g, '\\~~~'], 712 | [/\[/g, '\\['], 713 | [/\]/g, '\\]'], 714 | [/^>/g, '\\>'], 715 | [/_/g, '\\_'], 716 | [/^(\d+)\. /g, '$1\\. '] 717 | ]; 718 | 719 | function TurndownService (options) { 720 | if (!(this instanceof TurndownService)) return new TurndownService(options) 721 | 722 | var defaults = { 723 | rules: rules, 724 | headingStyle: 'setext', 725 | hr: '* * *', 726 | bulletListMarker: '*', 727 | codeBlockStyle: 'indented', 728 | fence: '```', 729 | emDelimiter: '_', 730 | strongDelimiter: '**', 731 | linkStyle: 'inlined', 732 | linkReferenceStyle: 'full', 733 | br: ' ', 734 | blankReplacement: function (content, node) { 735 | return node.isBlock ? '\n\n' : '' 736 | }, 737 | keepReplacement: function (content, node) { 738 | return node.isBlock ? '\n\n' + node.outerHTML + '\n\n' : node.outerHTML 739 | }, 740 | defaultReplacement: function (content, node) { 741 | return node.isBlock ? '\n\n' + content + '\n\n' : content 742 | } 743 | }; 744 | this.options = extend({}, defaults, options); 745 | this.rules = new Rules(this.options); 746 | } 747 | 748 | TurndownService.prototype = { 749 | /** 750 | * The entry point for converting a string or DOM node to Markdown 751 | * @public 752 | * @param {String|HTMLElement} input The string or DOM node to convert 753 | * @returns A Markdown representation of the input 754 | * @type String 755 | */ 756 | 757 | turndown: function (input) { 758 | if (!canConvert(input)) { 759 | throw new TypeError( 760 | input + ' is not a string, or an element/document/fragment node.' 761 | ) 762 | } 763 | 764 | if (input === '') return '' 765 | 766 | var output = process.call(this, new RootNode(input)); 767 | return postProcess.call(this, output) 768 | }, 769 | 770 | /** 771 | * Add one or more plugins 772 | * @public 773 | * @param {Function|Array} plugin The plugin or array of plugins to add 774 | * @returns The Turndown instance for chaining 775 | * @type Object 776 | */ 777 | 778 | use: function (plugin) { 779 | if (Array.isArray(plugin)) { 780 | for (var i = 0; i < plugin.length; i++) this.use(plugin[i]); 781 | } else if (typeof plugin === 'function') { 782 | plugin(this); 783 | } else { 784 | throw new TypeError('plugin must be a Function or an Array of Functions') 785 | } 786 | return this 787 | }, 788 | 789 | /** 790 | * Adds a rule 791 | * @public 792 | * @param {String} key The unique key of the rule 793 | * @param {Object} rule The rule 794 | * @returns The Turndown instance for chaining 795 | * @type Object 796 | */ 797 | 798 | addRule: function (key, rule) { 799 | this.rules.add(key, rule); 800 | return this 801 | }, 802 | 803 | /** 804 | * Keep a node (as HTML) that matches the filter 805 | * @public 806 | * @param {String|Array|Function} filter The unique key of the rule 807 | * @returns The Turndown instance for chaining 808 | * @type Object 809 | */ 810 | 811 | keep: function (filter) { 812 | this.rules.keep(filter); 813 | return this 814 | }, 815 | 816 | /** 817 | * Remove a node that matches the filter 818 | * @public 819 | * @param {String|Array|Function} filter The unique key of the rule 820 | * @returns The Turndown instance for chaining 821 | * @type Object 822 | */ 823 | 824 | remove: function (filter) { 825 | this.rules.remove(filter); 826 | return this 827 | }, 828 | 829 | /** 830 | * Escapes Markdown syntax 831 | * @public 832 | * @param {String} string The string to escape 833 | * @returns A string with Markdown syntax escaped 834 | * @type String 835 | */ 836 | 837 | escape: function (string) { 838 | return escapes.reduce(function (accumulator, escape) { 839 | return accumulator.replace(escape[0], escape[1]) 840 | }, string) 841 | } 842 | }; 843 | 844 | /** 845 | * Reduces a DOM node down to its Markdown string equivalent 846 | * @private 847 | * @param {HTMLElement} parentNode The node to convert 848 | * @returns A Markdown representation of the node 849 | * @type String 850 | */ 851 | 852 | function process (parentNode) { 853 | var self = this; 854 | return reduce.call(parentNode.childNodes, function (output, node) { 855 | node = new Node(node); 856 | 857 | var replacement = ''; 858 | if (node.nodeType === 3) { 859 | replacement = node.isCode ? node.nodeValue : self.escape(node.nodeValue); 860 | } else if (node.nodeType === 1) { 861 | replacement = replacementForNode.call(self, node); 862 | } 863 | 864 | return join(output, replacement) 865 | }, '') 866 | } 867 | 868 | /** 869 | * Appends strings as each rule requires and trims the output 870 | * @private 871 | * @param {String} output The conversion output 872 | * @returns A trimmed version of the ouput 873 | * @type String 874 | */ 875 | 876 | function postProcess (output) { 877 | var self = this; 878 | this.rules.forEach(function (rule) { 879 | if (typeof rule.append === 'function') { 880 | output = join(output, rule.append(self.options)); 881 | } 882 | }); 883 | 884 | return output.replace(/^[\t\r\n]+/, '').replace(/[\t\r\n\s]+$/, '') 885 | } 886 | 887 | /** 888 | * Converts an element node to its Markdown equivalent 889 | * @private 890 | * @param {HTMLElement} node The node to convert 891 | * @returns A Markdown representation of the node 892 | * @type String 893 | */ 894 | 895 | function replacementForNode (node) { 896 | var rule = this.rules.forNode(node); 897 | var content = process.call(this, node); 898 | var whitespace = node.flankingWhitespace; 899 | if (whitespace.leading || whitespace.trailing) content = content.trim(); 900 | return ( 901 | whitespace.leading + 902 | rule.replacement(content, node, this.options) + 903 | whitespace.trailing 904 | ) 905 | } 906 | 907 | /** 908 | * Determines the new lines between the current output and the replacement 909 | * @private 910 | * @param {String} output The current conversion output 911 | * @param {String} replacement The string to append to the output 912 | * @returns The whitespace to separate the current output and the replacement 913 | * @type String 914 | */ 915 | 916 | function separatingNewlines (output, replacement) { 917 | var newlines = [ 918 | output.match(trailingNewLinesRegExp)[0], 919 | replacement.match(leadingNewLinesRegExp)[0] 920 | ].sort(); 921 | var maxNewlines = newlines[newlines.length - 1]; 922 | return maxNewlines.length < 2 ? maxNewlines : '\n\n' 923 | } 924 | 925 | function join (string1, string2) { 926 | var separator = separatingNewlines(string1, string2); 927 | 928 | // Remove trailing/leading newlines and replace with separator 929 | string1 = string1.replace(trailingNewLinesRegExp, ''); 930 | string2 = string2.replace(leadingNewLinesRegExp, ''); 931 | 932 | return string1 + separator + string2 933 | } 934 | 935 | /** 936 | * Determines whether an input can be converted 937 | * @private 938 | * @param {String|HTMLElement} input Describe this parameter 939 | * @returns Describe what it returns 940 | * @type String|Object|Array|Boolean|Number 941 | */ 942 | 943 | function canConvert (input) { 944 | return ( 945 | input != null && ( 946 | typeof input === 'string' || 947 | (input.nodeType && ( 948 | input.nodeType === 1 || input.nodeType === 9 || input.nodeType === 11 949 | )) 950 | ) 951 | ) 952 | } 953 | 954 | return TurndownService; 955 | 956 | }()); 957 | --------------------------------------------------------------------------------