While waiting for Apple to fix Watched Notes-

This script performed a few nested notes, then stops; the Script Editor shows “Running…”

The single-script version reports the same at Line 493 fwiw

Here is a slightly modified testing version (with the lookup function factored out, and some logging added).

If you run it in Script Editor, with the Replies panel open at the bottom, what do you see ?

Ver 0.06
(() => {
    'use strict';

    ObjC.import('AppKit');

    // Copy all folders and notes in the Apple Notes database
    // (except any which are password-protected)
    // to the clipboard for direct pasting as Tinderbox notes
    // with extracted $Tag values.

    // Rob Trew 2020
    // Ver 0.06

    // Testing for Catalina:
    //     - factored out lookup
    //     - added logging

    // main :: IO ()
    const main = () => {

        // A list of generic tree structures,
        // representing folders of notes.
        // (in the Apple Notes app)
        const
            noteForest = map(fmapTree(hashTagsExtracted))(
                forestFromGroups(
                    groupsFromNotes(
                        Application('Notes')
                        .defaultAccount.notes.where({
                            passwordProtected: false
                        })
                    )
                )
            ),
            strClip = tbxXMLFromForest(
                either(
                    constant(noteForest)
                )(textTranslatedByPandoc(noteForest))(
                    pandocPath()
                )
            );

        return alert('Copy from Notes as Tinderbox')(
            either(identity)(constant(
                'Apple Notes copied to clipboard as Tinderbox 8 notes.\n\n' +
                '( Try pasting into an empty Tinderbox document. )'
            ))(
                bindLR(
                    isPrefixOf('<?xml')(strClip) ? (
                        Right(strClip)
                    ) : Left('Tinderbox XML not copied from Notes')
                )(
                    compose(
                        Right,
                        setClipOfTextType(
                            'com.eastgate.tinderbox.scrap'
                        )
                    )
                )
            )
        );
    };

    // -------------------TINDERBOX XML--------------------

    // asPlainText :: String -> String
    const asPlainText = (sa, fpPandoc) => htmlText =>
        // A plain text translation of htmlText, produced by a copy
        // of [pandoc](https://pandoc.org) at the given file path.
        // ( See the pandocPath() function below )
        sa.doShellScript(
            `echo "${htmlText}" | ${fpPandoc} -f html -t plain`
        );

    // forestFromGroups :: [[[a]]] -> [Tree Dict]
    const forestFromGroups = groups =>
        // A list of folder trees, derived from
        // lists of notes, in which each note is
        // itself a list of values.
        // ('FolderName', 'Name', 'Text', 'Created', 'Modified')
        map(grp => Node({
            Name: grp[0][0]
        })(
            map(
                xs => Node(
                    zip(['Name', 'Text', 'Created', 'Modified'])(
                        xs.slice(1)
                    ).reduce(
                        (a, kv) => Object.assign(
                            a, {
                                [kv[0]]: kv[1]
                            }
                        ), {}
                    )
                )([])
            )(grp)
        ))(groups);

    // groupsFromNotes :: Notes Object -> [[[a]]]
    const groupsFromNotes = notesRef =>
        // Groups of lists of notes, where
        // each note is a list of values.
        groupBy(on(eq)(fst))(
            sortBy(comparing(fst))(
                transpose([
                    map(x => {
                        showLog('x', x.properties());
                        return x.name();
                    })(
                        notesRef.container()
                    ),
                    ...map(k => {
                        showLog('k', k);
                        return notesRef[k]()
                    })([
                        'name', 'body',
                        'creationDate',
                        'modificationDate'
                    ])
                ])
            )
        );

    // hashTagsExtracted :: Dict -> Dict
    const hashTagsExtracted = dict => {
        // An updated dictionary, with a list of
        // any hash tag names extracted from .Name
        // .Text, and separately entered as .Tags
        const
            strName = dict['Name'] || '',
            strText = dict['Text'] || '';
        return Boolean(strName || strText) ? (() => {
            const [tplName, tplText] = [strName, strText].map(
                textAndHashTagList
            );
            const
                strPlainText = tplText[0].trim(),
                tags = tplName[1].concat(tplText[1]);
            return Object.assign({}, dict, {
                'Name': tplName[0]
            }, Boolean(strPlainText) ? {
                'Text': strPlainText
            } : {}, 0 < tags.length ? {
                'Tags': tags
            } : {});
        })() : dict;
    };

    // pandocPath :: IO () -> Either String FilePath
    const pandocPath = () => {
        // True if [pandoc](https://pandoc.org) is installed,
        // and we can use if from HTML to a text format
        // like Markdown or MultiMarkdown.
        // standardAdditions (for shell script use of pandoc)
        try {
            return Right(
                Object.assign(
                    Application.currentApplication(), {
                        includeStandardAdditions: true
                    })
                .doShellScript('command -v pandoc')
            );
        } catch (e) {
            return Left('Pandoc not found.');
        }
    };

    // tbxXMLFromForest :: [Tree Dict] -> XML String
    const tbxXMLFromForest = trees =>
        unlines([
            '<?xml version="1.0" encoding="UTF-8" ?>',
            '<tinderbox version="2" revision="12" >',
            xmlTag(false)('item')([])(
                unlines(map(
                    tpl => xmlTag(true)('attribute')(
                        [Tuple('name')(fst(tpl))]
                    )(snd(tpl))
                )([
                    Tuple('Name')('ImportedNote'),
                    Tuple('IsPrototype')('true'),
                    Tuple('NeverComposite')('true'),
                    Tuple('KeyAttributes')('Tags;Created;Modified'),
                    Tuple('HTMLDontExport')('true'),
                    Tuple('HTMLExportChildren')('false')
                ]))
            ),
            unlines(trees.map(
                foldTree(x => xs =>
                    xmlTag(false)('item')(
                        [Tuple('proto')('ImportedNote')]
                    )(
                        // ATTRIBUTES,
                        unlines(Object.keys(x).flatMap(
                            k => {
                                const v = x[k];
                                return 'Text' !== k ? ([
                                    xmlTag(true)('attribute')([
                                        Tuple('name')(k)
                                    ])('Tags' !== k ? (
                                        'Date' !== v.constructor.name ? (
                                            v
                                        ) : iso8601Local(v).replace(
                                            '.000Z', 'Z'
                                        )
                                    ) : v.join(';'))
                                ]) : []
                            })) + (
                            // ANY TEXT,
                            Boolean(x['Text']) ? (
                                '\n' + xmlTag(true)('text')([])(x.Text)
                            ) : ''
                        ) + (
                            // AND ANY CHILDREN.
                            0 < xs.length ? (
                                '\n' + unlines(xs)
                            ) : ''
                        )
                    )
                )
            )),
            '</tinderbox>'
        ]);

    // textAndHashTagList :: String -> (String, [String])
    const textAndHashTagList = s =>
        // A tuple of the tag-stripped body text, and
        // a list of tag names (without their hash-prefixes).
        0 < s.length ? (
            Tuple(s.replace(/#\w+\s?/g, ''))(
                map(compose(tail, fst))(
                    regexMatches(/#\w+/g)(s)
                )
            )
        ) : Tuple('')([]);

    // textTranslatedByPandoc :: Tree Dict -> FilePath -> Tree Dict
    const textTranslatedByPandoc = forest => fpPandoc => {
        // Any HTML mark up in Text fields cleaned up by Pandoc
        // [pandoc](https://pandoc.org)  (if installed)
        const sa = Object.assign(
            Application.currentApplication(), {
                includeStandardAdditions: true
            });
        return forest.map(fmapTree(
            dict => {
                const txt = dict.Text;
                return txt && txt.includes('</') ? (
                    Object.assign({}, dict, {
                        'Text': asPlainText(sa, fpPandoc)(txt)
                    })
                ) : dict;
            }
        ));
    };

    // xmlTag :: String -> [(String, String)] -> String
    const xmlTag = blnSingleLine =>
        // An XML tag of the given name,
        // with any name-value attribute pairs,
        // and an enclosed content string.
        name => kvs => content =>
        `<${name}${
            0 < kvs.length ? (
                ' ' + unwords(kvs.map(
                    kv => kv[0] + '="' + kv[1] + '"'
                ))
            ) : ''
        }>${blnSingleLine ? (
            content
        ) : '\n' + content + '\n'}</${name}>`;


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

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


    // copyText :: String -> IO String
    const copyText = s => {
        // String copied to general pasteboard.
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

    // iso8601Local :: Date -> String
    const iso8601Local = dte =>
        new Date(dte - (6E4 * dte.getTimezoneOffset()))
        .toISOString();


    // setClipOfTextType :: String -> String -> IO String
    const setClipOfTextType = utiOrBundleID =>
        txt => {
            const pb = $.NSPasteboard.generalPasteboard;
            return (
                pb.clearContents,
                pb.setStringForType(
                    $(txt),
                    utiOrBundleID
                ),
                txt
            );
        };

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

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

    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
        // Constructor for a Tree node which connects a
        // value of some kind to a list of zero or
        // more child trees.
        xs => ({
            type: 'Node',
            root: v,
            nest: xs || []
        });

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: 'Tuple',
            '0': a,
            '1': b,
            length: 2
        });

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

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

    // constant :: a -> b -> a
    const constant = k =>
        _ => k;

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

    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
        // True when a and b are equivalent in the terms
        // defined below for their shared data type.
        b => {
            const t = typeof a;
            return t !== typeof b ? (
                false
            ) : 'object' !== t ? (
                'function' !== t ? (
                    a === b
                ) : a.toString() === b.toString()
            ) : (() => {
                const kvs = Object.entries(a);
                return kvs.length !== Object.keys(b).length ? (
                    false
                ) : kvs.every(([k, v]) => eq(v)(b[k]));
            })();
        };

    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = f =>
        1 < f.length ? (
            (a, b) => f(b, a)
        ) : (x => y => f(y)(x));

    // fmapTree :: (a -> b) -> Tree a -> Tree b
    const fmapTree = f =>
        // A new tree. The result of a structure-preserving
        // application of f to each root in the existing tree.
        tree => {
            const go = x => Node(f(x.root))(
                x.nest.map(go)
            );
            return go(tree);
        };

    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f =>
        // The catamorphism on trees. A summary
        // value obtained by a depth-first fold.
        tree => {
            const go = x => f(x.root)(
                x.nest.map(go)
            );
            return go(tree);
        };

    // fst :: (a, b) -> a
    const fst = tpl =>
        // First member of a pair.
        tpl[0];

    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = fEq => xs =>
        // // Typical usage: groupBy(on(eq)(f), xs)
        0 < xs.length ? (() => {
            const
                tpl = xs.slice(1).reduce(
                    (gw, x) => {
                        const
                            gps = gw[0],
                            wkg = gw[1];
                        return fEq(wkg[0])(x) ? (
                            Tuple(gps)(wkg.concat([x]))
                        ) : Tuple(gps.concat([wkg]))([x]);
                    },
                    Tuple([])([xs[0]])
                );
            return tpl[0].concat([tpl[1]])
        })() : [];

    // identity :: a -> a
    const identity = x =>
        // The identity function. (`id`, in Haskell)
        x;

    // isPrefixOf :: [a] -> [a] -> Bool
    // isPrefixOf :: String -> String -> Bool
    const isPrefixOf = xs =>
        // True if xs is a prefix of ys.
        ys => {
            const go = (xs, ys) => {
                const intX = xs.length;
                return 0 < intX ? (
                    ys.length >= intX ? xs[0] === ys[0] && go(
                        xs.slice(1), ys.slice(1)
                    ) : false
                ) : true;
            };
            return 'string' !== typeof xs ? (
                go(xs, ys)
            ) : ys.startsWith(xs);
        };

    // length :: [a] -> Int
    const length = xs =>
        // Returns Infinity over objects without finite
        // length. This enables zip and zipWith to choose
        // the shorter argument when one is non-finite,
        // like cycle, repeat etc
        (Array.isArray(xs) || 'string' === typeof xs) ? (
            xs.length
        ) : Infinity;

    // // lookup :: String -> Dict -> a
    // const lookup = k =>
    //     // The value returned from obj by method k
    //     // Not a total function – assumes that k exists.
    //     obj => {
    //         const method = obj[k];
    //         return method.exists ? (
    //             method()
    //         ) : undefined;
    //     };

    // map :: (a -> b) -> [a] -> [b]
    const map = f =>
        // The list obtained by applying f to each element of xs.
        // (The image of xs under f).
        xs => (Array.isArray(xs) ? (
            xs
        ) : xs.split('')).map(f);

    // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
    const on = f =>
        // e.g. sortBy(on(compare,length), xs)
        g => a => b => f(g(a))(g(b));

    // regexMatches :: Regex -> String -> [[String]]
    const regexMatches = rgx =>
        // All matches of the given (global /g) regex in
        strHay => {
            let m = rgx.exec(strHay),
                xs = [];
            while (m)(xs.push(m), m = rgx.exec(strHay));
            return xs;
        };

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(x => JSON.stringify(x, null, 2))
            .join(' -> ')
        );

    // snd :: (a, b) -> b
    const snd = tpl => tpl[1];

    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        xs => xs.slice()
        .sort(uncurry(f));

    // tail :: [a] -> [a]
    const tail = xs =>
        // A new list consisting of all
        // items of xs except the first.
        0 < xs.length ? xs.slice(1) : [];


    // take :: Int -> [a] -> [a]
    // take :: Int -> String -> String
    const take = n =>
        // The first n elements of a list,
        // string of characters, or stream.
        xs => 'GeneratorFunction' !== xs
        .constructor.constructor.name ? (
            xs.slice(0, n)
        ) : [].concat.apply([], Array.from({
            length: n
        }, () => {
            const x = xs.next();
            return x.done ? [] : [x.value];
        }));


    // transpose :: [[a]] -> [[a]]
    const transpose = rows =>
        // The columns of the input transposed
        // into new rows.
        // Simpler version of transpose_, assuming input
        // rows of even length.
        0 < rows.length ? rows[0].map(
            (x, i) => rows.flatMap(
                x => x[i]
            )
        ) : [];

    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (x, y) => f(x)(y);


    // unlines :: [String] -> String
    const unlines = xs =>
        // A linefeed-delimited string constructed
        // from the list of lines in xs.
        xs.join('\n');

    // unwords :: [String] -> String
    const unwords = xs => xs.join(' ');

    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
        // Use of `take` and `length` here allows for zipping with non-finite
        // lists - i.e. generators like cycle, repeat, iterate.
        ys => {
            const
                lng = Math.min(length(xs), length(ys)),
                vs = take(lng)(ys);
            return take(lng)(xs).map(
                (x, i) => Tuple(x)(vs[i])
            );
        };

    // MAIN ---
    return main();
})();

This script performed a few nested notes, then stops; the Script Editor shows “Running…”

That’s an encouraging sign! If it shows “Running…” then it hasn’t stopped but is still processing. If you have long text in some of your notes then it needs to spend more time stripping the tags.

I just retried the script on 800+ notes after organizing them into nested folders and the script performed as expected (slow, but reliable and with no dependencies). The result of this “barebones” approach is a “flattened” presentation of containers in Tinderbox corresponding to the folder names in Notes, with each container in Tinderbox containing the corresponding notes. Then I can quickly reorganize in Tinderbox to suit.

@ComplexPoint -
Dialog box shows: “Error on line 107: Error: Can’t convert types.”
Results pane shows: “Error -1700: Can’t convert types.”

@sumnerg - I let it run for about 45 minutes, the note count in TB didn’t go above the initial 3, and the “Running…” message persisted.
There are 284 messages of which 90% are under 300 words, and none are beyond 4,000 words.

284 messages of which 90% are under 300 words, and none are beyond 4,000 words

The script certainly is not optimized for such long notes–thousands of words in some of the notes and hundreds of words in many others! It might finish if you gave it a few hours, assuming Tinderbox can accept text thousands of words long in a note. I have some ideas on how to speed up the AppleScript part, but suggest you might want to check on any relevant design limitations in Tinderbox regarding length of text.

1 Like

From the sidelines… I don’t believe Tinderbox has any hard limits on $Text size, but: I think it definitely the case that Tinderbox works better with shorter notes. Long notes tend to slow down note context (selection) switching as the myriad of things associated with rendering the new selection take longer. I’m not sure, but I’d imagine heavily (RTF) formatted text may add to this effect.

The problem for the designer is if people concentrate on hard limits it generates un-productive discussion of said limits. Tinderbox is self-described as a ‘tool for notes’. Though the term ‘note’ doesn’t define a length, IME a note is generally shorter—and shorter than the thing it annotates.

Bottom line, I’d suggest braking the items at source into smaller notes before ingest. For instance, the Tinderbox note that produces this aTbRef page has 1,121 words. In the Tinderbox app it feels ‘long’. My hunch is about half that is more manageable, ergo if splitting items in notes, perhaps look at those of >500 words and split them into a series of notes.

2 Likes

Thanks @sumnerg and @mwra - my notes are normally 50-100 words in length.

However, this is a special-use scenario for a one-time salvage out of Notes. Since I can’t afford to hold up my workflow until Apple fixes scripting, I’m abandoning Notes and taking all valuables with me.

I’ve ONLY used Notes for TB import anyway; it will be simple (beyond the export hump) to switch my workflow to Watching text/rtf files in a Dropbox (or similar) folder.

1 Like

Good to know - I’ll run it again and let it go for as long. Will report if and when the script wraps.

And in the Replies panel (rather than Results) ?

( the Replies panel needs to be clicked and opened before the script runs )

2 Likes

This one, still without frills and AppleScripty slow, will run much faster than the original, especially with longish body text in notes:

tell application "Notes"
	set {theNames, theBodies, theCreates, theModifies} to {name, body, creation date, modification date} of notes
	set theContainers to {}
	repeat with aNote in notes
		set end of theContainers to name of (get container of aNote)
	end repeat
end tell

tell front document of application "Tinderbox 8"
	repeat with n from 1 to (length of theNames)
		set myNote to make new note with properties {name:item n of theNames}
		tell myNote
			set value of attribute "Text" to my removeMarkup(item n of theBodies)
			set value of attribute "Created" to my dateToStr(item n of theCreates)
			set value of attribute "Modified" to my dateToStr(item n of theModifies)
			set value of attribute "Container" to item n of theContainers -- TB automatically creates if doesn't yet exist!
		end tell
	end repeat
end tell
display notification "Imported " & length of theNames & " notes from Apple Notes"

# HANDLERS (=subroutines)
to dateToStr(aDate) --> convert AppleScript date to string format that Tinderbox recognizes
	tell aDate to return short date string & ", " & time string
end dateToStr

to removeMarkup(someText)
	set text item delimiters to {"<div>", "</div>", "<br>", "<b>", "</b>", "<i>", "</i>", "<h1>", "</h1>"}
	set textItems to text items of someText
	set text item delimiters to ""
	return textItems as text
end removeMarkup

It removes the most common markup in export from Apple Notes. If you have other fancy formatting in Notes (including perhaps stuff copy-pasted in from the web) you may need to add to the list of tags to be removed after set text item delimiters to.

About text length

Tinderbox will be fine with very large texts. Doing tech support, I’ve seen at least one Tinderbox document in which each note was, literally, a novel — the entire novel was pasted into each note. That was a lot of text!

That said, lots of the things Tinderbox is good at work best when a note is small and focused. Agents and links are simply more informative when working on small, specific notes.

A few things don’t scale very well with exceptionally long texts. Text link updating needs to be performed after each keystroke and, if you have n characters and m text links, runs in O(nm) time.

2 Likes

It IS working! Another full container appeared in TB :+1:t4:

3 Likes

@ComplexPoint it’s long-ish. Attaching as a text file.

Ver006_RepliesPane.zip (2.1 KB)

Thanks ! It looks as if there is a particular note or folder on which it’s choking. Perhaps some kind of embedded image or other data.

Interesting that the very first AppleScript experiment, above seemed to complete - the main difference between that and the subsequent JS tests was that the AS version didn’t attempt to capture container names.

I wonder if there is something subtle or different about one of the containers …

A bit hard to test from here :slight_smile:

1 Like

It turns out extracting tags embedded in the body text is not that hard to do using evaluate. Have incorporated that here.

tell application "Notes"
	set {theNames, theBodies, theCreates, theModifies} to {name, body, creation date, modification date} of notes
	set theContainers to {}
	repeat with aNote in notes
		set end of theContainers to name of (get container of aNote)
	end repeat
end tell

set tbCode to "runCommand(\"grep -o '#[a-zA-Z0-9_]\\+'\",$Text).replace('\\r',';').replace('#','')"

tell front document of application "Tinderbox 8"
	repeat with n from 1 to (length of theNames)
		set myNote to make new note with properties {name:item n of theNames}
		tell myNote
			set theText to my removeMarkup(item n of theBodies)
			set value of attribute "Text" to theText
			set value of attribute "Created" to my dateToStr(item n of theCreates)
			set value of attribute "Modified" to my dateToStr(item n of theModifies)
			set mayHaveImbeddedTags to theText contains "#" -- boolean -- to avoid extra work
			if mayHaveImbeddedTags then set value of attribute "Tags" to evaluate it with tbCode
			set value of attribute "Container" to item n of theContainers -- TB automatically creates if doesn't yet exist!
		end tell
	end repeat
end tell
display notification "Imported " & length of theNames & " notes from Apple Notes"

# HANDLERS (=subroutines)
to dateToStr(aDate) --> convert AppleScript date to string format that Tinderbox recognizes
	tell aDate to return short date string & ", " & time string
end dateToStr

to removeMarkup(someText)
	set text item delimiters to {"<div>", "</div>", "<br>", "<b>", "</b>", "<i>", "</i>", "<h1>", "</h1>"}
	set textItems to text items of someText
	set text item delimiters to ""
	return textItems as text
end removeMarkup

@ComplexPoint

Capturing container names from Apple Notes had me stumped for a while. It doesn’t seem possible to easily grab them the way one can other properties of a note. Instead a container seems to be a folder id.

So in AppleScript I ended up having to “get” a note’s container and then reference the name of that.

For example, this doesn’t work:

tell first note to set theContainer to name of its container

But this does work:

tell first note to set theContainer to name of (get its container)

That would mean more to you than to me and I thought I’d pass it on in case it provides a clue useful for the JavaScript.

1 Like

So here’s the latest -
@sumnerg - the 2nd and 3rd script are halting with an error -
“Notes got an error: Can’t make show id “x-coredata://7F02A163-47E0-4A53-9F5E-C5D15BF8BB0A/ICFolder/p261” into type specifier.”

I’m attaching the tail part of the “Reply” text where it seems to hang.
sumner03-AS_Reply.zip (5.2 KB)

However, the first script absolutely worked!! Brilliant. Thank you SO much :blush:

Noted with thanks, @ComplexPoint! Once I complete salvaging these notes, I’ll create a limited set of notes and test and share my findings.
Thanks again!!

Yes, that looks like the folder id in the “capturing the container name” part of the script. My first (inefficient) script just cycled through the folders, avoiding that problem. The second and third scripts tried to be more efficient, and indeed are much faster on my machine. Could be that approach doesn’t work in Catalina. Or maybe @ComplexPoint will have insights. He’s a true expert at scripting.

In any case, glad to hear the first one worked for you in Catalina. I hope it didn’t take too many hours to do its thing.

1 Like