Search code examples
macospasteplaintext

Apple script for an automator service for pasting plain text in quotation marks


I'm new to MacOS but I could not find any solution so here it is what I need: I work with a lot of pdf-s while writing and I need to use a lot of quotations. I need an automator service for macos sierra with the following function: When I copy a text from PDF I need to paste it to a word processor (mainly scrivener but preferably to anywhere else too). The trick: I need to paste it as plain text to match formatting of the receiving document. I also need to get rid of all line breaks. And I need quotation marks at the beginning and at the end of the pasted text.

Thanks for any suggestions!


Solution

  • Here's the documentation on how to make a systemwide service using Automator. You'll want to read and follow its instructions, which goes through a step-by-step example of how to take some selected text and change it's case to uppercase, or what not.

    So the method won't be too far from what you'll need to employ to achieve your outcome.

    You've mentioned using Scrivener, but implicitly assumed that we all use it too. I, personally, don't, so I have no idea what Automator actions are supplied with it (if any), nor whether it is AppleScriptable or not. Therefore, I'm already limited by what I can assist you with, so I'm going to focus on using another application in this example.

    When I copy a text from PDF I need to paste it to a word processor

    So, your Automator service will take selected text, which can be from any application or limited to a specific application of your choice, such as Preview or some other PDF document viewer.

    The bulk of the Automator workflow will then be scripted, which I predominantly do with AppleScript. So I would insert a Run AppleScript action as the first action, which will receive the selected text.

    This will be sent through and stored in the variable input of the run handler:

        on run {input, parameters}
    

    I need to paste it as plain text

    So the first line of the script is probably going to be:

            set input to input as text
    

    It's probably premature and almost definitely redundant, as there'll be manipulations of the variable's contents in a moment. But, it's also useful to have it explicitly declared just for when one reads the script back to themselves.

    I need the line breaks to be replaced with a space

    i.e. No paragraphs, just one, single block of prose, like your question.

            set the text item delimiters to {space, linefeed, return}
            set input to the text items of input as text
    

    I need quotation marks at the beginning and at the end of the pasted text.

            set input to [quote, input, quote] as text
    

    Here's the workflow so far:

    Automator Workflow: Paste As Quote

    From here, there are a couple of options with regards to what you can do:

    ① Put the reformatted text onto the clipboard, as you see I've done there, and then allow the user to simply move into whichever application they wish and paste the text themselves. If this is what you elect for, then you're finised, and the above screenshot is your completed service, which—once saved—will be accessible from the services menus in whichever application you pick (or any application if you leave it).

    But, if you want to go on...

    ② Get the workflow to open a scriptable application and set the contents of the document to the contents of our input variable. I'll demonstrate this with Apple's TextEdit.

    ③ Get the workflow to open any regular (non-scriptable) application and use a rather inelegant technique of imposing control over certain elements of the application in a limited fashion. This is slightly less robust than ②, but if it's tested well on a single application that you'll always be using, there's no reason it won't be reliable.


    Insert into a scriptable app, e.g. TextEdit.

    It turned out to be slightly more involved that I had hoped. Whilst being scriptable does make it a lot easier, there were specific AppleScript properties missing from the likes of TextEdit and even Apple's Pages, which meant that regular scripting got me about half-way to where I was aiming for, so had to use some of that inelegance I mentioned in order to get the rest of the way there. Therefore, I won't bore you with going through how it works. But here's the portion of the script responsible for getting the highlighted input text into TextEdit:

        script TextEdit
            use application "System Events"
            use T : application "TextEdit"
            
            property parent : this
            
            
            to receiveTheText:(plainText as string)
                activate T
                
                repeat until name of processes ¬
                    contains "TextEdit"
                    delay 1
                end repeat
                
                if not (exists document 1 of T) then
                    tell T to set D to make new document
                else
                    set D to T's first document
                end if
                
                set [a, b] to the value of ¬
                    attribute "AXSelectedTextRange" of ¬
                    text area 1 of ¬
                    scroll area 1 of ¬
                    window 1 of ¬
                    process "TextEdit"
                
                set character a of D's text to plainText & linefeed
            end receiveTheText:
        end script
    

    However, it works surprisingly well, preserving the local formatting of the TextEdit document, and matching the incoming text to the local styling:

    Paste As Quote (TextEdit)

    Insert into a non-scriptable app, e.g. Scrivener

    So, I went ahead and downloaded a trial version of Scrivener. Sadly, it isn't AppleScript-able, so this limits us in some respects. Namely, whilst TextEdit doesn't have to be open because the Automator service can open the app and create a new document if need be, Scrivener does need to an open project ready to receive the text. I imagine this isn't too much of an inconvenience, but from a scripting point-of-view, it was a much tougher script to write and it has the potential to stop working properly if Scrivener decide to change their program.

    Here's the script for Scrivener:

        script Scrivener
            use scripting additions
            
            property parent : this
            
            
            to receiveTheText:(plainText as string)
                tell application "System Events" to ¬
                    if not (exists process "Scrivener") then ¬
                        return display alert ¬
                            "Application not running." & ¬
                            linefeed & linefeed & ¬
                            "When using this service with Scrivener, an open " & ¬
                            "project is required into which the text can be " & ¬
                            "inserted.  AppleScript is unable to create a project " & ¬
                            "for you." as critical
                
                tell application "Scrivener" to activate
                
                tell application "System Events"
                    
                    tell process "Scrivener" to set _P to ¬
                        a reference to (first window whose ¬
                            name of attributes contains "AXDocument" and ¬
                            value of attribute "AXDocument" contains ".scriv")
                    
                    if not (exists _P) then ¬
                        return display alert ¬
                            "No open documents." & ¬
                            linefeed & linefeed & ¬
                            "Please create a project and try again." as critical
                    
                    repeat until (count _P) = 0
                        set _T to a reference to (UI elements of _P ¬
                            whose role is "AXTextArea")
                        if (count _T) ≠ 0 then exit repeat
                        set _P to a reference to UI elements of _P
                    end repeat
                    
                    set [_T] to _T --> The text area
                    
                    tell _T to set value of attribute "AXSelectedText" to plainText
                    
                end tell
            end receiveTheText:
        end script
    

    Paste As Quote (Scrivener)


    Finally...

    To finish up, you'll want the entire script as a whole. The individual component scriptlets I've pasted above won't do much my themselves. You will need to copy and paste this entire script into the Automator workflow using the Run AppleScript action I showed you earlier.

    There's only one optional variable you might want to change, which is at the very bottom of the script: you'll see two almost-identical lines, one of which is commented out with a preceding #. These dictate whether the service passes the text to TextEdit or to Scrivener. Currently, it's set to go to Scrivener. Just switch the # symbol to the other line should you want to give it a try with TextEdit.

        property parent : AppleScript
        property this : me
        --------------------------------------------------------------------------------
        script TextEdit
            use application "System Events"
            use T : application "TextEdit"
            
            property parent : this
            
            
            to receiveTheText:(plainText as string)
                activate T
                
                repeat until name of processes ¬
                    contains "TextEdit"
                    delay 1
                end repeat
                
                if not (exists document 1 of T) then
                    tell T to set D to make new document
                else
                    set D to T's first document
                end if
                
                set [a, b] to the value of ¬
                    attribute "AXSelectedTextRange" of ¬
                    text area 1 of ¬
                    scroll area 1 of ¬
                    window 1 of ¬
                    process "TextEdit"
                
                set character a of D's text to plainText & linefeed
            end receiveTheText:
        end script
    
        script Scrivener
            use scripting additions
            
            property parent : this
            
            
            to receiveTheText:(plainText as string)
                tell application "System Events" to ¬
                    if not (exists process "Scrivener") then ¬
                        return display alert ¬
                            "Application not running." & ¬
                            linefeed & linefeed & ¬
                            "When using this service with Scrivener, an open " & ¬
                            "project is required into which the text can be " & ¬
                            "inserted.  AppleScript is unable to create a project " & ¬
                            "for you." as critical
                
                tell application "Scrivener" to activate
                
                tell application "System Events"
                    
                    tell process "Scrivener" to set _P to ¬
                        a reference to (first window whose ¬
                            name of attributes contains "AXDocument" and ¬
                            value of attribute "AXDocument" contains ".scriv")
                    
                    if not (exists _P) then ¬
                        return display alert ¬
                            "No open documents." & ¬
                            linefeed & linefeed & ¬
                            "Please create a project and try again." as critical
                    
                    repeat until (count _P) = 0
                        set _T to a reference to (UI elements of _P ¬
                            whose role is "AXTextArea")
                        if (count _T) ≠ 0 then exit repeat
                        set _P to a reference to UI elements of _P
                    end repeat
                    
                    set [_T] to _T --> The text area
                    
                    tell _T to set value of attribute "AXSelectedText" to plainText
                    
                end tell
            end receiveTheText:
        end script
        --------------------------------------------------------------------------------
        on run {input, parameters}
            set input to input as text
            
            set the text item delimiters to {space, linefeed, return}
            set input to the text items of input as text
            set input to [quote, input, quote] as text
            
            # tell TextEdit to receiveTheText:input
            tell Scrivener to receiveTheText:input
        end run
        -----------------------------------------------------------------------------END