Need Help With Using NSDictionary from XML

The only way to search all levels is, well, to search all levels. But assuming the structure is always as you post, you could do this sort of thing:

set theDict to getDict(pxml)
set theResults to current application's NSMutableArray's array()
set thenActionArrays to theDict's valueForKeyPath:"Actions.ThenActions"
set thePredicate to current application's NSPredicate's predicateWithFormat:"%K == %@" argumentArray:{"MacroActionType", "ExecuteAppleScript"}
repeat with anActionArray in thenActionArrays
	set thenMatches to (anActionArray's filteredArrayUsingPredicate:thePredicate)
	(theResults's addObjectsFromArray:(thenMatches's valueForKey:"Path"))
end repeat
return theResults as list

Shane, thanks. That’s a great suggestion, but unfortunately the structure can vary a lot.
So, how do I search all levels?

Thanks.

You could do something like this:

set theDict to getDict(pxml)
set theResults to current application's NSMutableArray's array()
set actionArrays to theDict's valueForKeyPath:"Actions"
my searchInArray:actionArrays addingTo:theResults
return theResults as list

on searchInArray:anArray addingTo:theResults
	repeat with anItem in anArray
		if (anItem's isKindOfClass:(current application's NSArray)) as boolean then
			repeat with subArray in anItem
				(my searchInArray:subArray addingTo:theResults)
			end repeat
		else if (anItem's isKindOfClass:(current application's NSDictionary)) as boolean then
			repeat with aKey in anItem's allKeys()
				set nextObject to (anItem's objectForKey:aKey)
				if aKey as text = "MacroActionType" and nextObject as text = "ExecuteAppleScript" then
					(theResults's addObject:(anItem's objectForKey:"Path"))
				else
					if (nextObject's isKindOfClass:(current application's NSArray)) as boolean then
						(my searchInArray:nextObject addingTo:theResults)
					end if
				end if
			end repeat
		end if
	end repeat
end searchInArray:addingTo:

Another approach (which I personally find easier to write and maintain) is to shift the traversal and querying into an XQuery description, using:

objectsForXQuery:error

https://developer.apple.com/documentation/foundation/nsxmlnode/1409768-objectsforxquery?language=objc

It might seem like overkill to have to learn the pattern of XQuery FLWOR expressions, but the basics are very quickly understood, and if you need more there’s plenty of documentation.

(Note that the baked-in XQuery provided by the Foundation classes is XQuery 1 (rather than the current XQuery 3, which provides some useful shortcuts, but isn’t at all necessary).

(an old, but still working example of querying OPML files at: https://github.com/RobTrew/txtquery-tools/tree/master/Yosemite%20Javascript%20XQuery%20demo
is written in JXA but could readily be reworked into AS)

Using XQuery will certainly be much faster. Personally, though, whenever I’ve looked at it, I’ve found the effort-reward balance too out of kilter. Maybe I need more complex problems to solve.

Always a good assumption, but not, in my experience, borne out here.

Maybe I need more complex problems to solve

Or just try it with something very simple. It lends itself well to incremental use.

The first edition of the OReilly book below should be very cheap by now (
it’s already in a second edition which covers Ver 3 - not needed for Foundation class use)

I was rather hoping my previous post would prompt you to provide an example for the case being discussed here…

Well, Jim is bilingual, so let’s start, in the interests of speed, before I leave the house, with what I would probably write myself (I’ll get back to my machine this evening, and translate to AppleScript if no-one else has done that).

Assuming the sample XML given above, and running this in Script Editor with the language tab set to JavaScript:

(() => {
    'use strict';

    const main = () => {
        const
            docXML = $.NSXMLDocument.alloc.initWithXMLStringOptionsError(
                readFile('~/Desktop/sample.xml'),
                0, null
            ),
            xs = ObjC.unwrap(docXML.objectsForXQueryError(
                'for $path in //key[text()="ThenActions"]/' +
                'following-sibling::array/dict/key[text()="Path"]/' +
                'following-sibling::string[1]\n' +
                'return string($path)',
                null
            ));
        return JSON.stringify(
            xs.map(x => ObjC.unwrap(x)),
            null, 2
        );
    };

    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // readFile :: FilePath -> IO String
    const readFile = fp => {
        const
            e = $(),
            uw = ObjC.unwrap,
            s = uw(
                $.NSString.stringWithContentsOfFileEncodingError(
                    $(fp)
                    .stringByStandardizingPath,
                    $.NSUTF8StringEncoding,
                    e
                )
            );
        return undefined !== s ? (
            s
        ) : uw(e.localizedDescription);
    };

    return main();
})();

we get:

[
  "",
  "%Variable%DND__KM_Scripts_Folder%/Test JXA File.scpt",
  "%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt"
]

Interesting, thanks. I think you’re oversimplifying what needs to be done so it’s not returning quite the correct answer, but it gives a taste of what might be involved.

Jim will be able to fine-tune his query, but starting with an AS translation and repackaging in which we start by looking for keys with the text MacroActionType (I did read his spec too fast), we might write:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions


-- valuesFromXQuery :: XQuery String -> XML String -> [a]
on valuesFromXQuery(strXQuery, strXML)
	((current application's NSXMLDocument's alloc()'s ¬
		initWithXMLString:strXML options:0 |error|:(missing value))'s ¬
		objectsForXQuery:(strXQuery) |error|:(missing value)) as list
end valuesFromXQuery

-- TEST -----------------------------------------------------------
on run
	set strXQuery to ¬
		"for $path in " & ¬
		"//key[text()='MacroActionType']/following-sibling::key[text()='Path']/following-sibling::string[1] " & ¬
		"return string($path)"
	
	valuesFromXQuery(strXQuery, readFile("~/Desktop/sample.xml"))
end run


-- GENERIC FUNCTIONS-------------------------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- readFile :: FilePath -> IO String
on readFile(strPath)
	set ca to current application
	set e to reference
	set {s, e} to (ca's NSString's ¬
		stringWithContentsOfFile:((ca's NSString's ¬
			stringWithString:strPath)'s ¬
			stringByStandardizingPath) ¬
			encoding:(ca's NSUTF8StringEncoding) |error|:(e))
	if missing value is e then
		s as string
	else
		(localizedDescription of e) as string
	end if
end readFile

Harvesting:

{"", "%Variable%DND__KM_Scripts_Folder%/Test JXA File.scpt", "%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt"}

One way of writing the ‘XPath’ pattern in the for ... in clause is to build it up step by step, and watch what it returns.

The / characters work as in POSIX file paths, with a special abbreviation for ‘any level’ which is double slash //

So, matching a key node at any level of nesting:

//key
-->
{"Actions", "Conditions", "ConditionList", "ConditionListMatch", "ElseActions", "MacroActionType", "ThenActions", "DisplayKind", "HonourFailureSettings", "IncludeStdErr", "MacroActionType", "Path", "Text", "UseText", "Variable", "DisplayKind", "MacroActionType", "NotifyOnFailure", "Path", "StopOnFailure", "Variable", "DisplayKind", "MacroActionType", "Path", "Text", "Variable", "TimeOutAbortsMacro", "MacroActionType", "TimeOutAbortsMacro"}

Square-bracket filtering down to key nodes with text MacroActionType at any level of nesting:

//key[text()='MacroActionType']
-->
{"MacroActionType", "MacroActionType", "MacroActionType", "MacroActionType", "MacroActionType"}

All nodes that are following siblings of key nodes with the text MacroActionType: (asterisk matches any node)

//key[text()='MacroActionType']/following-sibling::*
-->
{"IfThenElse", "ThenActions", "DisplayKindVariableHonourFailureSettingsIncludeStdErrMacroActionTypeExecuteAppleScriptPathText-- Script without a PATH --
set x to 1
UseTextVariableLocal__ScriptResultsDisplayKindVariableMacroActionTypeExecuteJavaScriptForAutomationNotifyOnFailurePath%Variable%DND__KM_Scripts_Folder%/Test JXA File.scptStopOnFailureVariableLocal__ScriptResultsDisplayKindVariableMacroActionTypeExecuteAppleScriptPath%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scptText-- Script without a PATH --
set x to 1
VariableLocal__ScriptResults", "ExecuteAppleScript", "Path", "", "Text", "-- Script without a PATH --
set x to 1
", "UseText", "", "Variable", "Local__ScriptResults", "ExecuteJavaScriptForAutomation", "NotifyOnFailure", "", "Path", "%Variable%DND__KM_Scripts_Folder%/Test JXA File.scpt", "StopOnFailure", "", "Variable", "Local__ScriptResults", "ExecuteAppleScript", "Path", "%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt", "Text", "-- Script without a PATH --
set x to 1
", "Variable", "Local__ScriptResults", "TimeOutAbortsMacro", "", "Group", "TimeOutAbortsMacro", ""}

Filtering those following siblings down to just any key nodes with the text Path:

//key[text()='MacroActionType']/following-sibling::key[text()='Path']
-->
{"Path", "Path", "Path"}

The first (one-based indexing [1]) following sibling of each of these path nodes which has the name string:

//key[text()='MacroActionType']/following-sibling::key[text()='Path']/following-sibling::string[1] 
--> 
{"", "%Variable%DND__KM_Scripts_Folder%/Test JXA File.scpt", "%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt"}

In addition to the following-sibling::* axis, and the // all descendants abbreviation we also have the ‘axes’ listed at:


Re: error messages for any XML or XQuery glitches:

Probably useful, in practice, to capture any XML or XQuery parsing error messages (or XML file-reading messages). I personally prefer to do this with a composable Either wrapping, but others may prefer try ... error etc.

With Either wrappings, (i.e. Left for error strings, Right for computed values):

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    set strXQuery to ¬
        "for $node in " & ¬
        "//key['MacroActionType'=text()]/following-sibling::key['Path'=text()]/following-sibling::string[1] " & ¬
        " return string($node)"
    
    script showError
        -- Any XML file-reading, XML parsing, or XQuery parsing error
        -- which has come through the Left channel of the Either chain.
        on |λ|(x)
            display dialog x with title "XQuery applied to plist"
        end |λ|
    end script
    
    script useResult
        on |λ|(xs)
            -- Whatever we want to do with the query results
            xs
        end |λ|
    end script
    
    either(showError, useResult, ¬
        bindLR(readFileLR("~/Desktop/sample.xml"), ¬
            valuesFromXQueryLR(strXQuery)))
end run


-- XQuery applied to XML ----------------------------------

-- valuesFromXQuery :: XQuery String -> XML String -> Either String [a]
on valuesFromXQueryLR(strXQuery)
    script
        on |λ|(strXML)
            set eXML to reference
            set {docXML, eXML} to (current application's NSXMLDocument's alloc()'s ¬
                initWithXMLString:strXML options:0 |error|:eXML)
            if missing value is eXML then
                set lrDoc to |Right|(docXML)
            else
                set lrDoc to |Left|((localizedDescription of eXML) as string)
            end if
            
            script query
                on |λ|(docXML)
                    set eXQ to reference
                    set {xs, eXQ} to (docXML's objectsForXQuery:strXQuery |error|:eXQ)
                    if missing value is eXQ then
                        |Right|(xs as list)
                    else
                        |Left|((localizedDescription of eXQ) as string)
                    end if
                end |λ|
            end script
            
            bindLR(lrDoc, query)
        end |λ|
    end script
end valuesFromXQueryLR


-- GENERIC FUNCTIONS---------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- Left :: a -> Either a b
on |Left|(x)
    script
        property type : "Either"
        property |Left| : x
        property |Right| : missing value
    end script
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    script
        property type : "Either"
        property |Left| : missing value
        property |Right| : x
    end script
end |Right|

-- bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
on bindLR(m, mf)
    if missing value is not |Left| of m then
        m
    else
        mReturn(mf)'s |λ|(|Right| of m)
    end if
end bindLR

-- either :: (a -> c) -> (b -> c) -> Either a b -> c
on either(lf, rf, e)
    if missing value is |Left| of e then
        tell mReturn(rf) to |λ|(|Right| of e)
    else
        tell mReturn(lf) to |λ|(|Left| of e)
    end if
end either

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- readFileLR :: FilePath -> Either String String
on readFileLR(strPath)
    set ca to current application
    set e to reference
    set {s, e} to (ca's NSString's ¬
        stringWithContentsOfFile:((ca's NSString's ¬
            stringWithString:strPath)'s ¬
            stringByStandardizingPath) ¬
            encoding:(ca's NSUTF8StringEncoding) |error|:(e))
    if e is missing value then
        |Right|(s as string)
    else
        |Left|((localizedDescription of e) as string)
    end if
end readFileLR

Thanks, Shane. Sorry it took me so long to respond, but I had some other pressing projects to finish, and I’ve not had time to analyze and test your script. Hopefully I will in the next few days.

Thanks, Rob. I am interested in your XQuery approach. Hopefully I’ll have time to analyze and test in the next few days.

No hurry, always happy to answer any questions about XPath or broader XQuery specifics.

The fuller JXA version (channelling any file-reading, XML parsing or XQuery parsing error messages to a dialog) might, FWIW, look like this:

(() => {
    'use strict';

    // xq :: String
    const xq =
        'for $path in \n' +
        '//key[text()="ThenActions"]/' +
        'following-sibling::array/dict/key[text()="Path"]/' +
        'following-sibling::string[1]\n' +
        'return string($path)';

    // main :: IO ()
    const main = () =>
        either(
            // msg => alert('Reading XML and applying an XQuery')(msg),
            alert('Reading XML and applying an XQuery'),
            xs => {
                // Values returned by query available for use here ...

                return JSON.stringify(
                    xs,
                    null, 2
                );
            },
            bindLR(
                readFileLR('~/Desktop/sample.xml'),
                valuesFromXQuery(xq)
            )
        );

    // valuesFromXQuery :: XQuery String -> XML String -> Either String [a]
    const valuesFromXQuery = xq => xml => {
        const
            uw = ObjC.unwrap,
            eXML = $(),
            docXML = $.NSXMLDocument.alloc.initWithXMLStringOptionsError(
                xml, 0, eXML
            );
        return bindLR(
            docXML.isNil() ? (
                Left(uw(eXML.localizedDescription))
            ) : Right(docXML),
            doc => {
                const
                    eXQ = $(),
                    xs = doc.objectsForXQueryError(xq, eXQ);
                return xs.isNil() ? (
                    Left(uw(eXQ.localizedDescription))
                ) : Right(uw(xs).map(uw));
            }
        );
    };

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

    // alert :: String => String -> IO String
    const alert = title => s => {
        const
            sa = Object.assign(Application('System Events'), {
                includeStandardAdditions: true
            });
        return (
            sa.activate(),
            sa.displayDialog(s, {
                withTitle: title,
                buttons: ['OK'],
                defaultButton: 'OK'
            }),
            s
        );
    };

    // GENERIC FUNCTIONS ----------------------------------
    // https://github.com/RobTrew/prelude-jxa

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

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

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = (fl, fr, e) =>
        'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // readFileLR :: FilePath -> Either String IO String
    const readFileLR = fp => {
        const
            e = $(),
            ns = $.NSString.stringWithContentsOfFileEncodingError(
                $(fp).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                e
            );
        return ns.isNil() ? (
            Left(ObjC.unwrap(e.localizedDescription))
        ) : Right(ObjC.unwrap(ns));
    };

    return main();
})();

Using vanilla AppleScript

The script below uses the variable plist, which was set to text content of the sample property list supplied in the original post:

AppleScript variable: plist
set plist 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>Actions</key>
	<array>
		<dict>
			<key>Conditions</key>
			<dict>
				<key>ConditionList</key>
				<array/>
				<key>ConditionListMatch</key>
				<string>All</string>
			</dict>
			<key>ElseActions</key>
			<array/>
			<key>MacroActionType</key>
			<string>IfThenElse</string>
			<key>ThenActions</key>
			<array>
				<dict>
					<key>DisplayKind</key>
					<string>Variable</string>
					<key>HonourFailureSettings</key>
					<true/>
					<key>IncludeStdErr</key>
					<false/>
					<key>MacroActionType</key>
					<string>ExecuteAppleScript</string>
					<key>Path</key>
					<string></string>
					<key>Text</key>
					<string>-- Script without a PATH --
set x to 1
</string>
					<key>UseText</key>
					<true/>
					<key>Variable</key>
					<string>Local__ScriptResults</string>
				</dict>
				<dict>
					<key>DisplayKind</key>
					<string>Variable</string>
					<key>MacroActionType</key>
					<string>ExecuteJavaScriptForAutomation</string>
					<key>NotifyOnFailure</key>
					<false/>
					<key>Path</key>
					<string>%Variable%DND__KM_Scripts_Folder%/Test JXA File.scpt</string>
					<key>StopOnFailure</key>
					<false/>
					<key>Variable</key>
					<string>Local__ScriptResults</string>
				</dict>
				<dict>
					<key>DisplayKind</key>
					<string>Variable</string>
					<key>MacroActionType</key>
					<string>ExecuteAppleScript</string>
					<key>Path</key>
					<string>%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt</string>
					<key>Text</key>
					<string>-- Script without a PATH --
set x to 1
</string>
					<key>Variable</key>
					<string>Local__ScriptResults</string>
				</dict>
			</array>
			<key>TimeOutAbortsMacro</key>
			<true/>
		</dict>
	</array>
	<key>MacroActionType</key>
	<string>Group</string>
	<key>TimeOutAbortsMacro</key>
	<true/>
</dict>
</plist>"
set MacroActionTypes to {}
set Paths to {}

tell application id "com.apple.systemevents"
	set nodes to a reference to (make new property list item with data xmltext)
	
	repeat while nodes exists
		tell nodes
			set keys to my flatten(name)
			set vals to a reference to value
			set mAcT to my flatten(vals whose name = "MacroActionType")
			set _pth to my flatten(vals whose name = "Path")
			if _pth = {} then set _pth to ""
			if keys contains "MacroActionType" then
				copy mAcT to the end of MacroActionTypes
				copy _pth to the end of Paths
			end if
			set nodes to a reference to property list items
		end tell
	end repeat
end tell

{MacroActionTypes:flatten(MacroActionTypes), Paths:flatten(Paths)}
--------------------------------------------------------------------------------
# HANDLERS:

# flatten()
#    A simple, generic, list-flattening handler: {a, {b}} → {a, b}
to flatten(L)
	local L
	
	if L's class ≠ list then return {L}
	if L = {} then return L
	
	flatten(item 1 of L) & flatten(rest of L)
end flatten

Using the given plist value, the output of the script (pretty-printed) is:

{
	MacroActionTypes: {
		"Group", 
		"IfThenElse", 
		"ExecuteAppleScript", 
		"ExecuteJavaScriptForAutomation", 
		"ExecuteAppleScript"
	}, 
	Paths: {
		"", 
		"", 
		"", 
		"%Variable%DND__KM_Scripts_Folder%/Test JXA File.scpt", 
		"%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt"
	}
}

From here, it’s pretty straight forward to zip and filter by macroActionType.

For anyone following along, I took this statement by the OP:

So, for example, given the XML at the bottom of this post, the Nodes for <MacroActionType> = “Execute AppleScript”, would be the first and third inside of the node:

to mean that he wanted to filter by that particular value of MacroActionType. I wrote my script with that assumption, but it seems everyone else read the exercise as getting the results for any value of MacroActionType.

I don’t know which is correct, but if anyone is comparing results and getting confused, this is probably the explanation.

I think you most probably assumed correctly. As a Keyboard Maestro user myself, I suspect I know what the end goal might be (or, at least, one specific use case), as I’ve approached a similar version of this task for my own needs in the past. Typically, one often wants to be able to retrieve the file paths to any/all/some/one external script file that is used in Keyboard Maestro by an individual action (atomic unit) within a macro.

So he’ll undoubtedly wish to filter by MacroActionType=='ExecuteAppleScript', though he may also end up filtering by MacroActionType CONTAINS 'Script', if he wants the paths to all script files and not just AppleScripts.

1 Like

so as a full XPath:

//key['MacroActionType'=text()]/following-sibling::string['ExecuteAppleScript'=text()]/following-sibling::key['Path'=text()]/following-sibling::string[1]
-->
{"", "%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt"}

which we might embed in an XQuery for clause

for $node in ... return string($node)

as something like:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    set strActionType to "ExecuteAppleScript"
    
    set strXPath to ¬
        "//key['MacroActionType'=text()]/" & ¬
        "following-sibling::string['" & strActionType & "'=text()]/" & ¬
        "following-sibling::key['Path'=text()]/" & ¬
        "following-sibling::string[1]"
    
    set strXQuery to "for $node in " & strXPath & " return string($node)"
    
    script showError
        -- Any XML file-reading, XML parsing, or XQuery parsing error
        -- which has come through the Left channel of the Either chain.
        on |λ|(x)
            display dialog x with title "XQuery applied to plist"
        end |λ|
    end script
    
    script useResult
        on |λ|(xs)
            -- Whatever we want to do with the query results
            xs
        end |λ|
    end script
    
    either(showError, useResult, ¬
        bindLR(readFileLR("~/Desktop/sample.xml"), ¬
            valuesFromXQueryLR(strXQuery)))
end run


-- XQuery applied to XML ----------------------------------

-- valuesFromXQuery :: XQuery String -> XML String -> Either String [a]
on valuesFromXQueryLR(strXQuery)
    script
        on |λ|(strXML)
            set eXML to reference
            set {docXML, eXML} to (current application's NSXMLDocument's alloc()'s ¬
                initWithXMLString:strXML options:0 |error|:eXML)
            if missing value is eXML then
                set lrDoc to |Right|(docXML)
            else
                set lrDoc to |Left|((localizedDescription of eXML) as string)
            end if
            
            script query
                on |λ|(docXML)
                    set eXQ to reference
                    set {xs, eXQ} to (docXML's objectsForXQuery:strXQuery |error|:eXQ)
                    if missing value is eXQ then
                        |Right|(xs as list)
                    else
                        |Left|((localizedDescription of eXQ) as string)
                    end if
                end |λ|
            end script
            
            bindLR(lrDoc, query)
        end |λ|
    end script
end valuesFromXQueryLR


-- GENERIC FUNCTIONS---------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- Left :: a -> Either a b
on |Left|(x)
    script
        property type : "Either"
        property |Left| : x
        property |Right| : missing value
    end script
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    script
        property type : "Either"
        property |Left| : missing value
        property |Right| : x
    end script
end |Right|

-- bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
on bindLR(m, mf)
    if missing value is not |Left| of m then
        m
    else
        mReturn(mf)'s |λ|(|Right| of m)
    end if
end bindLR

-- either :: (a -> c) -> (b -> c) -> Either a b -> c
on either(lf, rf, e)
    if missing value is |Left| of e then
        tell mReturn(rf) to |λ|(|Right| of e)
    else
        tell mReturn(lf) to |λ|(|Left| of e)
    end if
end either

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- readFileLR :: FilePath -> Either String String
on readFileLR(strPath)
    set ca to current application
    set e to reference
    set {s, e} to (ca's NSString's ¬
        stringWithContentsOfFile:((ca's NSString's ¬
            stringWithString:strPath)'s ¬
            stringByStandardizingPath) ¬
            encoding:(ca's NSUTF8StringEncoding) |error|:(e))
    if e is missing value then
        |Right|(s as string)
    else
        |Left|((localizedDescription of e) as string)
    end if
end readFileLR

or we can break the XPath location steps into separate lines (XQuery is quite happy with this),

and obtain:

"%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt

from:

for $node in 
//key['MacroActionType'=text()]
/following-sibling::string['ExecuteAppleScript'=text()]
/following-sibling::key['Path'=text()]
/following-sibling::string[1]
return string($node)"

by writing something like:

use AppleScript version "2.4"
use framework "Foundation"
use scripting additions

on run
    set strActionType to "ExecuteAppleScript"
    
    set XPathSteps to {¬
        "//key['MacroActionType'=text()]", ¬
        "/following-sibling::string['" & strActionType & "'=text()]", ¬
        "/following-sibling::key['Path'=text()]", ¬
        "/following-sibling::string[1]"}
    
    set strXQuery to "for $node in \n" & unlines(XPathSteps) & "\nreturn string($node)"
    
    script showError
        -- Any XML file-reading, XML parsing, or XQuery parsing error
        -- which has come through the Left channel of the Either chain.
        on |λ|(x)
            display dialog x with title "XQuery applied to plist"
        end |λ|
    end script
    
    script useResult
        on |λ|(xs)
            -- Whatever we want to do with the query results
            xs
        end |λ|
    end script
    
    concat(concat({¬
        either(showError, useResult, ¬
            bindLR(readFileLR("~/Desktop/sample.xml"), ¬
                valuesFromXQueryLR(strXQuery))), {"\n\nfrom:\n\n"}, {strXQuery}}))
end run


-- XQuery applied to XML ----------------------------------

-- valuesFromXQuery :: XQuery String -> XML String -> Either String [a]
on valuesFromXQueryLR(strXQuery)
    script
        on |λ|(strXML)
            set eXML to reference
            set {docXML, eXML} to (current application's NSXMLDocument's alloc()'s ¬
                initWithXMLString:strXML options:0 |error|:eXML)
            if missing value is eXML then
                set lrDoc to |Right|(docXML)
            else
                set lrDoc to |Left|((localizedDescription of eXML) as string)
            end if
            
            script query
                on |λ|(docXML)
                    set eXQ to reference
                    set {xs, eXQ} to (docXML's objectsForXQuery:strXQuery |error|:eXQ)
                    if missing value is eXQ then
                        |Right|(xs as list)
                    else
                        |Left|((localizedDescription of eXQ) as string)
                    end if
                end |λ|
            end script
            
            bindLR(lrDoc, query)
        end |λ|
    end script
end valuesFromXQueryLR


-- GENERIC FUNCTIONS---------------------------------------
-- https://github.com/RobTrew/prelude-applescript

-- Left :: a -> Either a b
on |Left|(x)
    script
        property type : "Either"
        property |Left| : x
        property |Right| : missing value
    end script
end |Left|

-- Right :: b -> Either a b
on |Right|(x)
    script
        property type : "Either"
        property |Left| : missing value
        property |Right| : x
    end script
end |Right|

-- bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
on bindLR(m, mf)
    if missing value is not |Left| of m then
        m
    else
        mReturn(mf)'s |λ|(|Right| of m)
    end if
end bindLR

-- concat :: [[a]] -> [a]
-- concat :: [String] -> String
on concat(xs)
    set lng to length of xs
    if 0 < lng and string is class of (item 1 of xs) then
        set acc to ""
    else
        set acc to {}
    end if
    repeat with i from 1 to lng
        set acc to acc & item i of xs
    end repeat
    acc
end concat

-- either :: (a -> c) -> (b -> c) -> Either a b -> c
on either(lf, rf, e)
    if missing value is |Left| of e then
        tell mReturn(rf) to |λ|(|Right| of e)
    else
        tell mReturn(lf) to |λ|(|Left| of e)
    end if
end either

-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
    -- 2nd class handler function lifted into 1st class script wrapper. 
    if script is class of f then
        f
    else
        script
            property |λ| : f
        end script
    end if
end mReturn

-- readFileLR :: FilePath -> Either String String
on readFileLR(strPath)
    set ca to current application
    set e to reference
    set {s, e} to (ca's NSString's ¬
        stringWithContentsOfFile:((ca's NSString's ¬
            stringWithString:strPath)'s ¬
            stringByStandardizingPath) ¬
            encoding:(ca's NSUTF8StringEncoding) |error|:(e))
    if e is missing value then
        |Right|(s as string)
    else
        |Left|((localizedDescription of e) as string)
    end if
end readFileLR

-- unlines :: [String] -> String
on unlines(xs)
    -- A single string formed by the intercalation
    -- of a list of strings with the newline character.
    set {dlm, my text item delimiters} to ¬
        {my text item delimiters, linefeed}
    set str to xs as text
    set my text item delimiters to dlm
    str
end unlines

OK, that is interesting, thanks. I didn’t think it could be done in one XPath query like that.

So:

set theDoc to current application's NSXMLDocument's alloc()'s initWithXMLString:pxml options:0 |error|:(missing value)
set {theNodes, theError} to theDoc's nodesForXPath:"//key['MacroActionType'=text()]/following-sibling::string['ExecuteAppleScript'=text()]/following-sibling::key['Path'=text()]/following-sibling::string[1]" |error|:(reference)
return (theNodes's valueForKey:"stringValue") as list --> {"", "%Variable%DND__KM_Scripts_Folder%/Test 2 AppleScript File.scpt"}
3 Likes