How Do I base64 Decode and Encode Multiple Lines?

Hey guys,

I’ve found a few bash commands that do this, but none of them seem to handle encode of multiple lines.

I’m hoping there is a ASObjC solution for this. Can anyone point me in the right direction?

I could also use help in extracting the encoded string from a XML string I get from an Keyboard Maestro Action object. It looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Action</key>
	<string>DisplayWindow</string>
	<key>MacroActionType</key>
	<string>InsertText</string>
	<key>StyledText</key>
	<data>
	cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC5WAQAAKwAAAAEAAABOAQAAe1xy
	dGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYxNDA0XGNvY29hc3VicnRmNDcwCntc
	Zm9udHRibFxmMFxmbmlsXGZjaGFyc2V0MCBIZWx2ZXRpY2FOZXVlO30Ke1xjb2xvcnRi
	bDtccmVkMjU1XGdyZWVuMjU1XGJsdWUyNTU7fQpccGFyZFx0eDU2MFx0eDExMjBcdHgx
	NjgwXHR4MjI0MFx0eDI4MDBcdHgzMzYwXHR4MzkyMFx0eDQ0ODBcdHg1MDQwXHR4NTYw
	MFx0eDYxNjBcdHg2NzIwXHBhcmRpcm5hdHVyYWxccGFydGlnaHRlbmZhY3RvcjAKClxm
	MFxmczI2IFxjZjAgJVZhcmlhYmxlJVRFU1RfX0VsYXBzZWRUaW1lJVwKVEVTVF9fVmFy
	MjoJJVZhcmlhYmxlJVRFU1RfX1ZhcjIlfQEAAAAjAAAAAQAAAAcAAABUWFQucnRmEAAA
	AJ4Ez1m2AQAAAAAAAAAAAAA=
	</data>
	<key>Text</key>
	<string>%Variable%TEST__ElapsedTime%
TEST__Var2:	%Variable%TEST__Var2%</string>
</dict>
</plist>

The text I need to decode is in the

<key>StyledText</key>
	<data>
       -- rich text base64 encoded
    </data>

So I need to:

  1. extract the rich text base64 encoded from the XML string.
  2. base64 Decode (results in multiple lines)
  3. Make some changes
  4. base64 encode multiple lines – haven’t been able to do this
  5. Update the xml source.

The only part I have not been able to figure out is #4.
I’ve been using RegEx for find, and then replace, but I’m thinking there is probably a better ASObjC dictionary method that I could/should use.

Here is the bash script, but it fails with multiple lines:

set encodedStr to "cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC5LAQAAKwAAAAEAAABDAQAAe1xy
  dGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYxNDA0XGNvY29hc3VicnRmNDcwCntc
  Zm9udHRibFxmMFxmbmlsXGZjaGFyc2V0MCBIZWx2ZXRpY2FOZXVlO30Ke1xjb2xvcnRi
  bDtccmVkMjU1XGdyZWVuMjU1XGJsdWUyNTU7XHJlZDBcZ3JlZW4wXGJsdWUwO30KXHBh
  cmRcdHg1NjBcdHgxMTIwXHR4MTY4MFx0eDIyNDBcdHgyODAwXHR4MzM2MFx0eDM5MjBc
  dHg0NDgwXHR4NTA0MFx0eDU2MDBcdHg2MTYwXHR4NjcyMFxwYXJkaXJuYXR1cmFsXHBh
  cnRpZ2h0ZW5mYWN0b3IwCgpcZjBcZnMyNiBcY2YyIFRFU1RfX1ZhcjM6CSVWYXJpYWJs
  ZSVURVNUX19WYXIzJX0BAAAAIwAAAAEAAAAHAAAAVFhULnJ0ZhAAAABoCs9ZtgEAAAAA
  AAAAAAAA"

--- base64 DECODE THE STRING --
set cmdStr to "echo '" & encodedStr & "' | openssl base64 -d"
set decodedStr to do shell script cmdStr

### In my production script will make changes to decodedStr here ###

--- NOW, base64 ENCODE after my changes ---
-- for testing, just encoding what I just decoded
set cmdStr to "echo -n '" & decodedStr & "' | openssl base64"
set encoded2Str to do shell script cmdStr
-->ERROR:  
(*
sh: -c: line 0: unexpected EOF while looking for matching `''
sh: -c: line 1: syntax error: unexpected end of file
*)

TIA for all help and suggestions.

1 Like

That’s a property list, so I’d expect something like this to work:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

set theString to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
	<key>Action</key>
	<string>DisplayWindow</string>
	<key>MacroActionType</key>
	<string>InsertText</string>
	<key>StyledText</key>
	<data>
	cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC5WAQAAKwAAAAEAAABOAQAAe1xy
	dGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYxNDA0XGNvY29hc3VicnRmNDcwCntc
	Zm9udHRibFxmMFxmbmlsXGZjaGFyc2V0MCBIZWx2ZXRpY2FOZXVlO30Ke1xjb2xvcnRi
	bDtccmVkMjU1XGdyZWVuMjU1XGJsdWUyNTU7fQpccGFyZFx0eDU2MFx0eDExMjBcdHgx
	NjgwXHR4MjI0MFx0eDI4MDBcdHgzMzYwXHR4MzkyMFx0eDQ0ODBcdHg1MDQwXHR4NTYw
	MFx0eDYxNjBcdHg2NzIwXHBhcmRpcm5hdHVyYWxccGFydGlnaHRlbmZhY3RvcjAKClxm
	MFxmczI2IFxjZjAgJVZhcmlhYmxlJVRFU1RfX0VsYXBzZWRUaW1lJVwKVEVTVF9fVmFy
	MjoJJVZhcmlhYmxlJVRFU1RfX1ZhcjIlfQEAAAAjAAAAAQAAAAcAAABUWFQucnRmEAAA
	AJ4Ez1m2AQAAAAAAAAAAAAA=
	</data>
	<key>Text</key>
	<string>%Variable%TEST__ElapsedTime%
TEST__Var2:	%Variable%TEST__Var2%</string>
</dict>
</plist>
"
set theString to current application's NSString's stringWithString:theString
set stringData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:stringData options:0 |format|:(missing value) |error|:(reference)
if theDict is missing value then error (theError's localizedDescription() as text)
set theData to theDict's objectForKey:"StyledText"
set theAttstring to current application's NSKeyedUnarchiver's unarchiveObjectWithData:theData

Sadly, it doesn’t. However, if I run it with:

set theText to current application's NSString's alloc()'s initWithData:theData encoding:(current application's NSASCIIStringEncoding)

I can see the rtf stuff in the data.

It’s possible something’s happened to the data in posting to the forum, so you should try it out there. But if that fails, I suspect your best bet is to ask Peter why he thinks it’s failing. He’s the one putting the data there, so he’ll know exactly what to do to extract it.

Just trying to edit the raw rtf string is a recipe for trouble, IMO.

1 Like

Shane, thanks for the great help.
You script runs fine for me, when I add the last line, which produces the decoded RTF text. What problem were you having?

All I want to change is the KM Variable names embedded in the RTF, which is easy after decoding.

But the main issue I have is how to base64 encode the changed RTF, which has multiple lines. Can you please help with that?

Thanks.

EDIT: I also need to know how to update the dictionary with the changed “StyledText” AFTER it has been base64 encoded, and then convert the dict back to XML.

Thanks.

1 Like

But that’s not really what you want.

What you want is the for the script to work without it. Then reassembling becomes a much simpler job.

1 Like

OK, but if it’s not working then I’ll have to use more conventional methods, like split using:

set AppleScript's text item delimiters to {"<key>StyledText</key>
  <data>", "</data>"}
set xmlParts to text items of actXML

This puts the encoded RTF in item 2.
Your ASObjC script showed me how to decode it.

Can you please show me how to base64 encode a string having multiple lines?

Then I can just rejoin the list.

Thanks.

1 Like

@ShaneStanley, thanks for all your help, but I’m back to my first, basic questions:

  1. How do I base64 decode a string.
  2. How do I base64 encode a string with multiple lines?

I tried to pull just the decoding parts from your above ASObjC script, but it’s not working.
Could you please show me how?

Many thanks.

Here’s my test script:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions


set actXML to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
  <key>Action</key>
  <string>DisplayWindow</string>
  <key>ActionColor</key>
  <string>Red</string>
  <key>ActionName</key>
  <string>❗️ Display Text “TEST__Var3:  %Variable%TEST__Var3%” in Window</string>
  <key>ActionNotes</key>
  <string>UNABLE to Change Variables in Rich Text Area</string>
  <key>MacroActionType</key>
  <string>InsertText</string>
  <key>StyledText</key>
  <data>
  cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC5LAQAAKwAAAAEAAABDAQAAe1xy
  dGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYxNDA0XGNvY29hc3VicnRmNDcwCntc
  Zm9udHRibFxmMFxmbmlsXGZjaGFyc2V0MCBIZWx2ZXRpY2FOZXVlO30Ke1xjb2xvcnRi
  bDtccmVkMjU1XGdyZWVuMjU1XGJsdWUyNTU7XHJlZDBcZ3JlZW4wXGJsdWUwO30KXHBh
  cmRcdHg1NjBcdHgxMTIwXHR4MTY4MFx0eDIyNDBcdHgyODAwXHR4MzM2MFx0eDM5MjBc
  dHg0NDgwXHR4NTA0MFx0eDU2MDBcdHg2MTYwXHR4NjcyMFxwYXJkaXJuYXR1cmFsXHBh
  cnRpZ2h0ZW5mYWN0b3IwCgpcZjBcZnMyNiBcY2YyIFRFU1RfX1ZhcjM6CSVWYXJpYWJs
  ZSVURVNUX19WYXIzJX0BAAAAIwAAAAEAAAAHAAAAVFhULnJ0ZhAAAABoCs9ZtgEAAAAA
  AAAAAAAA
  </data>
  <key>Text</key>
  <string>TEST__Var3:  %Variable%TEST__Var3%</string>
</dict>
</plist>
"


set splitDelim to {"<key>StyledText</key>
  <data>", "</data>"}
set AppleScript's text item delimiters to splitDelim
set xmlParts to text items of actXML
set rtfEncoded to item 2 of xmlParts


set nsStr to current application's NSString's stringWithString:rtfEncoded

set nsDecoded to current application's NSString's alloc()'s initWithData:nsStr encoding:(current application's NSASCIIStringEncoding)
-->ERROR:  -[__NSCFString bytes]: unrecognized selector sent to instance 0x7fa2b8f43060

set rtfDecoded to nsDecoded as text

--- Requires Satimage.osax ---
set rtfDecoded to change "TEST__" into "Local__" in rtfDecoded

### How do I base64 encode rtfDecoded ???
---set rtfEncodedNew to ???

set xmlNew to item 1 of xmlParts & item 1 of splitDelim & ¬
  rtfEncodedNew & item 2 of splitDelim & item 3 of xmlParts

It’s not a matter base64 encoding a string. What’s happening is that some object is being archived by NSKeyedArchiver (or NSArchiver) to serialize it into a block of data. What you’re thinking is a base64 encoded rtf string is actually something that contains the rtf string.

So the correct approach is get the attributed string, modify it how you want, archive it, make a mutable copy of theDict and set the new value for the StyledText key, then create a new property list from that.

Please, see if Peter can answer the question above.

1 Like

In one of my discussions with Peter, he said this:

It is the NSAttributedString archived to RTF and encoded in data format.

If you can help me with the proper solution, that would be great.

But, as a workaround, using base64 decode does work – it gives me the RTF codes in plain text that I can modify.

I have a bash script that will do the decode.
What I’ve been unable to do is base64 encode of multiple lines.

If you can’t figure out the proper solution, it would be really helpful if you could provide the ASObjC script to decode and encode.

Thanks.

BTW, if you, or anyone, is interested in the KM details, and the discussion I’ve had with Peter, see this KM forum thread: XML of Display Text in Window Does Not Update by Script

OK, that means he is doing what I suggested. So my code earlier is working to this point:

set theString to current application's NSString's stringWithString:theString
set stringData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:stringData options:0 |format|:(missing value) |error|:(reference)
if theDict is missing value then error (theError's localizedDescription() as text)
set theData to theDict's objectForKey:"StyledText"

Peter put that data in there by archiving an NSAttributedString. There are two ways to do that: using NSArchiver or NSKeyedArchiver. To undo that, you use NSUnarchiver or NSKeyedUnarchiver. So the next line should be either:

set theAttstring to current application's NSKeyedUnarchiver's unarchiveObjectWithData:theData

or:

set theAttstring to current application's NSUnarchiver's unarchiveObjectWithData:theData

But using the sample you have posted here, both those return missing value. That suggests the data is mangled. It might be happening when you post here, or where you extract it — I don’t know. But that’s the correct code.

If you get it to work, then the rest of your script would be like this (untested, obviously):

-- edit theAttstring however you want, for example
set mutableAttString to theAttstring's mutableCopy()
mutableAttString's replaceCharactersInRange:{0, 3} withString:"?"
-- archive new attributed string to data, using NSKeyedArchiver **or** NSArchiver, depending on what was used above
set theData to current application's NSKeyedArchiver's archivedDataWithRootObject:mutableAttString
-- **or**:
set theData to current application's NSArchiver's archivedDataWithRootObject:mutableAttString
-- make new dictionary
set newDict to theDict's mutableCopy()
newDict's setObject:theData forKey:"StyledText"
-- make new plist from dictionary
set {plistData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:newDict |format|:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference)
set newString to (current application's NSString's alloc()'s initWithData:plistData encoding:(current application's NSUTF8StringEncoding)) as text
1 Like

Shane, many thanks for you revised script.
Unfortunately, when I run it, after pulling the XML directly from the KM Action object, I also get missing value for both lines.

Peter suggested this ObjC to decode/encode the data:

The Objective C is:

Data to plain string:

[[[NSAttributedString alloc] initWithRTFD:data documentAttributes:nil] string];

String to data:

[[[NSAttributedString alloc] initWithString:s] RTFDFromRange:NSMakeRange(0,s.length) documentAttributes:@{}];

Can you please translate that into ASObjC I can use in your script?

BTW, just for reference, here is how I get the XML:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use scripting additions

tell application "Keyboard Maestro"
  set oMacro to item 1 of (get selected macros)
  
  set actionList to actions in oMacro
  set oAction to item 1 in actionList
  
  tell oAction
    set actXML to xml
  end tell -- oAction
  
end tell -- KM

set theString to actXML

set theString to current application's NSString's stringWithString:theString
set stringData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
set {theDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:stringData options:0 |format|:(missing value) |error|:(reference)
if theDict is missing value then error (theError's localizedDescription() as text)
set theData to theDict's objectForKey:"StyledText"

(*
Peter put that data in there by archiving an NSAttributedString. There are two ways to do that: using NSArchiver or NSKeyedArchiver. To undo that, you use NSUnarchiver or NSKeyedUnarchiver. So the next line should be either:
*)

### set theAttstring to current application's NSKeyedUnarchiver's unarchiveObjectWithData:theData
-->missing value

## or:

### set theAttstring to current application's NSUnarchiver's unarchiveObjectWithData:theData
-->missing value
1 Like

Perfect, thanks. So here’s working code:

use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions

set theString to "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
<dict>
	<key>Action</key>
	<string>DisplayWindow</string>
	<key>MacroActionType</key>
	<string>InsertText</string>
	<key>StyledText</key>
	<data>
	cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC5WAQAAKwAAAAEAAABOAQAAe1xy
	dGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYxNDA0XGNvY29hc3VicnRmNDcwCntc
	Zm9udHRibFxmMFxmbmlsXGZjaGFyc2V0MCBIZWx2ZXRpY2FOZXVlO30Ke1xjb2xvcnRi
	bDtccmVkMjU1XGdyZWVuMjU1XGJsdWUyNTU7fQpccGFyZFx0eDU2MFx0eDExMjBcdHgx
	NjgwXHR4MjI0MFx0eDI4MDBcdHgzMzYwXHR4MzkyMFx0eDQ0ODBcdHg1MDQwXHR4NTYw
	MFx0eDYxNjBcdHg2NzIwXHBhcmRpcm5hdHVyYWxccGFydGlnaHRlbmZhY3RvcjAKClxm
	MFxmczI2IFxjZjAgJVZhcmlhYmxlJVRFU1RfX0VsYXBzZWRUaW1lJVwKVEVTVF9fVmFy
	MjoJJVZhcmlhYmxlJVRFU1RfX1ZhcjIlfQEAAAAjAAAAAQAAAAcAAABUWFQucnRmEAAA
	AJ4Ez1m2AQAAAAAAAAAAAAA=
	</data>
	<key>Text</key>
	<string>%Variable%TEST__ElapsedTime%
TEST__Var2:	%Variable%TEST__Var2%</string>
</dict>
</plist>
"
set theString to current application's NSString's stringWithString:theString
-- convert string to data
set stringData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
-- convert plist to mutable dictionary
set {mutableDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:stringData options:(current application's NSPropertyListMutableContainersAndLeaves) |format|:(missing value) |error|:(reference)
if mutableDict is missing value then error (theError's localizedDescription() as text)
-- extract RTFD data and convert to a mutable atributed string
set theData to mutableDict's objectForKey:"StyledText"
set mutableAttString to current application's NSMutableAttributedString's alloc()'s initWithRTFD:theData documentAttributes:(missing value)
set plainString to mutableAttString's |string|()
-- modify the mutable atributed string
-- how you do that depends on what you want to do, obviously
set theRange to plainString's rangeOfString:"TEST__ElapsedTime"
mutableAttString's replaceCharactersInRange:theRange withString:"TEST__SomethingElse"
-- convert back to RTFD data
set theData to mutableAttString's RTFDFromRange:{0, mutableAttString's |length|()} documentAttributes:(missing value)
-- update the dictionary
mutableDict's setObject:theData forKey:"StyledText"
mutableDict's setObject:(mutableAttString's |string|()) forKey:"Text"
-- make new plist from dictionary
set {plistData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:mutableDict |format|:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference)
-- get text version
set newString to (current application's NSString's alloc()'s initWithData:plistData encoding:(current application's NSUTF8StringEncoding)) as text

The part this doesn’t really cover is how to edit the styled text (attributed string), which can be complicated depending on what you want to do. Assuming you don’t want to change the attributes themselves, the methods you’d use are replaceCharactersInRange:withString: and deleteCharactersInRange:. You get the ranges you use based on the unstyled text, as above.

2 Likes

Shane, you are the man​:exclamation:️ :+1:

Your script works perfectly, even updating the styled (rich) text properly.

I just have one more request: How do I do a find and replace ALL occurrences of a string?

These lines work perfectly to update the first occurrence of prefixCur

  -- modify the mutable atributed string
  -- how you do that depends on what you want to do, obviously
  set theRange to plainString's rangeOfString:prefixCur
  mutableAttString's replaceCharactersInRange:theRange withString:prefixNew

How do I update ALL occurrences of prefixCur?

Many, many thanks. You have solved two major problems I was having with reading and updating KM Actions.

@ShaneStanley, OK, I fibbed. :wink:
I do have another request:

I also need to change all occurrences of prefixCur in the plain text parts of the XML. I can do this with a simple Satimage.osax change command on the XML string, but I thought I’d ask you if there is a better way with ASObjC?

I’m really in your debt. If you need anything that I can possibly do to help you (I know, that’s really funny), please don’t hesitate to ask, either here or by email. I’m happy to be your grunt. :wink:

The easiest way is a repeat loop:

repeat
	set theRange to plainString's rangeOfString:"TEST__"
	if |length| of theRange = 0 then exit repeat
	mutableAttString's replaceCharactersInRange:theRange withString:"BLAH__"
end repeat

It may look a bit odd, but plainString is a pointer to the actual string content of the attributed string, so every time you modify the attributed string, you effectively also modify plainString at the same time.

For something more complex you could use NSRegularExpression and replace from back to front.

That’s what this line does:

mutableDict's setObject:(mutableAttString's |string|()) forKey:"Text"

But because of what I said above about plainString, you could actually simplify it to:

mutableDict's setObject:plainString forKey:"Text"
1 Like

OK, great! That works perfectly.

But some KM Actions will NOT have a “StyledText” key, only a “Text” key.
How do I get a mutableAttString reference to that?

I know how to get theData:
set theData to mutableDict's objectForKey:"Text"

It is the next step that’s got me baffled – getting mutableAttString reference.

Thanks again.

Unless you’re trying to add your own styling, you don’t. You need to test for the presence of “StyledText”, and behave accordingly:

[...]
-- extract RTFD data and convert to a mutable atributed string
set theData to mutableDict's objectForKey:"StyledText"
if theData = missing value then -- only plain text
	set plainText to mutableDict's objectForKey:"Text"
	-- edit text
	set plainText to plainText's stringByReplacingOccurrencesOfString:"TEST__" withString:"BLAH__"
else
	set mutableAttString to current application's NSMutableAttributedString's alloc()'s initWithRTFD:theData documentAttributes:(missing value)
	set plainString to mutableAttString's |string|()
	-- modify the mutable atributed string
	-- how you do that depends on what you want to do, obviously
	repeat
		set theRange to plainString's rangeOfString:"TEST__"
		if |length| of theRange = 0 then exit repeat
		mutableAttString's replaceCharactersInRange:theRange withString:"BLAH__"
	end repeat
	-- convert back to RTFD data
	set theData to mutableAttString's RTFDFromRange:{0, mutableAttString's |length|()} documentAttributes:(missing value)
	-- update the dictionary
	mutableDict's setObject:theData forKey:"StyledText"
end if
mutableDict's setObject:plainString forKey:"Text"
-- make new plist from dictionary
[...]
1 Like

OK, that is what I was looking for. I’ll test it and get back to you.

Sorry I’m so ASObjC illiterate. I keep trying, but I seem to be slow in learning this new (for me) language.

That works great Shane! :+1:

Now I need to use that solution and apply it to a broader set of KM Actions.

Hello there, thank you for that work, It helped me a lot to understand. I don’t know ASObjC and I’m wondering If you could help me with and issue : I’m trying to build an XML that contains base64 text, and I build it with a patern. I’d like to be able to edit the text without decoding and encoding it everytime, because I’m loosing information while doing that.
At some point in the XML I have to write something like this :
nAsAAAAAAAB7ACIAbQBUAGUAeAB0AFAAYQByAGEAbQAiADoAewAiAG0AQQBsAGkAZwBuAG0AZQBuAHQAIgA6ADAALAAiAG0AQgBhAGMAawBGAGkAbABsAEMAbwBsAG8AcgAiADoANAAxADQANAA5ADUAOQAsACIAbQBCAGEAYwBrAEYAaQBsAGwATwBwAGEAYwBpAHQAeQAiADoANAA0AC4ANQA1ADQANAA1ADQAOAAwADMANAA2ADYANwA5ADcALAAiAG0AQgBhAGMAawBGAGkAbABsAFMAaQB6AGUAIgA6ADQANgAuADUAMwA0ADYANQAyADcAMAA5ADkANgAwADkAMwA4ACwAIgBtAEIAYQBjAGsARgBpAGwAbABWAGkAcwBpAGIAbABlACIAOgB0AHIAdQBlACwAIgBtAEQAZQBmAGEAdQBsAHQAUgB1AG4AIgA6AFsAXQAsACIAbQBIAGUAaQBnAGgAdAAiADoAMAAsACIAbQBIAGkAbgBkAGkARABpAGcAaQB0AHMAIgA6AGYAYQBsAHMAZQAsACIAbQBJAG4AZABpAGMAIgA6AGYAYQBsAHMAZQAsACIAbQBJAHMATQBhAHMAawAiADoAZgBhAGwAcwBlACwAIgBtAEkAcwBNAGEAcwBrAEkAbgB2AGUAcgB0AGUAZAAiADoAZgBhAGwAcwBlACwAIgBtAEkAcwBWAGUAcgB0AGkAYwBhAGwAVABlAHgAdAAiADoAZgBhAGwAcwBlACwAIgBtAEwAZQBhAGQAaQBuAGcAIgA6ADAALAAiAG0ATABpAGcAYQB0AHUAcgBlAHMAIgA6AGYAYQBsAHMAZQAsACIAbQBMAGkAbgBlAEMAYQBwAFQAeQBwAGUAIgA6ADAALAAiAG0ATABpAG4AZQBKAG8AaQBuAFQAeQBwAGUAIgA6ADEALAAiAG0ATQBpAHQAZQByAEwAaQBtAGkAdAAiADoAMgAuADUALAAiAG0ATgB1AG0AUwB0AHIAbwBrAGUAcwAiADoAMgAsACIAbQBSAFQATAAiADoAZgBhAGwAcwBlACwAIgBtAFMAaABhAGQAbwB3AEEAbgBnAGwAZQAiADoAMQAzADUALAAiAG0AUwBoAGEAZABvAHcAQgBsAHUAcgAiADoANAAwACwAIgBtAFMAaABhAGQAbwB3AEMAbwBsAG8AcgAiADoANAAxADQANAA5ADUAOQAsACIAbQBTAGgAYQBkAG8AdwBPAGYAZgBzAGUAdAAiADoANwAsACIAbQBTAGgAYQBkAG8AdwBPAHAAYQBjAGkAdAB5ACIAOgA3ADUALAAiAG0AUwBoAGEAZABvAHcAUwBpAHoAZQAiADoAMAAsACIAbQBTAGgAYQBkAG8AdwBWAGkAcwBpAGIAbABlACIAOgBmAGEAbABzAGUALAAiAG0AUwB0AHkAbABlAFMAaABlAGUAdAAiADoAewAiAG0AQQBkAGQAaQB0AGkAbwBuAGEAbABTAHQAcgBvAGsAZQBDAG8AbABvAHIAIgA6AFsAewAiAG0AUABhAHIAYQBtAFYAYQBsAHUAZQBzACIAOgBbAFsAMAAsADEANgA3ADcANwAyADEANQBdAF0AfQBdACwAIgBtAEEAZABkAGkAdABpAG8AbgBhAGwAUwB0AHIAbwBrAGUAVgBpAHMAaQBiAGwAZQAiADoAWwB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAdAByAHUAZQBdAF0AfQBdACwAIgBtAEEAZABkAGkAdABpAG8AbgBhAGwAUwB0AHIAbwBrAGUAVwBpAGQAdABoACIAOgBbAHsAIgBtAFAAYQByAGEAbQBWAGEAbAB1AGUAcwAiADoAWwBbADAALAAxADIAXQBdAH0AXQAsACIAbQBCAGEAcwBlAGwAaQBuAGUATwBwAHQAaQBvAG4AIgA6AHsAIgBtAFAAYQByAGEAbQBWAGEAbAB1AGUAcwAiADoAWwBbADAALAAwAF0AXQB9ACwAIgBtAEIAYQBzAGUAbABpAG4AZQBTAGgAaQBmAHQAIgA6AHsAIgBtAFAAYQByAGEAbQBWAGEAbAB1AGUAcwAiADoAWwBbADAALAAwAF0AXQB9ACwAIgBtAEMAYQBwAHMATwBwAHQAaQBvAG4AIgA6AHsAIgBtAFAAYQByAGEAbQBWAGEAbAB1AGUAcwAiADoAWwBbADAALAAwAF0AXQB9ACwAIgBtAEYAYQB1AHgAQgBvAGwAZAAiADoAewAiAG0AUABhAHIAYQBtAFYAYQBsAHUAZQBzACIAOgBbAFsAMAAsAGYAYQBsAHMAZQBdAF0AfQAsACIAbQBGAGEAdQB4AEkAdABhAGwAaQBjACIAOgB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAZgBhAGwAcwBlAF0AXQB9ACwAIgBtAEYAaQBsAGwAQwBvAGwAbwByACIAOgB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAMQA2ADcANwA3ADIAMQA1AF0AXQB9ACwAIgBtAEYAaQBsAGwATwB2AGUAcgBTAHQAcgBvAGsAZQAiADoAewAiAG0AUABhAHIAYQBtAFYAYQBsAHUAZQBzACIAOgBbAFsAMAAsAHQAcgB1AGUAXQBdAH0ALAAiAG0ARgBpAGwAbABWAGkAcwBpAGIAbABlACIAOgB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAdAByAHUAZQBdAF0AfQAsACIAbQBGAG8AbgB0AE4AYQBtAGUAIgA6AHsAIgBtAFAAYQByAGEAbQBWAGEAbAB1AGUAcwAiADoAWwBbADAALAAiAEwAdQBjAGkAZABhAEcAcgBhAG4AZABlACIAXQBdAH0ALAAiAG0ARgBvAG4AdABTAGkAegBlACIAOgB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAMQAwADAAXQBdAH0ALAAiAG0ASwBlAHIAbgBpAG4AZwAiADoAewAiAG0AUABhAHIAYQBtAFYAYQBsAHUAZQBzACIAOgBbAFsAMAAsADAAXQBdAH0ALAAiAG0AUwB0AHIAbwBrAGUAQwBvAGwAbwByACIAOgB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAMQA2ADcAMQAxADYAOAAwAF0AXQB9ACwAIgBtAFMAdAByAG8AawBlAFYAaQBzAGkAYgBsAGUAIgA6AHsAIgBtAFAAYQByAGEAbQBWAGEAbAB1AGUAcwAiADoAWwBbADAALAB0AHIAdQBlAF0AXQB9ACwAIgBtAFMAdAByAG8AawBlAFcAaQBkAHQAaAAiADoAewAiAG0AUABhAHIAYQBtAFYAYQBsAHUAZQBzACIAOgBbAFsAMAAsADYANABdAF0AfQAsACIAbQBUAGUAeAB0ACIAOgAiAEMAZQBjAGkAIABlAHMAdAAgAHUAbgAgAHQAZQBzAHQAIABxAHUAaQAgAHMAZQAgAHQAcgBvAHUAdgBlAFwAcgBzAHUAcgAgAGQAZQB1AHgAIABsAGkAZwBuAGUAcwAiACwAIgBtAFQAcgBhAGMAawBpAG4AZwAiADoAewAiAG0AUABhAHIAYQBtAFYAYQBsAHUAZQBzACIAOgBbAFsAMAAsADAAXQBdAH0ALAAiAG0AVABzAHUAbQBpACIAOgB7ACIAbQBQAGEAcgBhAG0AVgBhAGwAdQBlAHMAIgA6AFsAWwAwACwAMABdAF0AfQB9ACwAIgBtAFQAYQBiAFcAaQBkAHQAaAAiADoANAAwADAALAAiAG0AVwBpAGQAdABoACIAOgAwAH0ALAAiAG0AVgBlAHIAcwBpAG8AbgAiADoAMQB9AA==

When I decode it, I have something like
å{“mTextParam”:{“mAlignment”:0,“mBackFillColor”:4144959,“mBackFillOpacity”:44.554454803466797,“mBackFillSize”:46.534652709960938,“mBackFillVisible”:true,“mDefaultRun”:[],“mHeight”:0,“mHindiDigits”:false,“mIndic”:false,“mIsMask”:false,“mIsMaskInverted”:false,“mIsVerticalText”:false,“mLeading”:0,“mLigatures”:false,“mLineCapType”:0,“mLineJoinType”:1,“mMiterLimit”:2.5,“mNumStrokes”:2,“mRTL”:false,“mShadowAngle”:135,“mShadowBlur”:40,“mShadowColor”:4144959,“mShadowOffset”:7,“mShadowOpacity”:75,“mShadowSize”:0,“mShadowVisible”:false,“mStyleSheet”:{“mAdditionalStrokeColor”:[{“mParamValues”:[[0,16777215]]}],“mAdditionalStrokeVisible”:[{“mParamValues”:[[0,true]]}],“mAdditionalStrokeWidth”:[{“mParamValues”:[[0,12]]}],“mBaselineOption”:{“mParamValues”:[[0,0]]},“mBaselineShift”:{“mParamValues”:[[0,0]]},“mCapsOption”:{“mParamValues”:[[0,0]]},“mFauxBold”:{“mParamValues”:[[0,false]]},“mFauxItalic”:{“mParamValues”:[[0,false]]},“mFillColor”:{“mParamValues”:[[0,16777215]]},“mFillOverStroke”:{“mParamValues”:[[0,true]]},“mFillVisible”:{“mParamValues”:[[0,true]]},“mFontName”:{“mParamValues”:[[0,“LucidaGrande”]]},“mFontSize”:{“mParamValues”:[[0,100]]},“mKerning”:{“mParamValues”:[[0,0]]},“mStrokeColor”:{“mParamValues”:[[0,16711680]]},“mStrokeVisible”:{“mParamValues”:[[0,true]]},“mStrokeWidth”:{“mParamValues”:[[0,64]]},“mText”:“This is a 2 lignes test\rI want to edit”,“mTracking”:{“mParamValues”:[[0,0]]},“mTsumi”:{“mParamValues”:[[0,0]]}},“mTabWidth”:400,“mWidth”:0},“mVersion”:1}

I’d like to edit the last text items in the end
“This is a 2 lignes text
I want to edit”

How could I do that ?

Thanks

@ShaneStanley

Hey Shane,

This is wonderful stuff! :sunglasses:

I’ve been meaning to do this for years and just got serious. I’m utterly delighted to find that nearly all the work has been done for me already. I knew Jim had been working on it, but I never got to see the final result.

I have a functioning prototype but still have a couple of things to work out:

  1. Figure out how to replace the URL in the single RTF link in the styled-text.
    • I have no clue how to do that and need help.
  1. Add regex support instead of fixed string replacement.
    • That I should be able to manage, as I have all the pieces.

Would you be able to help me with item one?

The appended code has the relevant Keyboard Maestro Action XML with style-text and embedded URL.

TIA

-Chris


AppleScript Code
--------------------------------------------------------
# dNam: Keyboard Maestro Attribution Action – Find/Replace Styled-Text.
# dMod: 2023/01/22 04:48 
--------------------------------------------------------
use AppleScript version "2.4" -- Yosemite (10.10) or later
use framework "Foundation"
use framework "AppKit"
use scripting additions
--------------------------------------------------------

set theString to current application's NSString's stringWithString:theString
-- convert string to data
set stringData to theString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
-- convert plist to mutable dictionary
set {mutableDict, theError} to current application's NSPropertyListSerialization's propertyListWithData:stringData options:(current application's NSPropertyListMutableContainersAndLeaves) |format|:(missing value) |error|:(reference)
if mutableDict is missing value then error (theError's localizedDescription() as text)
-- extract RTFD data and convert to a mutable atributed string
set theData to mutableDict's objectForKey:"StyledText"
set mutableAttString to current application's NSMutableAttributedString's alloc()'s initWithRTFD:theData documentAttributes:(missing value)
set plainString to mutableAttString's |string|()
-- modify the mutable atributed string
-- how you do that depends on what you want to do, obviously
set theRange to plainString's rangeOfString:"How Do I base64 Decode and Encode Multiple Lines?"

--» REPLACING RTF URL'S TITLE HERE:

mutableAttString's replaceCharactersInRange:theRange withString:"New URL Title" --» •••••

--» NEED TO KNOW HOW TO REPLACE THE URL ITSELF.

# STUB

-- convert back to RTFD data
set theData to mutableAttString's RTFDFromRange:{0, mutableAttString's |length|()} documentAttributes:(missing value)
-- update the dictionary
mutableDict's setObject:theData forKey:"StyledText"
mutableDict's setObject:(mutableAttString's |string|()) forKey:"Text"
-- make new plist from dictionary
set {plistData, theError} to current application's NSPropertyListSerialization's dataWithPropertyList:mutableDict |format|:(current application's NSPropertyListXMLFormat_v1_0) options:0 |error|:(reference)
-- get text version
set newString to (current application's NSString's alloc()'s initWithData:plistData encoding:(current application's NSUTF8StringEncoding)) as text

--------------------------------------------------------
--» ACTION XML
--------------------------------------------------------

property theString : "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">
<plist version=\"1.0\">
   <dict>
      <key>ActionColor</key>
      <string>Aqua</string>
      <key>ActionName</key>
      <string>Macro Notes</string>
      <key>ActionUID</key>
      <integer>12930987</integer>
      <key>MacroActionType</key>
      <string>Comment</string>
      <key>StyledText</key>
      <data>
      cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC4iBwAAKwAAAAEAAAAa
      BwAAe1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYxNjcxXGNvY29h
      c3VicnRmNjAwCntcZm9udHRibFxmMFxmbmlsXGZjaGFyc2V0MCBMdWNpZGFH
      cmFuZGU7XGYxXGZyb21hblxmY2hhcnNldDAgVGltZXMtUm9tYW47fQp7XGNv
      bG9ydGJsO1xyZWQyNTVcZ3JlZW4yNTVcYmx1ZTI1NTtccmVkMFxncmVlbjBc
      Ymx1ZTIzMztccmVkMFxncmVlbjBcYmx1ZTA7fQp7XCpcZXhwYW5kZWRjb2xv
      cnRibDs7XGNzc3JnYlxjMFxjMFxjOTMzMzM7XGNzc3JnYlxjMFxjMFxjMDt9
      ClxwYXJkXHR4NTYwXHR4MTEyMFx0eDE2ODBcdHgyMjQwXHR4MjgwMFx0eDMz
      NjBcdHgzOTIwXHR4NDQ4MFx0eDUwNDBcdHg1NjAwXHR4NjE2MFx0eDY3MjBc
      cGFyZGlybmF0dXJhbFxwYXJ0aWdodGVuZmFjdG9yMAoKXGYwXGZzMjYgXGNm
      MCBcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdi
      N1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3
      XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdc
      J2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wn
      YjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdi
      N1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3
      XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdc
      J2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wn
      YjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdi
      N1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcCkF1dGhvcgkgOiBDaHJp
      c3RvcGhlciBTdG9uZSA8c2NyaXB0bWVpc3RlckB0aGVzdG9uZWZvcmdlLmNv
      bT5cCkNyZWF0ZWQJIDogIDIwMjMvMDEvMjIgMDM6MThcCk1vZGlmaWVkCSA6
      ICAyMDIzLzAxLzIyIDAzOjE4XApUYXNrCSA6ICBcClVSTHMJIDoge1xmaWVs
      ZHtcKlxmbGRpbnN0e0hZUEVSTElOSyAiaHR0cHM6Ly9mb3J1bS5sYXRlbmln
      aHRzdy5jb20vdC9ob3ctZG8taS1iYXNlNjQtZGVjb2RlLWFuZC1lbmNvZGUt
      bXVsdGlwbGUtbGluZXMvNzU5LzExIn19e1xmbGRyc2x0IFxjZjIgXGV4cG5k
      MFxleHBuZHR3MFxrZXJuaW5nMApcdWwgXHVsYzIgSG93IERvIEkgYmFzZTY0
      IERlY29kZSBhbmQgRW5jb2RlIE11bHRpcGxlIExpbmVzP319ClxmMVxmczI0
      IFxjZjMgXGV4cG5kMFxleHBuZHR3MFxrZXJuaW5nMAogClxmMFxmczI2IFxj
      ZjAgXGtlcm5pbmcxXGV4cG5kMFxleHBuZHR3MCAgXApUYWdzCSA6ICBAY2Nz
      dG9uZSwgQEtNRm9ydW1cCkttVnIJIDogIEtleWJvYXJkIE1hZXN0cm8gMTAu
      MiBvbiBtYWNPUyAxMC4xNC42XApNYWNyb1ZyCSA6ICAxLjAwXApcJ2I3XCdi
      N1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3
      XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdc
      J2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wn
      YjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdi
      N1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3
      XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdc
      J2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wn
      YjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdi
      N1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3XCdiN1wnYjdcJ2I3
      XCdiN1wnYjdcJ2I3XCdiN1wnYjd9AQAAACMAAAABAAAABwAAAFRYVC5ydGYQ
      AAAAawbNY7YBAAAAAAAAAAAAAA==
      </data>
      <key>Title</key>
      <string>↙…↘</string>
   </dict>
</plist>
"

--------------------------------------------------------