Converting one or more links to Markdown links with title?

I often have a link or a number of links that I'd like to massage into Markdown links. An example would be:

https://some.tld/some/path/here/file%20name_goes-here.ext
https://another.tld/some/path/here/file%20name_goes-here.ext
https://and-so-forth.tld/some/path/here/file%20name_goes-here.ext

I'd like to convert these to:

[file name 1 goes here.ext](https://some.tld/some/path/here/file%20name+1_goes-here.ext)
[file name 2 goes here.ext](https://another.tld/some/path/here/file%20name_2_goes-here.ext)
[file name 3 goes here.ext](https://and-so-forth.tld/some/path/here/file%20name-3_goes-here.ext)

I think that example is pretty self explanatory, but you can see it will pull the last part of the URL in as the filename (it can ignore other URL arguments after a filename) and replace any normal spacing delimiters like "%20", "-", "_", "+" or maybe others with just a space for the actual text title placed at the front.

Anyone have a regex that would make quick work of this?

1 Like

Here's an example macro based solely on the sample data and desired results you provided to get you started. Feel free to adjust as necessary for your needs.

Convert Links to Markdown.kmmacros (4.0 KB)
image

Results

38%20PM

Regex used to extract file name: \/([^/]+)$

2 Likes

Nice, thanks for this! I'll get back if or when I have followup questions.

1 Like

OK, so I think I see what you're doing. I tried to convert it so that I could run it on a selected chunk of text and have it cut it, modify the text and paste it back out as the list, but it's not getting the new list so far:
example

I'm afraid you're unwittingly assembling the results in a new variable called Clipboard, rather than the clipboard itself, and then not doing anything with that variable once the loop is complete, which is why it's not pasting the results. Make sure that the filter action is filtering the text extracted from the end of the link (the text contained in the LocalLink variable in this example), that the final Set variable action is being set to itself, and to paste that variable containing the results rather than just the clipboard:

06%20PM

and of course, as an alternative to regex, we can always split on "/" and reassemble – using an Execute Script action.

In Applescript for example, composing something from existing functions:

-- mdFromLinkList :: String -> String
on mdFromLinkList(s)
    script mdLink
        on |λ|(x)
            set xs to splitOn("/", strip(x))
            "[" & |last|(xs) & "](" & intercalateS("/", xs) & ")"
        end |λ|
    end script
    
    unlines(map(mdLink, ¬
        |lines|(strip(s))))
end mdFromLinkList

on run
    set strSample to "https://some.tld/some/path/here/file%20name_goes-here.ext\n        https://another.tld/some/path/here/file%20name_goes-here.ext\n        https://and-so-forth.tld/some/path/here/file%20name_goes-here.ext"
    
    mdFromLinkList(strSample)
    
    -- [file%20name_goes-here.ext](https://some.tld/some/path/here/file%20name_goes-here.ext)
    -- [file%20name_goes-here.ext](https://another.tld/some/path/here/file%20name_goes-here.ext)
    -- [file%20name_goes-here.ext](https://and-so-forth.tld/some/path/here/file%20name_goes-here.ext)
end run



-- GENERIC FUNCTIONS ---------------------------------------------------------

-- https://github.com/RobTrew/prelude-applescript

-- dropWhile :: (a -> Bool) -> [a] -> [a]
-- dropWhileEnd :: (Char -> Bool) -> String -> String
on dropWhile(p, xs)
    set lng to length of xs
    set i to 1
    tell mReturn(p)
        repeat while i ≤ lng and |λ|(item i of xs)
            set i to i + 1
        end repeat
    end tell
    if i ≤ lng then
        if class of xs ≠ string then
            items i thru lng of xs
        else
            text i thru lng of xs
        end if
    else
        {}
    end if
end dropWhile


-- dropWhileEnd :: (a -> Bool) -> [a] -> [a]
-- dropWhileEnd :: (Char -> Bool) -> String -> String
on dropWhileEnd(p, xs)
    set i to length of xs
    tell mReturn(p)
        repeat while i > 0 and |λ|(item i of xs)
            set i to i - 1
        end repeat
    end tell
    if i > 0 then
        if class of xs ≠ string then
            items 1 thru i of xs
        else
            text 1 thru i of xs
        end if
    else
        {}
    end if
end dropWhileEnd

-- intercalateS :: String -> [String] -> String
on intercalateS(sep, xs)
    set {dlm, my text item delimiters} to {my text item delimiters, sep}
    set s to xs as text
    set my text item delimiters to dlm
    return s
end intercalateS

-- isSpace :: Char -> Bool
on isSpace(c)
    set i to id of c
    i = 32 or (i ≥ 9 and i ≤ 13)
end isSpace

-- last :: [a] -> a
on |last|(xs)
    item -1 of xs
end |last|

-- lines :: String -> [String]
on |lines|(xs)
    paragraphs of xs
end |lines|

-- Lift 2nd class handler function into 1st class script wrapper 
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    if class of f is script then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- map :: (a -> b) -> [a] -> [b]
on map(f, xs)
    tell mReturn(f)
        set lng to length of xs
        set lst to {}
        repeat with i from 1 to lng
            set end of lst to |λ|(item i of xs, i, xs)
        end repeat
        return lst
    end tell
end map

-- splitOn :: String -> String -> [String]
on splitOn(needle, haystack)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, needle}
    set xs to text items of haystack
    set my text item delimiters to dlm
    return xs
end splitOn

-- strip :: String -> String
on strip(s)
    script isSpace
        on |λ|(c)
            set i to id of c
            i = 32 or (i ≥ 9 and i ≤ 13)
        end |λ|
    end script
    dropWhile(isSpace, dropWhileEnd(isSpace, s))
end strip

-- unlines :: [String] -> String
on unlines(xs)
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, linefeed}
    set str to xs as text
    set my text item delimiters to dlm
    str
end unlines

On in an Execute Javascript for Automation action:

(() => {
    'use strict';

    // mdFromLinkList :: String -> String
    const mdFromLinkList = s =>
        unlines(
            map(x => {
                    const xs = splitOn('/', strip(x));
                    return `[${last(xs)}](${
                        intercalateS('/', xs)
                    })`
                },
                lines(strip(s))
            )
        );


    // MAIN -----------------------------------------------
    const main = () => {

        const strSample = `
        https://some.tld/some/path/here/file%20name_goes-here.ext
        https://another.tld/some/path/here/file%20name_goes-here.ext
        https://and-so-forth.tld/some/path/here/file%20name_goes-here.ext
        `;

        return mdFromLinkList(strSample);
    };

    // GENERIC FUNCTIONS ----------------------------------

    // https://github.com/RobTrew/prelude-jxa

    // intercalateS :: String -> [String] -> String
    const intercalateS = (s, xs) =>
        xs.join(s);

    // last :: [a] -> a
    const last = xs => 0 < xs.length ? xs.slice(-1)[0] : undefined;

    // lines :: String -> [String]
    const lines = s => s.split(/[\r\n]/);

    // map :: (a -> b) -> [a] -> [b]
    const map = (f, xs) => xs.map(f);

    // splitOn :: String -> String -> [String]
    const splitOn = (needle, haystack) =>
        haystack.split(needle);

    // strip :: String -> String
    const strip = s => s.trim();

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

    // MAIN ---
    return main();
})();