Help with JXA code for extracting RTF from a Plist to disk

@ComplexPoint - I'm hoping you, or someone else can help.

I asked ChatGPT for this: "jxa plist save rtf data to file", and it gave me the following script:

ObjC.import('Foundation');

// Function to read plist file and extract RTF data
function getRTFDataFromPlist(plistPath) {
    // Load the plist file
    var plistData = $.NSData.dataWithContentsOfFile(plistPath);
    if (!plistData) {
        throw new Error("Failed to load plist data from file.");
    }

    // Deserialize the plist data into an object
    var plist = $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(plistData, 0, null, null);
    if (!plist) {
        throw new Error("Failed to deserialize plist data.");
    }

    // Extract RTF data (assuming it's stored in a key named "RTFKey")
    var rtfData = plist.objectForKey('RTFKey');
    if (!rtfData) {
        throw new Error("No RTF data found in plist.");
    }

    return rtfData;
}

// Function to save RTF data to a file
function saveRTFToFile(rtfData, outputPath) {
    var success = rtfData.writeToFileAtomically(outputPath, true);
    if (!success) {
        throw new Error("Failed to save RTF data to file.");
    }
}

// Example usage
var plistPath = "/path/to/your/file.plist";  // Change this to your plist file path
var outputPath = "/path/to/your/output.rtf";  // Change this to your desired output file path

try {
    var rtfData = getRTFDataFromPlist(plistPath);
    saveRTFToFile(rtfData, outputPath);
    console.log("RTF data successfully saved to file.");
} catch (e) {
    console.log("Error: " + e.message);
}

The problem is this line:

plist.objectForKey('RTFKey');
...
plist.objectForKey is not a function.

Any help?

Can you show us, or point us to, a sample of the kind of plist file which you are expecting to parse to an Object, finding "RTFKey" as a top-level key ?

Presumably the plist you are looking at is not structured as an Array at the top level ?

PS, if it is a serialization of a Dictionary rather than an Array, then my habit would be something like we do with the KM macros plist:

// jsoFromPlistPathLR :: FilePath -> 
// Either String Dict 
const jsoFromPlistPathLR = fp => {
    const
        nsDict = $.NSDictionary.dictionaryWithContentsOfURL(
            $.NSURL.fileURLWithPath(fp)
        );

    return nsDict.isNil()
        ? Left(`Could not be read as .plist: "${fp}"`)
        : Right(ObjC.deepUnwrap(nsDict));
};

as in:

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    ObjC.import("AppKit");

    const main = () =>
        either(
            message => message
        )(
            dict => JSON.stringify(dict, null, 2)
        )(
            jsoFromPlistPathLR(
                kmPlistPath()
            )
        );

    // --------------------- JXA ---------------------

    // applicationSupportPath :: () -> String
    const applicationSupportPath = () => {
        const uw = ObjC.unwrap;

        return uw(
            uw($.NSFileManager.defaultManager
                .URLsForDirectoryInDomains(
                    $.NSApplicationSupportDirectory,
                    $.NSUserDomainMask
                )
            )[0].path
        );
    };


    // jsoFromPlistPathLR :: FilePath -> 
    // Either String Dict 
    const jsoFromPlistPathLR = fp => {
        const
            nsDict = $.NSDictionary.dictionaryWithContentsOfURL(
                $.NSURL.fileURLWithPath(fp)
            );

        return nsDict.isNil()
            ? Left(`Could not be read as .plist: "${fp}"`)
            : Right(ObjC.deepUnwrap(nsDict));
    };


    // kmPlistPath :: () -> IO FilePath
    const kmPlistPath = () => {
        const
            kmMacros = [
                "/Keyboard Maestro/",
                "Keyboard Maestro Macros.plist"
            ].join("");

        return `${applicationSupportPath()}${kmMacros}`;
    };

    // --------------------- GENERIC ---------------------


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


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

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => "Left" in e
            ? fl(e.Left)
            : fr(e.Right);


    return main();
})();

As usual, you've shown me where I'm being forehead-slapping foolish. With the XML now set to a single, <dict> instead of an array, and with some changes I'll discuss below, this works:

Click to expand
(() => {
	ObjC.import('Foundation');

	// Function to read plist file and extract RTF data
	function getRTFDataFromPlist(plistPath, rtfKey) {
		// Load the plist file
		var plistData = $.NSData.dataWithContentsOfFile(plistPath);
		if (!plistData) {
			throw new Error("Failed to load plist data from file.");
		}

		// Deserialize the plist data into an object
		var plist = $.NSPropertyListSerialization.propertyListWithDataOptionsFormatError(plistData, 0, null, null);
		if (!plist) {
			throw new Error("Failed to deserialize plist data.");
		}

		// Extract RTF data (assuming it's stored in a key named "RTFKey")
		var rtfData = plist.objectForKey(rtfKey);
		if (!rtfData) {
			throw new Error("No RTF data found in plist.");
		}

		return rtfData;
	}

	// Function to save RTF data to a file
	function saveRTFToFile(rtfData, outputPath) {
		var success = rtfData.writeToFileAtomically(outputPath, true);
		if (!success) {
			throw new Error("Failed to save RTF data to file.");
		}
	}

function fixRTFFile(filePath) {

	const getNSErrorMessage = (nsError, message) => {
		try {
			return message + ". Error: " + ObjC.unwrap(nsError.localizedDescription);
		} catch (e) {
			return message;
		}
	}

	const readTextFile = path => {
		var nsError = $();
		var result = ObjC.unwrap($.NSString.stringWithContentsOfFileEncodingError(
				$(path).stringByStandardizingPath, $.NSMacOSRomanStringEncoding, nsError));
		if (result == null)
			throw Error(getNSErrorMessage(nsError, `File not found or couldn't be read: "${path}"`));
		return result;
	}

	const writeTextFile = (text, path) => {
		var nsError = $();
		var str = $.NSString.alloc.initWithUTF8String(text);
		var result = str.writeToFileAtomicallyEncodingError(
			$(path).stringByStandardizingPath, true, $.NSMacOSRomanStringEncoding, nsError);
		if (!result)
			throw Error(getNSErrorMessage(nsError, `Could not write file: "${path}"`));
	}

	const text = readTextFile(filePath)
		.replace(/(^.*?)(?={\\rtf1)/, "")
		.replace(/(?:})([^}]*?$)/, "}");
	writeTextFile(text, filePath);
}

	// Example usage
	var plistPath = "/Users/Dan/Documents/Development/Keyboard Maestro Offload/_testFiles/Comment.xml";
	var outputPath = "/Users/Dan/Documents/Development/Keyboard Maestro Offload/_testFiles/Comment.rtf";

	try {
		var rtfData = getRTFDataFromPlist(plistPath, "StyledText");
		saveRTFToFile(rtfData, outputPath);
		fixRTFFile(outputPath)
		console.log("RTF data successfully saved to file.");
	} catch (e) {
		console.log("Error: " + e.message);
	}
})();

Here's some sample RTF data:

Click to expand
<?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>Yellow</string>
	<key>ActionUID</key>
	<integer>15816097</integer>
	<key>MacroActionType</key>
	<string>Comment</string>
	<key>StyledText</key>
	<data>
	cnRmZAAAAAADAAAAAgAAAAcAAABUWFQucnRmAQAAAC6FAwAAKwAAAAEAAAB9
	AwAAe1xydGYxXGFuc2lcYW5zaWNwZzEyNTJcY29jb2FydGYyNzA5Clxjb2Nv
	YXRleHRzY2FsaW5nMFxjb2NvYXBsYXRmb3JtMHtcZm9udHRibFxmMFxmc3dp
	c3NcZmNoYXJzZXQwIEhlbHZldGljYS1Cb2xkO1xmMVxmc3dpc3NcZmNoYXJz
	ZXQwIEhlbHZldGljYTt9CntcY29sb3J0Ymw7XHJlZDI1NVxncmVlbjI1NVxi
	bHVlMjU1O30Ke1wqXGV4cGFuZGVkY29sb3J0Ymw7O30KXHBhcmRcdHg1NjBc
	dHgxMTIwXHR4MTY4MFx0eDIyNDBcdHgyODAwXHR4MzM2MFx0eDM5MjBcdHg0
	NDgwXHR4NTA0MFx0eDU2MDBcdHg2MTYwXHR4NjcyMFxwYXJkaXJuYXR1cmFs
	XHBhcnRpZ2h0ZW5mYWN0b3IwCgpcZjBcYlxmczI4IFxjZjAgTmFtZQpcZjFc
	YjAgOiBDb3B5IHRvIENsaXBib2FyZCAiT2ZmbG9hZENvbW1lbnRzUlRGIiBh
	bmQgVmVyaWZ5IFJpY2ggVGV4dFwKClxmMFxiIFZlcnNpb24KXGYxXGIwIDog
	MS4wXAoKXGYwXGIgVXBkYXRlZApcZjFcYjAgOiAyMDI0LzA3LzI2IDExOjI4
	IFBUXAoKXGYwXGIgQnkKXGYxXGIwIDogRGFuIFRob21hc1wKXAoKXGYwXGIg
	UFVSUE9TRQpcZjFcYjAgOlwKXApDbGVhcnMgdGhlIE5hbWVkIENsaXBib2Fy
	ZCAiT2ZmbG9hZENvbW1lbnRzUlRGIiwgdGhlbjpcClwKRG9lcyBhICJTZWxl
	Y3QgQWxsIiwgdGhlbiBDb3B5IHRvIE5hbWVkIENsaXBib2FyZCAiT2ZmbG9h
	ZENvbW1lbnRzUlRGIiwgdGhlbiB1cC1hcnJvdy5cClwKClxmMFxiIFJFU1VM
	VApcZjFcYjAgOlwKXAppZiBPZmZsb2FkQ29tbWVudHNSVEYgZG9lc24ndCBj
	b250YWlucyByaWNoIHRleHQsIHRoZSBtYWNybyB3aWxsIGJlIGFib3J0ZWQg
	d2l0aCAiTG9jYWxfRXJyb3JNZXNzYWdlIi5cClwKClxmMFxiIFZFUlNJT04g
	SElTVE9SWQpcZjFcYjAgOlwKMS4wIC0gSW5pdGlhbCB2ZXJzaW9uLn0BAAAA
	IwAAAAEAAAAHAAAAVFhULnJ0ZhAAAADG6qNmtgEAAAAAAAAAAAAA
	</data>
	<key>Title</key>
	<string>Copy to Clipboard "OffloadCommentsRTF" and Verify Rich Text v1.0</string>
</dict>
</plist>

As for changes, I passed the key into the routine. But the big change was adding this at the end:

fixRTFFile(outputPath)

...and the corresponding function.

It's to remove the "cruft" at the start and end of the file:

image

Ideally I'd like to remove the cruft before saving it to disk, but I can't figure out how to convert rtfData to a JS string I can manipulate, then change it back to the correct format and save it to disk.

So I have something that appears to work (big thanks for rattling my brain), but it would be cool if I could fix the cruft before saving it out to disk in the first place.

Awaiting your brilliance... ;p

PS: Regarding your other comment, let's talk about that after we finish talking about this.

convert rtfData to a JS string

Are you getting NSData from which you can derive an NSAttributedString ?

Expand disclosure triangle to view JS source
// rtfFromNSDataLR :: NSData ->
// Either String NSAttributedString
const rtfFromNSDataLR = data => {
    const
        error = $(),
        rtf = $.NSAttributedString.alloc
        .initWithDataOptionsDocumentAttributesError(
            data, void 0, void 0, error
        );

    return !error.code ? (
        Right(rtf)
    ) : Left(ObjC.unwrap(error.localizedDescription));
};

or an NSString ?

Expand disclosure triangle to view JS source
ObjC.unwrap(
    $.NSString.alloc.initWithDataEncoding(
        rtfData,
        $.NSUTF8StringEncoding
    )
)

I'll look into it - I've got some wifey things I have to do right now.

I'm going to put this on hold for now. I'll post when/if I need more help. Thanks so much for the help up to this point!

1 Like