For comparison, or anyone using JS, we could write, either in a briefer idiom (not really my thing, as it happens):
Relatively parsimonious JS
(() => {
'use strict';
const main = () => {
const docs = Application('Tinderbox').documents;
return 0 < docs.length ? (() => {
const
selns = docs[0].selections,
tags = 0 < selns.length ? (
Array.from(new Set(
concatMap(
s => s.split(';')
.filter(k => 0 < k.trim().length),
selns.attributes.byName('Tags').value()
)
))
) : [],
sa = Object.assign(Application('System Events'), {
includeStandardAdditions: true
}),
strList = tags.sort().join('\n')
return (
sa.setTheClipboardTo(strList),
strList
);
})() : 'No document open';
};
// GENERIC FUNCTIONS ----------------------------------
// https://github.com/RobTrew/prelude-jxa
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) => [].concat(...xs.map(f))
// MAIN ---
return console.log(
main()
);
})();
or
Slightly fuller JS (alerts, and slightly more explanatory messages)
(() => {
'use strict';
const main = () => {
const strTitle = 'Tags in selected Tinderbox notes';
return either(
// A message (why no tags – in the Left channel),
msg => alert(strTitle)(msg),
// OR a list of tags (in the Right channel).
strTagList => (
sa.setTheClipboardTo(strTagList),
alert(strTitle)(
'Copied to clipboard:\n\n' + strTagList
),
strTagList
),
bindLR(
bindLR( // If a document is found, bind it to the name 'doc'.
tbxFrontDocLR(),
doc => 0 < doc.selections.length ? (
Right(doc.selections)
) : Left('No notes selected in: ' + doc.name())
),
// If the selection is not empty, bind it to the name 'selns'.
selns => {
const
tags = Array.from(new Set(
concatMap(
s => s.split(';')
.map(x => x.trim())
.filter(k => 0 < k.length),
selns.attributes.byName('Tags').value()
)
));
return 0 < tags.length ? (
Right(tags.sort().join('\n'))
) : Left(
'No tags attached to selected notes.'
);
}
)
);
};
// TINDERBOX ------------------------------------------
// tbxFrontDocLR :: Tbx IO () -> Either String Tbx Doc
const tbxFrontDocLR = () => {
// Either the front document in Tinderbox 8, or an
// explanatory message if no documents are open,
// or Tinderbox 8 is not running.
const tbx = Application('Tinderbox 8');
return tbx.running() ? (() => {
const ds = tbx.documents;
return 0 < ds.length ? (
Right(ds.at(0))
) : Left('No documents open in Tinderbox');
})() : Left('Tinderbox 8 is not running.');
};
// JXA ------------------------------------------------
const
sa = Object.assign(Application('System Events'), {
includeStandardAdditions: true
});
// alert :: String => String -> IO String
const alert = title => s => (
sa.activate(),
sa.displayDialog(s, {
withTitle: title,
buttons: ['OK'],
defaultButton: 'OK',
withIcon: sa.pathToResource('AppIcon.icns', {
inBundle: 'Applications/Tinderbox 8.app'
})
}),
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);
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) =>
xs.reduce((a, x) => a.concat(f(x)), []);
// 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;
// MAIN ---
return main();
})();