Action

Link from selection & clipboard

Posted by @ComplexPoint, Last update over 6 years ago

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

  1. Paste [Markdown](link) brackets around the selected phrase,

    ( or around any word at the collapsed cursor ),

  2. inserting any url found in the clipboard between the ‘()’ brackets of the MD link.

Either:

  1. In a Drafts 5 script action, 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

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.