Applescript create URL from text

Turns out that Mail.app has the ability to open a specific email message based on a URL. I can assemble an anchor tag in a web page

link

and it works like a charm.

I have an AppleScript service that copies elements of a message into OmniOutliner. I want to turn it into a URL so I can click on it and have failed.

When I construct the URL as a string like the above, it lands in OO as a string and is not clickable.

When I try to construct something “as URL” (a real AppleScript type), it errors.

None of these work

36 PM

This would be a HUGE WIN for productivity if it could be solved.

Any advice?

IIRC, the ‘is a link’ attribute is something we get from the underlying Rich Text format, so my starting assumption was that this was something that could be accomplished by working that angle.

From a few minutes looking at OmniOutliner and TextEdit’s script dictionaries, I’m beginning to think it’s a feature request. Both offer rich text classes, but I don’t see an obvious way to link-ify something.

(At the very least, if it does turn out to be possible, it could be easier/more obvious. Will file!)

Boy that would be great. Right now I’m thinking of making a service that uses a browser to copy then paste. Ugly.

If you wrap the link in HTML you can then convert that to RTF and paste into an OO cell by shelling out to textutil,

See, for example:

or by using the NSAttributedString interface:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

-- rtfFromHTML :: String -> Either String String
on rtfFromHTML(strHTML)
    set ca to current application
    set s to ca's NSString's stringWithString:strHTML
    set d to (s)'s dataUsingEncoding:(ca's NSUTF8StringEncoding)
    
    set attStr to ca's NSAttributedString's alloc()'s initWithHTML:d documentAttributes:(missing value)
    if attStr is missing value then
        |Left|("String could not be parsed as HTML")
    else
        set {rtfData, err} to attStr's ¬
            dataFromRange:{location:0, |length|:attStr's |length|()} ¬
                documentAttributes:{DocumentType:"NSRTF"} ¬
                |error|:(reference)
        
        if (missing value = rtfData) or (missing value is not err) then
            |Left|(err's localizedDescription() as text)
        else
            |Right|((ca's NSString's alloc()'s ¬
                initWithData:rtfData encoding:(ca's NSUTF8StringEncoding)) as text)
        end if
    end if
end rtfFromHTML

-- Left :: a -> Either a b
on |Left|(x)
    {type:"Either", |Left|:x, |Right|:missing value}
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    {type:"Either", |Left|:missing value, |Right|:x}
end |Right|

-- bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
on bindLR(m, mf)
    if missing value is not |Right| of m then
        mReturn(mf)'s |λ|(|Right| of m)
    else
        m
    end if
end bindLR

or, using JS:

// rtfFromHTML :: String -> Either String String
const rtfFromHTML = strHTML => {
    const
        as = $.NSAttributedString.alloc
        .initWithHTMLDocumentAttributes($(strHTML)
            .dataUsingEncoding($.NSUTF8StringEncoding), 0
        );
    return bindLR(
        typeof as
        .dataFromRangeDocumentAttributesError !== 'function' ? (
            Left('String could not be parsed as HTML')
        ) : Right(as),

        // Function bound if Right value obtained above:
        htmlAS => {
            let error = $();
            const rtfData = htmlAS
                .dataFromRangeDocumentAttributesError({
                        'location': 0,
                        'length': htmlAS.length
                    }, {
                        DocumentType: 'NSRTF'
                    },
                    error
                );
            return Boolean(ObjC.unwrap(rtfData) && !error.code) ? Right(
                ObjC.unwrap($.NSString.alloc.initWithDataEncoding(
                    rtfData,
                    $.NSUTF8StringEncoding
                ))
            ) : Left(ObjC.unwrap(error.localizedDescription));
        }
    );
};

// Left :: a -> Either a b
const Left = x => ({
    type: 'Either',
    Left: x
});

// Right :: b -> Either a b
const Right = x => ({
    type: 'Either',
    Right: x
});

// bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
const bindLR = (m, mf) =>
    undefined !== m.Left ? (
        m
    ) : mf(m.Right);
2 Likes

Try this. It’s also derived from the ‘Selecting text and…’ post. It will deposit the subject of a message (and a link to it) into the first empty child of the first topic. I hope this is basically what you’re looking for.

I got the ‘message’ URL format from this page: ‘message:’ URLs in Leopard Mail
There are several variants. I picked the second one. You can read why there.

tell application "Mail"

	-- pick a message, any message
third message of mailbox 1 of account 2
set targetMess to third message of mailbox 1 of account 2

-- get subject and message ID
set subLine to subject of targetMess
set messID to message id of targetMess
	
	-- create message URL for desired email
	set fUrl to "message://%3c" & messID & "%3e"

end tell
tell application "OmniOutliner"

	-- a place to save the link
	set d1 to document 1
	set c1 to first child of child 1 of d1 whose topic is ""
	
	-- make (subject and message url) into (topic with link)
	set topic of c1 to subLine
	set c1's topic's style's attribute "link"'s value to fUrl
	
end tell

Awesomeness Galore!!

This works:

	set currentItem to make new row at first row of front document with properties {topic:("email link" as text)}
	set currentItem's topic's style's attribute "link"'s value to "message://<" & theMessageId & ">" as text

Thank you all for your help.