Assuming the context of working with a text file and a Markdown previewer:
If there is an image in the clipboard, this macro:
- Opens a Save As dialog:
- pointing at the folder in which the currently edited text file is saved,
- and defaulting to a date-stamped file name for the graphic.
- saves the graphic content of the clipboard to the chosen file path,
- and pastes a markdown link with a

bang prefix at the current position in the text file.
The idea is that you can then view the text file, with inline images, in the Markdown previewer.
I personally work, most of the time, with the TaskPaper plain text outliner and with Brett Tersptra's Marked 2 previewer.
Paste image as MD link.kmmacros (30.4 KB)
JS Source
(() => {
'use strict';
// Paste any image in clipboard to a file in
// the same folder as active (text) document.
// Where successful, return an MD image file link
// of the form  for pasting.
// Rob Trew @
// Ver 0.03
ObjC.import('AppKit');
// --------------------- OPTIONS ---------------------
// .tiff files can be rather large, so the default
// here is to save smaller PNG files.
// If you are typically saving *vector* images you may
// prefer to set `saveTiffAsPNG = false`
// and preserve full vector scalability, at the cost
// of significantly larger files.
// saveTiffAsPNG :: Bool
const saveTiffAsPNG = true;
// preferredTypes :: [String]
const preferredTypes = ['.pdf', '.png', '.tiff'];
// ---------------------- MAIN -----------------------
// main :: ()
const main = () => either(
alert('Page image to MD link in text')
)(
([mdLink, appID]) => (
Application(appID).activate(),
delay(0.5),
mdLink
)
)(
bindLR(
filePathAndAppIDFromFrontWindowLR()
)(([fpDocFile, bundleID]) => bindLR(
chosenTypeInClipboardLR(preferredTypes)
)(uti => bindLR(
confirmSavePathLR(
defaultPath(saveTiffAsPNG)(fpDocFile)(uti)
)
)(fpImage => bindLR(
((uti === 'public.tiff' && saveTiffAsPNG) ? (
tiffClipboardWrittenToFileAsPNGLR
) : clipboardWrittenToFileLR(uti))(fpImage)
)(
fpChecked => Right([
mdImageLinkForFilePath(fpChecked),
bundleID
])
))))
);
// defaultPath :: Bool -> FilePath -> UTI -> IO FilePath
const defaultPath = saveTiffAsPNG =>
fpDocFile => uti => combine(
takeDirectory(fpDocFile)
)(
'copied' + iso8601Now() + (
'public.tiff' !== uti ? (
takeExtension(uti)
) : saveTiffAsPNG ? (
'.png'
) : '.tiff'
)
)
.split(':')
.join('-');
// mdImageLinkForFilePath :: FilePath -> String
const mdImageLinkForFilePath = fp =>
`})`;
// -------------------- CLIPBOARD --------------------
// chosenTypeInClipboardLR :: [String] -> Either String UTI
const chosenTypeInClipboardLR = preferredExtensions => {
const
matches = typesInClipboard().filter(
uti => preferredExtensions.includes(
takeExtension(uti)
)
);
return 0 < matches.length ? (
Right(matches[0])
) : Left(
'No clipboard content with type drawn from:\n\t{' + (
preferredExtensions.join(', ') + (
'}\n\nFound only:\n\t' + (
typesInClipboard().join('\n\t')
)
)
)
);
};
// typesInClipboard :: () -> IO [UTI]
const typesInClipboard = () =>
ObjC.deepUnwrap(
$.NSPasteboard.generalPasteboard
.pasteboardItems.js[0].types
);
// clipboardWrittenToFileLR :: UTI -> FilePath ->
// Either IO String IO FilePath
const clipboardWrittenToFileLR = uti =>
fp => (
$.NSPasteboard.generalPasteboard
.pasteboardItems.js[0]
.dataForType(uti)
.writeToFileAtomically(fp, true),
doesFileExist(fp) ? (
Right(fp)
) : Left(
"${uti} clipboard not be written to:" + (
`\t\t${fp}`
)
)
);
// tiffClipboardWrittenToFileAsPNGLR :: UTI -> FilePath ->
// Either IO String IO FilePath
const tiffClipboardWrittenToFileAsPNGLR = fp =>
typesInClipboard().includes('public.tiff') ? (
// In the pasteboard and file system,
$.NSBitmapImageRep.imageRepWithData(
$.NSPasteboard.generalPasteboard
.pasteboardItems.js[0]
.dataForType('public.tiff')
)
.representationUsingTypeProperties(
$.NSPNGFileType, $()
)
.writeToFileAtomically(fp, true),
// and thence back to the JS interpreter.
doesFileExist(fp) ? (
Right(fp)
) : Left(
"${uti} clipboard not be written to:" + (
`\t\t${fp}`
)
)
) : Left('No public.tiff content found in clipboard');
// ----------------------- 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
);
};
// confirmSavePathLR :: FilePath -> Either Message FilePath
const confirmSavePathLR = fp => (
([fldr, fname], sa) => {
sa.activate();
try {
return Right(
sa.chooseFileName({
withPrompt: 'Save As:',
defaultName: fname,
defaultLocation: Path(ObjC.unwrap(
$(doesDirectoryExist(fldr) ? (
fldr
) : '~')
.stringByExpandingTildeInPath
))
})
.toString()
);
} catch (e) {
return Left(e.message);
}
})(
Array.from(splitFileName(fp)),
Object.assign(Application('System Events'), {
includeStandardAdditions: true
})
);
// doesDirectoryExist :: FilePath -> IO Bool
const doesDirectoryExist = fp => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(fp)
.stringByStandardizingPath, ref
) && ref[0];
};
// doesFileExist :: FilePath -> IO Bool
const doesFileExist = fp => {
const ref = Ref();
return $.NSFileManager.defaultManager
.fileExistsAtPathIsDirectory(
$(fp)
.stringByStandardizingPath, ref
) && 1 !== ref[0];
};
// filePathAndAppIDFromFrontWindowLR :: () ->
// Either String (FilePath, String)
const filePathAndAppIDFromFrontWindowLR = () => {
// ObjC.import ('AppKit')
const
appName = ObjC.unwrap(
$.NSWorkspace.sharedWorkspace
.frontmostApplication.localizedName
),
appProcess = Application('System Events')
.applicationProcesses.byName(appName),
ws = appProcess.windows;
return bindLR(
0 < ws.length ? Right(
ws.at(0).attributes.byName('AXDocument').value()
) : Left(`No document windows open in ${appName}.`)
)(
docURL => null !== docURL ? (
Right([
decodeURIComponent(docURL.slice(7)),
appProcess.bundleIdentifier()
])
) : Left(`No saved document active in ${appName}.`)
);
};
// iso8601Now :: () -> IO String
const iso8601Now = () =>
iso8601Local(new Date())
.split('.')
.join('')
.slice(0, -1);
// iso8601Local :: Date -> String
const iso8601Local = dte =>
new Date(dte - (6E4 * dte.getTimezoneOffset()))
.toISOString();
// --------------------- 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
});
// 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);
// combine (</>) :: FilePath -> FilePath -> FilePath
const combine = fp =>
// Two paths combined with a path separator.
// Just the second path if that starts
// with a path separator.
fp1 => Boolean(fp) && Boolean(fp1) ? (
'/' === fp1.slice(0, 1) ? (
fp1
) : '/' === fp.slice(-1) ? (
fp + fp1
) : fp + '/' + fp1
) : fp + fp1;
// compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
const compose = (...fs) =>
// A function defined by the right-to-left
// composition of all the functions in fs.
fs.reduce(
(f, g) => x => f(g(x)),
x => x
);
// 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;
// last :: [a] -> a
const last = xs =>
// The last item of a list.
0 < xs.length ? (
xs.slice(-1)[0]
) : undefined;
// splitFileName :: FilePath -> (String, String)
const splitFileName = strPath =>
// Tuple of directory and file name, derived from file path.
// Inverse of combine.
('' !== strPath) ? (
('/' !== strPath[strPath.length - 1]) ? (() => {
const
xs = strPath.split('/'),
stem = xs.slice(0, -1);
return stem.length > 0 ? (
Tuple(stem.join('/') + '/')(xs.slice(-1)[0])
) : Tuple('./')(xs.slice(-1)[0]);
})() : Tuple(strPath)('')
) : Tuple('./')('');
// takeDirectory :: FilePath -> FilePath
const takeDirectory = fp =>
'' !== fp ? (
(xs => xs.length > 0 ? xs.join('/') : '.')(
fp.split('/').slice(0, -1)
)
) : '.';
// takeExtension :: FilePath -> String
const takeExtension = fp => (
fs => {
const fn = last(fs);
return fn.includes('.') ? (
'.' + last(fn.split('.'))
) : '';
}
)(fp.split('/'));
// takeFileName :: FilePath -> FilePath
const takeFileName = fp =>
'' !== fp ? (
'/' !== fp[fp.length - 1] ? (
fp.split('/').slice(-1)[0]
) : ''
) : '';
return main();
})();