More Markdown reference links in BBEdit

My last post described a set of scripts I use to add new reference links to a Markdown document in BBEdit. This one will show the scripts I use to reference links that are already in the document.

There are two situations in which I need to reference a link that’s already in the file being edited:

Either way, the process followed by the script(s) is the same:

  1. Scan the document and collect all the reference links in it.
  2. Present the list of links to me and allow me to choose the one I want.
  3. Add the Markdown code that refers to the chosen link.

I use a set of scripts to do this.

Get the reference links and return the chosen one

This is a Python script that’s essentially the same as a script I wrote about a year ago for TextMate. It scans standard input for reference links, presents the list of links to the user through a dropdown menu, and returns the chosen reference through standard output. I call the script getreflink, and here’s its source code:

python:
 1:  #!/usr/bin/python
 2:  # -*- coding: utf-8 -*-
 3:  
 4:  import sys
 5:  import re
 6:  from subprocess import *
 7:  
 8:  # Set the environment for calling CocoaDialog.
 9:  dialogpath = '/Applications/CocoaDialog.app/Contents/MacOS/'
10:  dialogenv = {'PATH': dialogpath}
11:  
12:  # Utility function for shortening text.
13:  def shorten(str, n):
14:    'Truncate str to no more than n chars'
15:    return str if len(str) <= n else str[:n-1] + '…'
16:  
17:  # Read in the buffer and collect all the reference links.
18:  text = sys.stdin.read()
19:  refs = re.findall(r'^\[([^^\]]+)\]: +(https?://)?(.+)$', text, re.MULTILINE)
20:  
21:  # Display a dialog asking for which link to use.
22:  choices = [ shorten(x[2], 100) for x in refs ]
23:  chosen = Popen(['CocoaDialog', 'standard-dropdown', '--exit-onchange', '--title', 'Previous links', '--text', 'Choose an existing link', '--items'] + choices, stdout=PIPE, env =dialogenv).communicate()[0].split()
24:  
25:  # Print the reference for the chosen link.
26:  if int(chosen[0]) == 1 or int(chosen[0]) == 4:
27:    sys.stdout.write(refs[int(chosen[1])][0])

The list of reference links is constructed in Lines 18-19. When Line 19 finishes, the refs list contains a 3-tuple for each link found. The components of each tuple are

  1. the reference, which is whatever’s in the brackets;
  2. the URL’s http:// or https:// prefix, if there is one (relative links and internal links won’t have one); and
  3. the rest of the URL.

Line 22 then creates a list of the third item of each tuple in refs. The string is truncated to 100 characters if necessary to make it fit better in the dropdown menu presented to the user. I don’t show the http:// part of the URL because it doesn’t really add any information to the list and it takes up space.

Line 23 creates a dialog window with a dropdown menu of all the choices and gets the chosen item from the user. The GUI is handled by CocoaDialog, an application written by Mark Stratman to help programmers add a bit of GUI interaction to their scripts. In the version of this script I wrote for TextMate, I used the copy of CocoaDialog that was bundled with TextMate. Here, I’m assuming the user has installed a copy of CocoaDialog in the system-wide Applications folder. The path to the CocoaDialog binary is given in Line 9, and an environment variable for running CocoaDialog through Python’s subprocess module is set in Line 10.

CocoaDialog dropdown

If the user chooses a link, getreflink returns the reference to that link on standard output. If the user cancels, nothing is returned.

Running getreflink and inserting Markdown code

The AppleScript, called “Old Reference Link”, is what the user calls, either through the BBEdit scripts menu or through a keyboard shortcut. I use a shortcut of ⌃⌥L.1 Here’s the source code:

applescript:
 1:  tell application "BBEdit"
 2:    set myText to contents of front document
 3:    set myRef to do shell script "~/bin/bbstdin " & quoted form of myText & " | ~/bin/getreflink"
 4:    
 5:    if myRef is not "" then
 6:      if length of selection is 0 then
 7:        -- Add link with empty text and set the cursor between the brackets.
 8:        set curPt to characterOffset of selection
 9:        select insertion point before character curPt of front document
10:        set selection to "[][" & myRef & "]"
11:        select insertion point after character curPt of front document
12:        
13:      else
14:        -- Turn selected text into link and put cursor after the reference.
15:        add prefix and suffix of selection prefix "[" suffix "]" & "[" & myRef & "]"
16:        select insertion point after last character of selection
17:      end if
18:    end if
19:    
20:  end tell

Lines 2-3 use the same trick I used in my earlier post to convert carriage returns to linefeeds and pipe the contents of a BBEdit document to a Unix script. The bbstdin script was described in that post, and I won’t repeat the description here.

Line 5 checks to see if the return value of the call to getreflink was empty. If it was, nothing happens and the AppleScript returns without doing anything to the BBEdit document. If it wasn’t, the Markdown code referring to the chosen link is inserted using the same logic (and the same AppleScript code) as in my previous post:

Notes


  1. BBEdit already uses ⌃⌥L to bring up the Language menu from the Status Bar, and old-time BBEdit users may prefer to keep it that way. To me, ⌃⌥L makes more sense for Old Reference Link, so I redefined the Language menu shortcut to ⌃⌥⌘L. 


8 Responses to “More Markdown reference links in BBEdit”

  1. Simon Jones says:

    Thanks very much for sharing this Dr. Drang. Would it be possible to use the scripts directly as a BBEdit Text Filter rather than running them through AppleScript?

  2. Simon Jones says:

    Just realised that the cursor placement wouldn’t work without the AppleScript, sorry for the daft question!

  3. Ryan Gray says:

    The other script works fine in TextWrangler (by changing the “tell” command), but this one is giving me an error on line 23 of getreflink.

    Traceback (most recent call last):
      File "/Users/ryan/bin/getreflink", line 23, in <module>
        chosen = Popen(['CocoaDialog', 'standard-dropdown', '--exit-onchange' '--title', 'Previous links', '--text', 'Choose an existing link', '--items'] + choices, stdout=PIPE, env =dialogenv).communicate()[0].split()
      File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 679, in __init__
    errread, errwrite)
      File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1228, in _execute_child
        raise child_exception
    OSError: [Errno 2] No such file or directory
    
  4. Dr. Drang says:

    Ryan,
    I would say that either you don’t have CocoaDialog installed or you have it installed somewhere other than /Applications. Follow the link if you need to install it or change Line 9 of getreflink if you have it somewhere else.

  5. Ryan Gray says:

    Okay, got it. Should have seen that. Works fine now.

  6. Ryan Gray says:

    Also, the listing is missing a comma between '--exit-onchange' and '--title' in line 23 of getreflink.

  7. Ryan Gray says:

    That fix makes both options start working, and the --exit-onchange will cause the return value for any item other than the first to be 4. So, line 26 should be changed to be:

    if int(chosen[0]) == 1 or int(chosen[0]) == 4:
    
  8. Dr. Drang says:

    Ryan,
    Thanks for finding and fixing that bug. I’ve incorporated your changes in the code listing in the post.

    The bug is an old one. It was carried over from the similar TextMate command I wrote about a year ago. I’m not sure why I included the --exit-onchange switch in the CocoaDialog call but never noticed that it wasn’t doing its job. Now I don’t have to hit the Return key twice (once to select the item, once to say OK) to make a link.