You could test this variant to see if it matches what you are after – it experimentally trims the time component off interval expressions if the unit is a day
or larger, but leaves it in place where N hours
or N minutes
are given.
Convert parenthesised informal date to ISO (draft alternative,).kmmacros (31.2 KB)
JS Source
(() => {
'use strict';
// Rob Trew @2020
// Draft 0.02
// -- Rob Trew (c) 2020
// --
// -- Permission is hereby granted, free of charge,
// -- to any person obtaining a copy of this software
// -- and associated documentation files (the "Software"),
// -- to deal in the Software without restriction,
// -- including without limitation the rights to use, copy,
// -- modify, merge, publish, distribute, sublicense,
// -- and/or sell copies of the Software, and to permit persons
// -- to whom the Software is furnished to do so,
// -- subject to the following conditions:
// -- *******
// -- The above copyright notice and this permission notice
// -- shall be included in ALL copies
// -- or substantial portions of the Software.
// -- *******
// -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// -- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// -- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
// -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
// -- OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// main :: IO ()
const main = () => {
const inner = () => {
const
ds = Application('TaskPaper')
.documents;
return either(
alert('Problem')
)(
x => x
)(
bindLR(
0 < ds.length ? (
Right(ds.at(0))
) : Left('No TaskPaper documents open')
)(
d => d.evaluate({
script: tp3Context.toString(),
withOptions: {
tagName: 'now'
}
})
)
);
};
// -------------- TASKPAPER CONTEXT --------------
const tp3Context = (editor, options) => {
const
rgxInterval = (
/^\s*\d+\s*(d|w|week|month|y|year)/
);
const main = () => {
const lrExtended = extendedSelectionLR(
editor
);
return bindLR(
Boolean(lrExtended.Left) ? (
selectedTagValueAndPosnLR(editor)
) : lrExtended
)(
([phrase, from]) => {
const parse = DateTime.format(phrase);
return bindLR(
'invalid date' !== parse ? (
Right(
rgxInterval.test(phrase) ? (
parse.slice(0, 10)
) : parse
)
) : Left(`${parse} :: ${phrase}`)
)(
isoString => (
// ----- EDITOR TEXT -----
editor.outline
.groupUndoAndChanges(() =>
editor.selection.startItem
.replaceBodyRange(
from,
phrase.length,
isoString
)
),
// ---- OUTPUT STREAM ----
Right(`${phrase} -> ${isoString}`)
)
);
}
);
};
// selectedTagValueAndPosnLR :: Editor ->
// Either String (String, Int, Int)
const selectedTagValueAndPosnLR = editor => {
// Either a message or some parenthesised text.
// From preceding opening parenthesis if any
// to next closing parenthesis on same line,
// if any.
const
seln = editor.selection,
txt = seln.startItem.bodyString,
iPosn = seln.startOffset,
iClose = [...txt.slice(iPosn)]
.findIndex(
c => ')' === c
),
iOpen = [...txt.slice(0, iPosn)]
.reverse().findIndex(
c => '(' === c
),
value = dropWhileEnd(
c => c === ')'
)(txt);
return Right(
-1 === iOpen ? [
value, 0
] : [
value.slice(
iPosn - iOpen,
-1 !== iClose ? (
iPosn + iClose
) : undefined
),
iPosn - iOpen
]
);
};
// extendedSelectionLR :: Editor ->
// Either String String
const extendedSelectionLR = editor => {
// Either a message or the text selected
// in the first selected item.
const
seln = editor.selection,
start = seln.startOffset;
return seln.isCollapsed ? (
Left('Selection not extended')
) : Right([
seln.startItem.bodyString
.slice(
start,
seln.endOffset
),
start
]);
};
// ----------------- 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
});
// bindLR (>>=) :: Either a ->
// (a -> Either b) -> Either b
const bindLR = m =>
mf => undefined !== m.Left ? (
m
) : mf(m.Right);
// dropWhileEnd :: (a -> Bool) -> [a] -> [a]
// dropWhileEnd :: (Char -> Bool) -> String -> [Char]
const dropWhileEnd = p =>
// xs without the longest suffix for which
// p returns true for all elements.
xs => {
let i = xs.length;
while (i-- && p(xs[i])) {}
return xs.slice(0, i + 1);
};
// findIndex :: (a -> Bool) -> [a] -> Maybe Int
const findIndex = p =>
// Just the index of the first element in
// xs for which p(x) is true, or
// Nothing if there is no such element.
xs => {
const i = [...xs].findIndex(p);
return -1 !== i ? (
Just(i)
) : Nothing();
};
return main();
};
// 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',
withIcon: sa.pathToResource('TaskPaper.icns', {
inBundle: 'Applications/TaskPaper.app'
})
}),
s
);
};
return inner()
};
// ----------------- LIBRARY IMPORT ------------------
// Evaluate a function f :: (() -> a)
// in the context of the JS libraries whose source
// filePaths are listed in fps :: [FilePath]
// Evaluate a function f :: (() -> a)
// in the context of the JS libraries whose source
// filePaths are listed in fps :: [FilePath]
// usingLibs :: [FilePath] -> (() -> a) -> a
const usingLibs = (fps, f) => {
const gaps = fps.filter(fp => !doesFileExist(fp));
return 1 > gaps.length ? eval(
`(() => {
'use strict';
${fps.map(readFile).join('\n\n')}
return (${f})();
})();`
) : 'Library not found at: ' + gaps.join('\n');
};
// doesFileExist :: FilePath -> IO Bool
const doesFileExist = strPath => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(strPath)
.stringByStandardizingPath, ref
) && ref[0] !== 1;
};
// readFile :: FilePath -> IO String
const readFile = strPath => {
let error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
$(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return Boolean(error.code) ? (
ObjC.unwrap(error.localizedDescription)
) : str;
};
// -------- GENERIC FUNCTIONS FOR JXA CONTEXT --------
// 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 =>
// 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 => 'Either' === e.type ? (
undefined !== e.Left ? (
fl(e.Left)
) : fr(e.Right)
) : undefined;
// ---------------------- MAIN -----------------------
return main();
})();