Action
Link from selection & clipboard
A cross-platform (Drafts 5 + macOS BBEdit) variant of the Markdown link action.
- Link-wraps any word at a collapsed cursor, or any extended selection.
- Detects and uses any url in the clipboard
( Link detection is based on the Diego Perini © 2010 url regex )
Convert selection (and any URL in clipboard) to Markdown link
Paste
[Markdown](link)
brackets around the selected phrase,( or around any word at the collapsed cursor ),
inserting any url found in the clipboard between the ‘()’ brackets of the MD link.
Either:
- In a Drafts 5 script action, or
- as a JXA script for macOS BBEdit.
BBEdit use requires Ver 0.10 or above of the library at:
https://gist.github.com/RobTrew/675b0f14f87b77ee025755e067022c62
saved on macOS as: ~/Library/Script Libraries/BBDrafts.js
Steps
-
script
(() => { 'use strict'; // 1. Paste [Markdown](link) brackets around the selected phrase, // (or around the word at the collapsed cursor), // 2. inserting any url in the clipboard between the '()' // Either: // // 1. In a Drafts 5 script action for TaskPaper mode, or // 2. as a JXA script for macOS BBEdit. // BBEdit use requires **VER 0.10 or above** of the library at: // https://gist.github.com/RobTrew/675b0f14f87b77ee025755e067022c62 // saved on macOS as: ~/Library/Script Libraries/BBDrafts.js // Rob Trew (c) 2018 // Ver 0.2 // MAIN ----------------------------------------------- // pasteAsLink :: Drafts IO () -> String const pasteAsLink = () => { const e = editor, strLabel = ( expandSelnByWord(e), e.getSelectedText() || '' ), rngSeln = e.getSelectedRange(), strLink = '[' + strLabel + '](' + (() => { const strClip = app.getClipboard(); return isURL(strClip) ? ( strClip ) : ''; })() + ')'; return ( e.setSelectedText(strLink), e.setSelectedRange( rngSeln[0] + ( strLabel.length > 0 ? ( strLink.length ) : 1 ), 0, ) ); }; // EXPANDING SELECTION TO WORD // expandSelnByWord :: () -> IO () const expandSelnByWord = (editor, blnMultiWord, blnLeft) => { const e = editor, tplSeln = e.getSelectedRange(), tplLine = e.getSelectedLineRange(), strLine = e.getTextInRange(...tplLine), intPosn = tplSeln[0], xy = splitAt( intPosn - tplLine[0], strLine ), [dl, dr] = concatMap( x => x !== null ? ( [x[0].length] ) : [0], // [/\b[\S]*$/.exec(xy[0]), /^[\S]*\b/.exec(xy[1])] ); return (tplSeln[1] === 0 || dl > 0 && dr > 0) ? ( e.setSelectedRange( intPosn - dl, // Adjust by one for BBEDIT (not for Drafts) (dl + dr) - (this.editor ? 0 : 1) ), 'extended' ) : blnMultiWord ? [ // additionalWord( // blnLeft, tplSeln, tplLine, strLine // ) ] : 'No further'; }; // LINK DETECTION USING DIEGO PERINI'S REGEX // isURL :: String -> Bool const isURL = s => // // Regular Expression for URL validation // // Author: Diego Perini // Updated: 2010/12/05 // License: MIT // // Copyright (c) 2010-2013 Diego Perini (http://www.iport.it) // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // (new RegExp( "^" + // protocol identifier "(?:(?:https?|ftp)://)" + // user:pass authentication "(?:\\S+(?::\\S*)?@)?" + "(?:" + // IP address exclusion // private & local networks "(?!(?:10|127)(?:\\.\\d{1,3}){3})" + "(?!(?:169\\.254|192\\.168)(?:\\.\\d{1,3}){2})" + "(?!172\\.(?:1[6-9]|2\\d|3[0-1])(?:\\.\\d{1,3}){2})" + // IP address dotted notation octets // excludes loopback network 0.0.0.0 // excludes reserved space >= 224.0.0.0 // excludes network & broacast addresses // (first & last IP address of each class) "(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" + "(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" + "(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" + "|" + // host name "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" + // domain name "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" + // TLD identifier "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" + // TLD may end with dot "\\.?" + ")" + // port number "(?::\\d{2,5})?" + // resource path "(?:[/?#]\\S*)?" + "$", "i" )).test(s); // GENERIC FUNCTIONS ----------------------------- // Tuple (,) :: a -> b -> (a, b) const Tuple = (a, b) => ({ type: 'Tuple', '0': a, '1': b, length: 2 }); // Determines whether all elements of the structure // satisfy the predicate. // all :: (a -> Bool) -> [a] -> Bool const all = (p, xs) => xs.every(p); // concatMap :: (a -> [b]) -> [a] -> [b] const concatMap = (f, xs) => [].concat.apply([], xs.map(f)); // doesFileExist :: FilePath -> IO Bool const doesFileExist = strPath => { const ref = Ref(); return $.NSFileManager.defaultManager .fileExistsAtPathIsDirectory( $(strPath) .stringByStandardizingPath, ref ) && ref[0] !== 1; }; // readFile :: FilePath -> IO String const readFile = strPath => { let error = $(), str = ObjC.unwrap( $.NSString.stringWithContentsOfFileEncodingError( $(strPath) .stringByStandardizingPath, $.NSUTF8StringEncoding, error ) ); return Boolean(error.code) ? ( ObjC.unwrap(error.localizedDescription) ) : str; }; // splitAt :: Int -> [a] -> ([a],[a]) const splitAt = (n, xs) => Tuple(xs.slice(0, n), xs.slice(n)); // LIBRARY IMPORT -------------------------------------- // Evaluate a function f :: (() -> a) // in the context of the JS libraries whose source // filePaths are listed in fps :: [FilePath] // usingLibs :: [FilePath] -> (() -> a) -> a const usingLibs = (fps, f) => all(doesFileExist, fps) ? ( eval(`(() => { 'use strict'; ${fps.map(readFile).join('\n\n')} return (${f})(); })();`) ) : libraryRequest(fps); // libraryRequest :: [FilePath] -> IO [FilePath] const libraryRequest = fps => { const sa = standardSEAdditions(), gaps = concatMap( fp => doesFileExist(fp) ? ( [] ) : [fp], fps ); return ( sa.activate(), sa.displayDialog( `Library not found at: ${gaps.join('\n')}`, { withTitle: 'Library file needed', buttons: ['OK'] } ), gaps ); }; // standardSEAdditions :: () -> Application const standardSEAdditions = () => Object.assign(Application('System Events'), { includeStandardAdditions: true }); // iOS Drafts 5 ? return Boolean(this.editor) ? ( pasteAsLink() // OTHERWISE: // macOS JXA, using VER 0.10 or above of the library at: // https://gist.github.com/RobTrew/675b0f14f87b77ee025755e067022c62 // Saved as ~/Library/Script Libraries/BBDrafts.js ) : usingLibs( [ '~/Library/Script Libraries/BBDrafts.js' ], pasteAsLink ); })();
Options
-
After Success Default Notification Error Log Level Error
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.