Searching for a note across several folders and or files (XQuery + Keyboard Maestro)

Here is a first draft of a Keyboard Maestro macro which searches for matching notes in any .tbx files in a given list of folders and/or specific file-paths.

Ver 0.01 (zipped)
xQuery over multiple Tinderbox files.kmmacros.zip (16.6 KB)

This version just gets a search pattern from a user prompt, and searches for matching text (plain strings or regex) in the $Name and $Text values of every note in every specified .tbx file.

(We could use the same XQuery-based approach to define search predicates over other attribute values etc)

Any matching notes are listed, with Markdown links (both tinderbox:// and hook:// URLs) back to those notes.

The listing is copied to the clipboard.

To use the Macro:

  1. Edit the value of its tbxFoldersAndOrFilePaths variable, listing the folders and or specific .tbx files which you wish to include in searches
  2. Assign a hotkey to the macro and experiment.
6 Likes

And something like an AppleScript ‘hello world’ for XQuery over Tinderbox files

(In this case, over just one Tinderbox file).

use AppleScript version "2.4"
use framework "Foundation" -- We need Apple's Foundation framework for its XQuery processor
use scripting additions

on run
    
    -- The XML contents of the .tbx file that interests us,
    set strTBX to readFile("~/Desktop/sample-003.tbx")
    
    
    -- The string value of the Text element of every item at every level of nesting
    
    -- (View in the Messages panel of the Script Editor Results window)
    
    log valuesFromXQuery("for $item in //item return string($item/text)", strTBX)
    
    
    -- The string value of the attribute named "Name" for every item at every level of nesting
    
    -- (An AppleScript list of $Name attribute values)
    valuesFromXQuery("for $item in //item return string($item/attribute[@name='Name'])", strTBX)
    
end run

-----------------Reusable XQUERY function------------------

-- valuesFromXQuery :: XQuery String -> XML String -> Either String [a]
on valuesFromXQuery(strXQuery, strXML)
    -- Just in case: holders for reporting any errors in the XML or XQuery:
    set xmlError to reference
    set xqueryError to reference
    
    
    -- XML parsed,
    set {docXML, xmlError} to (current application's NSXMLDocument's alloc()'s ¬
        initWithXMLString:strXML options:0 |error|:xmlError)
    if xmlError is not missing value then
        return (localizedDescription of xmlError) as string
    end if
    
    -- XQuery applied
    
    set {xs, xqueryError} to (docXML's objectsForXQuery:strXQuery |error|:xqueryError)
    if xqueryError is not missing value then
        (localizedDescription of xqueryError) as string
    else
        -- A list of values returned from the XQuery over the XML
        xs as list
    end if
end valuesFromXQuery


--------------------------GENERIC--------------------------

-- 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
1 Like

For XQueries over multiple .tbx documents, we generate a small virtual XML document, which contains an include tag (with a url) for each document which we want to include in the query.

We can then apply our XQuery to that virtual document, which, for a query over three .tbx files for example, might look something like:

<?xml version="1.0" encoding="utf-8"?>
<tbxfiles xmlns:xi="http://www.w3.org/2003/XInclude">
    <tbxdoc text="" path="/Users/houthakker/Desktop/" file="SampleA.tbx">
        <xi:include href="file:///Users/houthakker/Desktop/SampleA.tbx"/>
    </tbxdoc>
    <tbxdoc text="" path="/Users/houthakker/Desktop/" file="SampleB.tbx">
        <xi:include href="file:///Users/houthakker/Desktop/SampleB.tbx"/>
    </tbxdoc>
    <tbxdoc text="" path="/Users/houthakker/Desktop/" file="SampleC.tbx">
        <xi:include href="file:///Users/houthakker/Desktop/SampleC.tbx"/>
    </tbxdoc>
</tbxfiles>
1 Like

And we save that that small virtual file as collection.xml, we can run an XQuery over it to generate a sorted list of the names of all notes in those three files:

use AppleScript version "2.4"
use framework "Foundation" -- We need Apple's Foundation framework for its XQuery processor
use scripting additions

on run
    
    -- XML with an <include> tag for the URL of each file that interests us:
    
    set strTBX to readFile("~/Desktop/collection.xml")
    
    -- A sorted AppleScript list of $Name values drawn from 3 .tbx files
    
    set strXQuery to unlines({¬
        "for $item in //item ", ¬
        "let $name := $item/attribute[@name='Name']\n", ¬
        "order by $name return string($name)"})
    
    valuesFromXQueryOverCollection(strXQuery, strTBX)
end run

-----------------Reusable XQUERY function------------------

-- valuesFromXQueryOverCollection :: XQuery String -> XML String -> Either String [a]
on valuesFromXQueryOverCollection(strXQuery, strXML)
    -- Just in case: holders for reporting any errors in the XML or XQuery:
    set xmlError to reference
    set xqueryError to reference
    
    -- XML parsed,
    set ca to current application
    set {docXML, xmlError} to (ca's NSXMLDocument's alloc()'s ¬
        initWithXMLString:strXML options:(ca's NSXMLDocumentXInclude) |error|:xmlError)
    if xmlError is not missing value then
        return (localizedDescription of xmlError) as string
    end if
    
    -- XQuery applied
    
    set {xs, xqueryError} to (docXML's objectsForXQuery:strXQuery |error|:xqueryError)
    if xqueryError is not missing value then
        (localizedDescription of xqueryError) as string
    else
        -- A list of values returned from the XQuery over the XML
        xs as list
    end if
end valuesFromXQueryOverCollection


--------------------------GENERIC--------------------------

-- 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

-- 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
4 Likes

This sounds very promising! Giving it a spin this evening.

Outstanding, @ComplexPoint. Thanks for sharing.