-
Notifications
You must be signed in to change notification settings - Fork 215
Description
It would be great if images could be paste in the editor and automatically stored to a file next to the markdown file, for example by storing it under an assets folder.
I've a prototype for this feature that uses a custom html file to observe "paste" events and replace any images with their base64-encoded version, and a helper C# project that monitors changes to the .md files, extract inline images and stores them in an assets folder next to the .md file. It would be great to have this kind of functionality out of the box.
Code for listening and modifying paste events on the editor:
<script>
(function() {
const hookedEditors = new Set();
const readSyncDataURL=function(file) {
var url=URL.createObjectURL(file);
var xhr=new XMLHttpRequest();
xhr.open("GET",url,false);
xhr.overrideMimeType("text/plain; charset=x-user-defined");
xhr.send();
URL.revokeObjectURL(url);
var returnText="";
for (var i=0;i<xhr.responseText.length;i++){
returnText+=String.fromCharCode(xhr.responseText.charCodeAt(i)&0xff);};
return "data:"+file.type+";base64,"+btoa(returnText);
}
function setupImagePasteHook(editor) {
if (hookedEditors.has(editor)) return;
hookedEditors.add(editor);
editor.addEventListener('paste', (event) => {
const items = (event.clipboardData || event.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.indexOf('image') !== -1) {
event.preventDefault();
event.stopPropagation();
const base64Data = readSyncDataURL(item.getAsFile());
const markdownImage = ``;
console.log(markdownImage);
var dT = null;
try{ dT = new DataTransfer();} catch(e){}
var evt = new ClipboardEvent('paste', {clipboardData: dT});
console.log('clipboardData available: ', !!evt.clipboardData);
evt.clipboardData.setData('text/plain', markdownImage);
event.target.dispatchEvent(evt);
}
}
}, true);
}
function scanAndHook() {
var retypeEditor = document.body.querySelector("#retype-editor-root");
if (typeof retypeEditor === 'undefined' || !retypeEditor) return;
setupImagePasteHook(retypeEditor);
}
const observer = new MutationObserver((mutations) => {
let shouldCheck = false;
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
shouldCheck = true;
break;
}
}
if (shouldCheck) {
setTimeout(scanAndHook, 100);
}
});
document.addEventListener('DOMContentLoaded', function() {
observer.observe(document.body, {
childList: true,
subtree: true
});
scanAndHook();
}, false);
console.log("Clipboard Image to Base64 Text handler active.");
})();
</script>Helper project that monitors for changes to the markdown files and extracts any images
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Diagnostics;
var base64ImageRegex = new Regex(@"!\[(?<subtitle>.*?)\]\(data:image\/(?<ext>png|jpg|jpeg|gif);base64,(?<data>.*?)\)", RegexOptions.Compiled);
string path = Directory.GetCurrentDirectory();
Console.WriteLine($"Image: Starting retype & watching for .md changes in: {path}");
var tcs = new TaskCompletionSource();
using var watcher = new FileSystemWatcher(path);
watcher.Filter = "*.md";
watcher.IncludeSubdirectories = true;
watcher.NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName;
watcher.Changed += OnChanged;
watcher.Created += OnChanged;
watcher.EnableRaisingEvents = true;
var pi = new ProcessStartInfo()
{
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
Arguments = "start --pro",
FileName = "retype",
WorkingDirectory = path,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
var process = new Process
{
StartInfo = pi,
EnableRaisingEvents = true
};
process.ErrorDataReceived += Retype_ErrorDataReceived;
process.OutputDataReceived += Retype_OutputDataReceived;
process.Exited += Retype_Exited;
Console.CancelKeyPress += (_, __) =>
{
try
{
process.Kill();
}
catch
{
//Ignore
}
tcs.TrySetResult();
Environment.Exit(0);
};
void Retype_ErrorDataReceived(object sender, DataReceivedEventArgs e)
{
Console.WriteLine($"Retype: Error: {e?.Data ?? ""}");
}
void Retype_OutputDataReceived(object sender, DataReceivedEventArgs e)
{
Console.WriteLine($"Retype: {e?.Data ?? ""}");
}
void Retype_Exited(object sender, EventArgs e)
{
tcs.TrySetResult();
}
try
{
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
}
catch (Exception E)
{
Console.WriteLine($"Retype: Error starting retype: {E.Message}");
tcs.TrySetException(E);
}
await tcs.Task;
void OnChanged(object sender, FileSystemEventArgs e)
{
// Small delay to ensure the writing process has finished and released the file lock
Thread.Sleep(500);
try
{
ProcessFile(e.FullPath);
}
catch (IOException ex)
{
Console.WriteLine($"Image: Could not access file {e.Name}: {ex.Message}");
}
}
void ProcessFile(string filePath)
{
string content = File.ReadAllText(filePath);
var newContent = base64ImageRegex.Replace(content, match =>
{
string mdDir = Path.GetDirectoryName(filePath);
string assetsDir = Path.Combine(mdDir, "assets");
if (!Directory.Exists(assetsDir))
{
Directory.CreateDirectory(assetsDir);
Console.WriteLine($"Image: Created assets directory: {assetsDir}");
}
string subtitle = match.Groups["subtitle"].Value;
string extension = match.Groups["ext"].Value;
string base64Data = match.Groups["data"].Value;
try
{
byte[] imageBytes = Convert.FromBase64String(base64Data);
// Create a short hash of the data to keep filenames unique but consistent
string hash = GetShortHash(base64Data);
string safeSubtitle = GetSafeFilename(subtitle);
string fileName = $"{safeSubtitle}_{hash}.{extension}";
string savePath = Path.Combine(assetsDir, fileName);
if (!File.Exists(savePath))
{
File.WriteAllBytes(savePath, imageBytes);
Console.WriteLine($"Image: Found image: {fileName} from {Path.GetFileName(filePath)}");
}
return $"";
}
catch (Exception ex)
{
Console.WriteLine($"Image: Error processing image '{subtitle}': {ex.Message}");
return match.Value;
}
});
if (newContent != content)
{
File.WriteAllText(filePath, newContent);
}
}
string GetSafeFilename(string name)
{
foreach (char c in Path.GetInvalidFileNameChars())
name = name.Replace(c, '_');
return string.IsNullOrWhiteSpace(name) ? "image" : name.Replace(" ", "_");
}
string GetShortHash(string input)
{
using var sha1 = SHA1.Create();
var hash = sha1.ComputeHash(Encoding.UTF8.GetBytes(input));
return BitConverter.ToString(hash).Replace("-", "").Substring(0, 8).ToLower();
}The C# code above is responsible for starting and stoping retyped, so it's run as:
dotnet run --project .\_helpers\retype-helper.csproj --