Apple Event: May 7th at 7 am PT

Looks like no one’s replied in a while. To start the conversation again, simply ask a new question.

Find text, remove brackets, and convert to superscript

Apologies if this was not posted in the appropriate Community. There are none for Automator or Applescript and so this one seemed most relevant.


I have a Numbers spreadsheet wherein there are many cells containing text adjacent to other text, delimited by brackets (e.g., abc[123]). I want to create an Automator service, or Applescript app (still pretty new to this, so I am not sure which would be more applicable to my needs), that will find the brackets and text between the brackets (e.g., [123]), remove the brackets (e.g., 123), and convert the text within the brackets to a superscript (e.g.,123), with the final output being "abc123" (and yes, I at least knew how to use the HTML editor to create that superscript with the <sup></sup> tags!).


Any help would be greatly appreciated. Thank you.

MacBook Pro (Retina, 13-inch, Mid 2014), OS X Yosemite (10.10.2)

Posted on Mar 28, 2015 3:37 PM

Reply
30 replies

Apr 2, 2015 12:19 AM in response to rjpalumbo24

Hello


You might try the following script using rubycocoa, which will get rtf data from clipboard and put edited rtf data back to clipboard. In order to use it, copy source range in Numbers, run the script and paste result to destination range in Numbers.


E.g.,


User uploaded file



Tested with Numbers v2.0.5 under OS X 10.6.8.


Under OS X 10.10, you need to manually install RubyCocoa 1.2.0 which supports Ruby 2.0 or later.


http://rubycocoa.sourceforge.net/

http://sourceforge.net/projects/rubycocoa/files/RubyCocoa/1.2.0/


and change ruby interpreter in script to:


/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby





#!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby require 'osx/cocoa' include OSX SUPERSCRIPT_FACTOR = 1.0 SUPERSCRIPT_NESTED = true def indices(s, q) # string s, q : source string, query string # return array : indices of q in s pp = [] tr = NSMakeRange(0, s.length) while tr.length > 0 r = s.objc_send( :rangeOfString, q, :options, NSLiteralSearch, :range, tr) # only use r.location, for r.length can be invalid (e.g., <= 0). # check if r.location is valid, for r.location can be invalid (e.g., < 0 or >= s.length) break if r.location < 0 or r.location >= s.length # NSNotFound should work but not actually. pp << r.location tr = NSMakeRange(r.location + q.length, s.length - r.location - q.length) end pp end def blocks(pp, qq) # array pp : array of integers sorted in ascending order (e.g. offsets of block start tag in text) # array qq : list of integers sorted in ascending order (e.g. offsets of block end tag in text) # return array : array representing block structure, such that - # each element is an array whose first and last elements represent block start and end offsets respectively, # while middle elements, if any, represent nested blocks. # # * Note # A: atomic block = [p, q]; p = start offset, q = end offset # G: general block (with any nested blocks) = [p] + [(G|A)*] + [q]; (G|A)* = any occurences of G or A # Block structure = [(G|A)*];. array of any occurences of G or A # # e.g., # pp = [2, 9, 13, 21, 33] # qq = [18, 26, 27, 31, 36] # return = [[2, [9, [13, 18], [21, 26], 27], 31], [33, 36]] ix, jx = pp.length, qq.length rr = [] rr.push pp[0] if ix > 0 i, j = 1, 0 while j < jx p, q = pp[i], qq[j] if p and p < q rr.push p i += 1 else a = [q] 1 while (r = rr.pop and a.unshift r and r.is_a? Array) a = a.first unless r rr.push a j += 1 end end rr end # # get rtf data from pboard # pboard = NSPasteboard.generalPasteboard pbtype = pboard.availableTypeFromArray(['public.rtf']) exit if pbtype == nil rtf = pboard.dataForType(pbtype) docattr = OCObject.new mas = NSMutableAttributedString.alloc.objc_send( :initWithRTF, rtf, :documentAttributes, docattr) # # get block structure defined by [ and ] # s = mas.string pp = indices(s, '[') qq = indices(s, ']') bb = blocks(pp, qq) # # apply superscript to detected blocks # bb.reverse.each do |b| p, q = b[0], b[-1] r = NSMakeRange(p, q - p + 1) # reduce font size in r if SUPERSCRIPT_FACTOR > 0.0 and SUPERSCRIPT_FACTOR < 1.0 tr = r.dup while tr.length > 0 er = NSRange.new attr = mas.objc_send( :attribute, NSFontAttributeName, :atIndex, tr.location, :longestEffectiveRange, er, :inRange, tr) font = NSFont.objc_send( :fontWithDescriptor, attr.fontDescriptor, :size, attr.pointSize * SUPERSCRIPT_FACTOR) mas.objc_send( :addAttributes, {NSFontAttributeName => font}, :range, er) tr = NSMakeRange(NSMaxRange(er), tr.length - er.length) end end # superscript r mas.superscriptRange(r) # superscript nested ranges recursively if SUPERSCRIPT_NESTED u = b while u = u.length > 2 ? u[1] : nil i, j = u[0], u[-1] mas.superscriptRange(NSMakeRange(i, j - i + 1)) end end # delete [ and ] in r b.flatten.reverse.each do |k| mas.deleteCharactersInRange(NSMakeRange(k, 1)) end end mas.fixAttributesInRange(NSMakeRange(0, mas.length)) # # create rtf # rtf1 = mas.objc_send( :RTFFromRange, NSMakeRange(0, mas.length), :documentAttributes, docattr) # # put rtf data in pboard # pbtype = 'public.rtf' pboard.objc_send( :declareTypes, [pbtype], :owner, nil) pboard.objc_send( :setData, rtf1, :forType, pbtype) exit




Notes.


• When I paste the resulting rtf data to TextEdit rtf document, everything is fine and as expected.


• However, when I paste the resulting rtf data to Numbers v2 (2.0.5) document or Pages v4 (4.0.5) document, some text may be incorrectly pasted. It appears these applications are doing something quite strange in pasting rtf data – for instance, a) some base text followed by superscript text may be pasted as all superscript text, b) superscript font size is arbitrarily reduced from the source size even if the size is already reduced in superscript. Especially a) is problematic and I have no workaround. E.g.,


abc[123]


is processed correctly and pasted correctly whereas


abc[1234]


is processed correctly but pasted wrongly as all superscript.


Some other anomalies.


User uploaded file



It seems lengths of base and superscript text in paragraph are playing some role but after all, it is unpredictable and I'd classify it as bug of Numbers v2 and Pages v4. Later versions may vary.



• SUPERSCRIPT_FACTOR = 1.0, which generates superscript with the same font size as the base text. I set it to 1.0 because Numbers v2 and Pages v4 arbitrarily reduces the superscript font size in pasting rtf data.



• SUPERSCRIPT_NESTED = true, which generates nested superscripts although Numbers v2 and Pages v4 flattens it. You can see what it generates for


a[2[3]]


in TextEdit rtf document.



• It might not work with Numbers v3...



Good luck,

H

Apr 2, 2015 12:21 AM in response to Hiroto

You may run the script via Automator service as follows.


User uploaded file



* Under OS X 10.10, please change ruby interpreter to:


/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby



Code for Run Shell Script action:


/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby <<'EOF' require 'osx/cocoa' include OSX SUPERSCRIPT_FACTOR = 1.0 SUPERSCRIPT_NESTED = true def indices(s, q) # string s, q : source string, query string # return array : indices of q in s pp = [] tr = NSMakeRange(0, s.length) while tr.length > 0 r = s.objc_send( :rangeOfString, q, :options, NSLiteralSearch, :range, tr) # only use r.location, for r.length can be invalid (e.g., <= 0). # check if r.location is valid, for r.location can be invalid (e.g., < 0 or >= s.length) break if r.location < 0 or r.location >= s.length # NSNotFound should work but not actually. pp << r.location tr = NSMakeRange(r.location + q.length, s.length - r.location - q.length) end pp end def blocks(pp, qq) # array pp : array of integers sorted in ascending order (e.g. offsets of block start tag in text) # array qq : list of integers sorted in ascending order (e.g. offsets of block end tag in text) # return array : array representing block structure, such that - # each element is an array whose first and last elements represent block start and end offsets respectively, # while middle elements, if any, represent nested blocks. # # * Note # A: atomic block = [p, q]; p = start offset, q = end offset # G: general block (with any nested blocks) = [p] + [(G|A)*] + [q]; (G|A)* = any occurences of G or A # Block structure = [(G|A)*];. array of any occurences of G or A # # e.g., # pp = [2, 9, 13, 21, 33] # qq = [18, 26, 27, 31, 36] # return = [[2, [9, [13, 18], [21, 26], 27], 31], [33, 36]] ix, jx = pp.length, qq.length rr = [] rr.push pp[0] if ix > 0 i, j = 1, 0 while j < jx p, q = pp[i], qq[j] if p and p < q rr.push p i += 1 else a = [q] 1 while (r = rr.pop and a.unshift r and r.is_a? Array) a = a.first unless r rr.push a j += 1 end end rr end # # get rtf data from pboard # pboard = NSPasteboard.generalPasteboard pbtype = pboard.availableTypeFromArray(['public.rtf']) exit if pbtype == nil rtf = pboard.dataForType(pbtype) docattr = OCObject.new mas = NSMutableAttributedString.alloc.objc_send( :initWithRTF, rtf, :documentAttributes, docattr) # # get block structure defined by [ and ] # s = mas.string pp = indices(s, '[') qq = indices(s, ']') bb = blocks(pp, qq) # # apply superscript to detected blocks # bb.reverse.each do |b| p, q = b[0], b[-1] r = NSMakeRange(p, q - p + 1) # reduce font size in r if SUPERSCRIPT_FACTOR > 0.0 and SUPERSCRIPT_FACTOR < 1.0 tr = r.dup while tr.length > 0 er = NSRange.new attr = mas.objc_send( :attribute, NSFontAttributeName, :atIndex, tr.location, :longestEffectiveRange, er, :inRange, tr) font = NSFont.objc_send( :fontWithDescriptor, attr.fontDescriptor, :size, attr.pointSize * SUPERSCRIPT_FACTOR) mas.objc_send( :addAttributes, {NSFontAttributeName => font}, :range, er) tr = NSMakeRange(NSMaxRange(er), tr.length - er.length) end end # superscript r mas.superscriptRange(r) # superscript nested ranges recursively if SUPERSCRIPT_NESTED u = b while u = u.length > 2 ? u[1] : nil i, j = u[0], u[-1] mas.superscriptRange(NSMakeRange(i, j - i + 1)) end end # delete [ and ] in r b.flatten.reverse.each do |k| mas.deleteCharactersInRange(NSMakeRange(k, 1)) end end mas.fixAttributesInRange(NSMakeRange(0, mas.length)) # # create rtf # rtf1 = mas.objc_send( :RTFFromRange, NSMakeRange(0, mas.length), :documentAttributes, docattr) # # put rtf data in pboard # pbtype = 'public.rtf' pboard.objc_send( :declareTypes, [pbtype], :owner, nil) pboard.objc_send( :setData, rtf1, :forType, pbtype) exit EOF



Regards,

H

Apr 2, 2015 12:14 PM in response to Hiroto

Oops. Previous script contains incorrect code to process nested superscripts.


Script should have considered the number of child superscript blocks in a parent superscript block is not necessarily one.


Also script should have removed only [ and ] actually processed as superscript boundary but indeed it removes every [ and ] at detected boundaries regardless of whether it is processed as superscript boundary.



WRONG:


# superscript r mas.superscriptRange(r) # superscript nested ranges recursively if SUPERSCRIPT_NESTED u = b while u = u.length > 2 ? u[1] : nil i, j = u[0], u[-1] mas.superscriptRange(NSMakeRange(i, j - i + 1)) end end # delete [ and ] in r b.flatten.reverse.each do |k| mas.deleteCharactersInRange(NSMakeRange(k, 1)) end




CORRECT:


# superscript r mas.superscriptRange(r) d = [p, q] # superscript nested ranges recursively if SUPERSCRIPT_NESTED a = b.select { |e| e.is_a? Array } while u = a.shift i, j = u[0], u[-1] mas.superscriptRange(NSMakeRange(i, j - i + 1)) d << i << j a += u.select { |e| e.is_a? Array } end end # delete [ and ] recorded in d d.sort.reverse.each do |k| mas.deleteCharactersInRange(NSMakeRange(k, 1)) end




Corrected script is as follows.



#!/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby # # superscript n in [n] # # v0.21 # written by Hiroto, 2015-04 # require 'osx/cocoa' include OSX SUPERSCRIPT_FACTOR = 1.0 SUPERSCRIPT_NESTED = true def indices(s, q) # string s, q : source string, query string # return array : indices of q in s pp = [] tr = NSMakeRange(0, s.length) while tr.length > 0 r = s.objc_send( :rangeOfString, q, :options, NSLiteralSearch, :range, tr) # only use r.location, for r.length can be invalid (e.g., <= 0). # check if r.location is valid, for r.location can be invalid (e.g., < 0 or >= s.length) break if r.location < 0 or r.location >= s.length # NSNotFound should work but not actually. pp << r.location tr = NSMakeRange(r.location + q.length, s.length - r.location - q.length) end pp end def blocks(pp, qq) # array pp : array of integers sorted in ascending order (e.g. offsets of block start tag in text) # array qq : list of integers sorted in ascending order (e.g. offsets of block end tag in text) # return array : array representing block structure, such that - # each element is an array whose first and last elements represent block start and end offsets respectively, # while middle elements, if any, represent nested blocks. # # * Note # A: atomic block = [p, q]; p = start offset, q = end offset # G: general block (with any nested blocks) = [p] + [(G|A)*] + [q]; (G|A)* = any occurences of G or A # Block structure = [(G|A)*];. array of any occurences of G or A # # e.g., # pp = [2, 9, 13, 21, 33] # qq = [18, 26, 27, 31, 36] # return = [[2, [9, [13, 18], [21, 26], 27], 31], [33, 36]] ix, jx = pp.length, qq.length rr = [] rr.push pp[0] if ix > 0 i, j = 1, 0 while j < jx p, q = pp[i], qq[j] if p and p < q rr.push p i += 1 else a = [q] 1 while (r = rr.pop and a.unshift r and r.is_a? Array) a = a.first unless r rr.push a j += 1 end end rr end # # get rtf data from pboard # pboard = NSPasteboard.generalPasteboard pbtype = pboard.availableTypeFromArray(['public.rtf']) exit if pbtype == nil rtf = pboard.dataForType(pbtype) docattr = OCObject.new mas = NSMutableAttributedString.alloc.objc_send( :initWithRTF, rtf, :documentAttributes, docattr) # # get block structure defined by [ and ] # s = mas.string pp = indices(s, '[') qq = indices(s, ']') bb = blocks(pp, qq) # # apply superscript to detected blocks # bb.reverse.each do |b| p, q = b[0], b[-1] r = NSMakeRange(p, q - p + 1) # reduce font size in r if SUPERSCRIPT_FACTOR > 0.0 and SUPERSCRIPT_FACTOR < 1.0 tr = r.dup while tr.length > 0 er = NSRange.new attr = mas.objc_send( :attribute, NSFontAttributeName, :atIndex, tr.location, :longestEffectiveRange, er, :inRange, tr) font = NSFont.objc_send( :fontWithDescriptor, attr.fontDescriptor, :size, attr.pointSize * SUPERSCRIPT_FACTOR) mas.objc_send( :addAttributes, {NSFontAttributeName => font}, :range, er) tr = NSMakeRange(NSMaxRange(er), tr.length - er.length) end end # superscript r mas.superscriptRange(r) d = [p, q] # superscript nested ranges recursively if SUPERSCRIPT_NESTED a = b.select { |e| e.is_a? Array } while u = a.shift i, j = u[0], u[-1] mas.superscriptRange(NSMakeRange(i, j - i + 1)) d << i << j a += u.select { |e| e.is_a? Array } end end # delete [ and ] recorded in d d.sort.reverse.each do |k| mas.deleteCharactersInRange(NSMakeRange(k, 1)) end end mas.fixAttributesInRange(NSMakeRange(0, mas.length)) # # create rtf # rtf1 = mas.objc_send( :RTFFromRange, NSMakeRange(0, mas.length), :documentAttributes, docattr) # # put rtf data in pboard # pbtype = 'public.rtf' pboard.objc_send( :declareTypes, [pbtype], :owner, nil) pboard.objc_send( :setData, rtf1, :forType, pbtype) exit




Regards,

H

Apr 4, 2015 1:16 PM in response to Pierre L.

When running the script with all of its updates, I get something that looks like this, after it finishes any cell in this particular column (B). It somehow makes the row very wide, and then shrinks it again, and then stalls there. If I manually stop the script, and then run it again, it'll skip to column C, then go back to column A (as there are only 3 columns), and once it gets to the end of column B again, it will repeat the same thing.User uploaded file

Apr 4, 2015 3:21 PM in response to rjpalumbo24

Here's what I get on my computer with a table apparently similar to the one you posted:

User uploaded file

I actually can't understand why the script doesn't work on your computer. Just in case, I post it again:


tell application "Numbers"

activate

tell table 1 of sheet 1 of document 1

set theCells to cells whose value contains "[" and formula does not start with "="

repeat with thisCell in theCells

-- Select the cell:

set thisName to name of thisCell

set selection range to range (thisName & ":" & thisName)

delay 0.2 -- adjust if necessary

-- Move the insertion point to the beginning of the cell's content:

tell application "System Events"

keystrokereturnusing {option down} -- ⌥↵

key code 126 using {command down} -- ⌘↑

end tell

-- Format the cell's content:

set P0 to 0 -- initial position of the insertion point

set thisText to value of thisCell

repeat

tell me to set P1 to offsetof "[" inthisText

if P1 = 0 then exit repeat

tell me to set P2 to offsetof "]" inthisText

if P2 < P1 + 2 then exit repeat -- empty square brackets for example

tell application "System Events"

repeat (P1 - P0 - 1) times

key code 124 -- to move the insertion point to the right

end repeat

key code 117 -- to delete the left bracket

repeat (P2 - P1 - 1) times

key code 124 using {shift down} -- ⇧→ to select the next character

end repeat

keystroke "+" using {control down, command down} -- superscript

delay 0.2 -- adjust if necessary

key code 124 -- to move the insertion point to the right

key code 117 -- to delete the right bracket

end tell

set L to length of thisText

if P1 = 1 and P2 = L then

set thisText to text (P1 + 1) through (P2 - 1) of thisText

else if P1 = 1 then

set thisText to text (P1 + 1) through (P2 - 1) of thisText ¬

& text (P2 + 1) through -1 of thisText

else if P2 = L then

set thisText to text 1 through (P1 - 1) of thisText ¬

& text (P1 + 1) through (P2 - 1) of thisText

else

set thisText to text 1 through (P1 - 1) of thisText ¬

& text (P1 + 1) through (P2 - 1) of thisText ¬

& text (P2 + 1) through -1 of thisText

end if

set P0 to P2 - 2 -- new position of the insertion point

end repeat

-- Reselect the cell:

tell application "System Events" to keystrokereturnusing {command down}

repeat until formatted value of thisCell = thisText

end repeat

end repeat

-- Unselect all:

tell application "System Events" to keystroke "a" using {shift down, command down}

end tell

end tell

Apr 4, 2015 3:23 PM in response to Pierre L.

Turns out there was just something up with that spreadsheet/file. When I copied the text into a new Numbers document, it worked quickly and flawlessly. Thank you SO MUCH for your effort. How you came up with all of this is well beyond me. It will be a big help in improving the readability and usability of my spreadsheets.


But now that it strikes me, is there any way to adapt this to use with Pages?

Apr 5, 2015 2:31 AM in response to rjpalumbo24

Most likely you're using wrong code in Automator's Run Shell Script action.


The last code I posted is standalone ruby code, which is not for Run Shell Script action.


For Run Shell Script action, replace the first line:


#!/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby



with:


/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby <<'EOF'



and replace the last line:


exit



with:


exit EOF



* Note that the last EOF is the entire text in the last line.



Regards,

H

Apr 5, 2015 2:13 PM in response to rjpalumbo24

But now that it strikes me, is there any way to adapt this to use with Pages?

The following script should do the trick:


set CCC to "†" -- any character not used anywhere in the document


tell application "Pages"

activate

set theParagraphs to paragraphs of body text of document 1

if "[" is not in theParagraphs as text then return

-- Enter CCC into the Find & Replace window:

tell application "System Events"

tell process "Pages"

repeat until frontmost

end repeat

keystroke "f" using {command down}

repeat until existswindow "Find & Replace"

end repeat

keystroke "a" using {command down}

keystrokeCCC

delay 1 -- essential delay

keystroke "w" using {command down}

repeat while existswindow "Find & Replace"

end repeat

end tell

end tell

tell document 1

set k to 0

repeat with thisText in theParagraphs

set k to k + 1

if thisText contains "[" then

-- Move the insertion point to the beginning of the paragraph:

set paragraphk of body text to CCC & thisText

tell application "System Events"

keystroke "g" using {command down}

delay 0.1 -- adjust if necessary

key code 123 -- to place the insertion point at the beginning of the paragraph

key code 117 -- to delete CCC

end tell

-- Format the paragraph's content:

set P0 to 0 -- initial position of the insertion point

repeat

tell me to set P1 to offsetof "[" inthisText

if P1 = 0 then exit repeat

tell me to set P2 to offsetof "]" inthisText

if P2 < P1 + 2 then exit repeat -- empty square brackets for example

tell application "System Events"

repeat (P1 - P0 - 1) times

key code 124 -- to move the insertion point to the right

end repeat

key code 117 -- to delete the left bracket

repeat (P2 - P1 - 1) times

key code 124 using {shift down} -- ⇧→ to select the next character

end repeat

keystroke "+" using {control down, command down} -- superscript

delay 0.2 -- adjust if necessary

key code 124 -- to move the insertion point to the right

key code 117 -- to delete the right bracket

end tell

set L to length of thisText

if P1 = 1 and P2 = L then

set thisText to text (P1 + 1) through (P2 - 1) of thisText

else if P1 = 1 then

set thisText to text (P1 + 1) through (P2 - 1) of thisText ¬

& text (P2 + 1) through -1 of thisText

else if P2 = L then

set thisText to text 1 through (P1 - 1) of thisText ¬

& text (P1 + 1) through (P2 - 1) of thisText

else

set thisText to text 1 through (P1 - 1) of thisText ¬

& text (P1 + 1) through (P2 - 1) of thisText ¬

& text (P2 + 1) through -1 of thisText

end if

set P0 to P2 - 2 -- new position of the insertion point

end repeat

repeat until paragraphk of body text = thisText

end repeat

end if

end repeat

-- Unselect all:

tell application "System Events" to keystroke "a" using {shift down, command down}

end tell

end tell

Let me know if the script doesn't work as expected.

Find text, remove brackets, and convert to superscript

Welcome to Apple Support Community
A forum where Apple customers help each other with their products. Get started with your Apple ID.