Add Custom Icons and Commands on the Editor Toolbar
The Editor toolbar at the top of the Window allows embedding of common Markdown markup on the selected text. The toolbar is kept relatively simple and so it doesn't wrap all operations that are supported.
It's possible to extend the toolbar with custom operations:
- Adding additional built-in commands
- Adding custom HTML markup commands
You can do this by using the Configuration.Editor.AdditionalToolbarIcons which is a collection of an icon name plus a an action name. For example:
"Editor": {
"AdditionalToolbarIcons": {
// internal MM Editor Commands
"Pencil": "mark",
"ArrowDown": "small",
// html tag wrapping
"Coffee": "html|cite",
// template for text selection
"Bolt": "html|<b><i>{0}</i></b>"
},
![]()
The item in each map is a FontAwesome 6 Icon to be used as an icon, the second is the name of an existing command. The last (Coffee) example, uses html|cite as the value which results in custom tag created around the text as <cite>text</cite>.
Built-in Commands
Built in commands handle the visible toolbar operations, plus a few additional ones that are not actually shown on the toolbar. The following 3 commands are not already on the toolbar:
small- adds text in<small>txt</small>mark- adds textin<mark>txt</mark>numberedlist- formats lines of text as a numbered listpagebreak- inserts an HTML pagebreak into the document
All available commands can be found in the code shown at the end of this topic:
MarkdownDocumentEditor::MarkupMarkdown()
You can also create custom addins that extend the default markup 'commands' by implmenting MarkdownMonsterAddin.OnEditorCommand and creating your own commands.
Custom HTML Tag Wrapping
You can also create a custom HTML commands that wrap the selected text into an HTML element tag. For example:
"AdditionalToolbarIcons": {
"Pencil": "html|cite"
}
<cite>selected text</cite>
This gives you access to formatting just about any HTML markup.
Template Content
Additionally you can also create custom HTML output using a simple 'wrapping' template where you use {0} to signify the selection. For example:
"AdditionalToolbarIcons": {
"Bolt": "html|<b><i>{0}</i></b>"
}
Which renders:
<b><i>selected Text</i></b>
Custom Operations
If you need to add toolbar buttons with more control you can also use either the Commander Addin or a full Addin.
// Font Awesome Icon
public void AddEditToolbarIcon(string iconName, string markdownActionCommand,
ToolBar toolbar = null, ICommand command = null )
// Image Source Icon (sized to 16 high)
public void AddEditToolbarIcon(ImageSource icon, string markdownActionCommand,
ToolBar toolbar = null, ICommand command = null)
For example using the Commander Addin you could use the following:
Model.Window.AddEditToolbarIcon("Microphone", "html|speak");
which can be triggered off a hotkey (or by running the script).
Using a full addin gives you the added capability of loading icon on application startup without requiring an explicit hotkey.
Note: You can also pass completely custom commands to the
AddEditToolbarIcon()method using an addin so you can completely take over custom button processing.
Available Html Operation Commands
The built-in Html markup commands and actions are described by the conditionals in the following code block. Note that many of the commands are already on the toolbar, but a few obscure ones are on lower level menus or invoked implicitly.
public async Task<MarkupMarkdownResult> MarkupMarkdown(string action, string input, string style = null)
{
var result = new MarkupMarkdownResult();
if (string.IsNullOrEmpty(action))
{
result.Html = input;
return result;
}
action = action.ToLower();
input = input ?? string.Empty;
string html = input;
int cursorMovement = 0;
if (action == "softbreak")
{
var lines = input.GetLines();
html = string.Empty;
foreach (var line in lines)
{
html += line + mmApp.Configuration.Markdown.SoftReturnSymbol + mmApp.GetNewLine();
}
//if (lines.Length > 1)
// html = html.TrimEnd(new char[] { '\n', '\r'});
}
if (action == "bold")
{
html = WrapValue(input, "**", "**", stripExtraSpaces: true, multiline: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -2;
}
else if (action == "italic")
{
var italic = mmApp.Configuration.Markdown.ItalicSymbol;
html = WrapValue(input, italic, italic, stripExtraSpaces: true, multiline: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -1;
}
else if (action == "inlinecode")
{
if (input.Contains('`'))
{
// nested ` reqiures double escaping
if (input.StartsWith("`"))
input = ' ' + input;
if (input.EndsWith("`"))
input = input + ' ';
html = "``" + input + "``";
}
else
{
html = WrapValue(input, "`", "`", stripExtraSpaces: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -1;
}
}
else if (action == "small")
{
// :-( no markdown spec for this - use HTML
html = WrapValue(input, "<small>", "</small>", stripExtraSpaces: true, multiline: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -8;
}
else if (action == "underline")
{
// :-( no markdown spec for this - use HTML
html = WrapValue(input, "<u>", "</u>", stripExtraSpaces: true, multiline: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -4;
}
else if (action == "strikethrough")
{
html = WrapValue(input, "~~", "~~", stripExtraSpaces: true, multiline: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -2;
}
else if (action == "mark")
{
html = WrapValue(input, "<mark>", "</mark>", stripExtraSpaces: true, multiline: true, allowEmpty: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -7;
}
else if (action == "pagebreak")
html = "\n<div style='page-break-after: always'></div>\n";
else if (action == "nonbreakingspace")
{
html = " ";
}
else if (action == "nonbreakinghyphen")
{
html = "‑";
}
else if (action == "h1")
{
html = await PrefixSelectedLine("# ", "#", " ", "\t");
cursorMovement = -1; // linefeed
}
else if (action == "h2")
{
html = await PrefixSelectedLine("## ", "#", " ", "\t");
cursorMovement = -1; // linefeed
}
else if (action == "h3")
{
html = await PrefixSelectedLine("### ", "#", " ", "\t");
cursorMovement = -1; // linefeed
}
else if (action == "h4")
{
html = await PrefixSelectedLine("#### ", "#", " ", "\t");
cursorMovement = -1; // linefeed
}
else if (action == "h5")
{
html = await PrefixSelectedLine("##### ", "#", " ", "\t");
cursorMovement = -1; // linefeed
}
else if (action == "h6")
{
html = await PrefixSelectedLine("###### ", "#", " ", "\t");
cursorMovement = -1; // linefeed
}
else if (action == "quote")
{
input = input.Trim();
// if there's no selection - select the current line
if (Editor != null && string.IsNullOrEmpty(input))
{
var lineno = await Editor.GetLineNumber();
await Editor.SetSelectionRange(lineno, 0, lineno, 999999);
input = await Editor.GetSelection();
input = input?.Trim();
}
var sb = new StringBuilder();
var lines = StringUtils.GetLines(input);
foreach (var line in lines)
{
var ln = line.Trim();
if (!ln.StartsWith(">"))
sb.AppendLine("> " + ln);
else
sb.AppendLine(ln);
}
html = sb.ToString();
if (lines.Length < 2)
html = html.TrimEnd(); // strip line feed
}
else if (action == "list" || action == "checklist" || action == "numberlist")
{
input = input.Trim();
bool isEmptySelection = string.IsNullOrEmpty(input);
var listPrefix = "* ";
if (action == "checklist")
listPrefix = "* [ ] ";
else if (action == "numberlist")
listPrefix = "{0}. ";
// if there's no selection - select the current line
if (Editor != null && isEmptySelection && listPrefix != "{0}. ")
{
html = await PrefixSelectedLine(listPrefix, "*", " ");
}
else
{
var sb = new StringBuilder();
var lines = input.GetLines();
var ct = 0;
foreach (var line in lines)
{
ct++;
var ln = line.Trim();
var prefix = string.Format(listPrefix, ct);
if (!ln.StartsWith(prefix))
sb.AppendLine(prefix + ln);
else
sb.AppendLine(ln);
}
html = sb.ToString();
}
if (isEmptySelection)
{
cursorMovement = -1;
// html = html.TrimEnd() + " "; // strip off LF
}
}
else if (action == "table")
{
if (Editor != null)
{
var line = await Editor.GetCurrentLine();
line = line.Trim();
if (line.StartsWith("|") ||
line.StartsWith("+-") ||
line.StartsWith("+="))
{
// Edit existing table
var pos = await Editor.GetCursorPosition();
var tableMarkdown = await TableParser.SelectPipeAndGridTableMarkdown();
await Editor.EditorSelectionOperation("table", tableMarkdown, pos);
}
else
{
// This one display non-modal so you can access
// the document and move the cursor around
var form = new TableEditor();
form.Owner = Window;
form.Show(); // html remains unchanged = nothing updated
}
}
// if running modal
// var form = new TableEditor();
// DialogResult = form.ShowDialog();
//if (!form.Cancelled)
// html = form.TableHtml;
}
else if (action == "emoji")
{
result.DontSetEditorFocus = true;
if (_emojiWindow is { IsClosed: false })
{
_emojiWindow.Activate();
_emojiWindow.MoveWindowToToolButtonAndFocus();
}
else
{
_emojiWindow = new EmojiWindow(Window) { LeaveOpenAfterSelection = mmApp.Configuration.ToolWindows.LeaveEmojiWindowOpen };
_emojiWindow.Show();
}
//var dresult = await Window.Dispatcher.InvokeAsync(() => form.ShowDialog());
//if (dresult.HasValue && dresult.Value)
//html = form.EmojiString + " ";
}
else if (action == "youtube")
{
var form = new PasteYouTubeWindow(Window);
form.AutoEmbed = true; // form sets editor text directly
form.Show();
}
else if (action == "href")
{
var form = new PasteHref()
{
Owner = Window,
LinkText = input,
};
if (Editor != null)
form.MarkdownFile = Editor.MarkdownDocument.Filename;
// check for links in input or on clipboard
string link = input;
if (string.IsNullOrEmpty(link))
link = ClipboardHelper.GetText();
if (!(input.StartsWith("http:") ||
input.StartsWith("https:") ||
input.StartsWith("mailto:") ||
input.StartsWith("ftp:")))
link = string.Empty;
form.Link = link;
bool? res = await Window.Dispatcher.InvokeAsync(() => form.ShowDialog());
if (res != null && res.Value)
{
if (form.IsExternal)
html = $"<a href=\"{form.Link}\" target=\"_blank\">{form.LinkText}</a>";
else if (form.IsLinkReference)
// this doesn't set Html it directly updates the document
await AddLinkReference(form.Link, form.LinkText);
else
html = $"[{form.LinkText}]({form.Link.Replace(" ", "%20")})";
}
}
// code based href
else if (action == "href2")
{
var ct = ClipboardHelper.GetText();
if (ct.StartsWith("http"))
{
html = $"[{input}]({ct?.Replace(" ", "%20")})";
//cursorMovement = (ct.Length + 1) * -1;
//result.SelectionLength = ct.Length;
}
else
{
html = $"[{input}]()";
cursorMovement = -1;
}
}
else if (Editor != null && action == "image")
{
var form = new PasteImageWindow(Window)
{
ImageText = input,
MarkdownFile = Editor.MarkdownDocument.Filename
};
// check for links in input or on clipboard
string link = input;
if (string.IsNullOrEmpty(link))
link = ClipboardHelper.GetText();
if (!(input.StartsWith("http:") || input.StartsWith("https:") || input.StartsWith("mailto:") ||
input.StartsWith("ftp:")))
link = string.Empty;
if (input.Contains(".png") || input.Contains(".jpg") || input.Contains(".gif"))
link = input;
form.Image = link;
bool? res = await Window.Dispatcher.InvokeAsync(() => form.ShowDialog());
if (res != null && res.Value && form.Image != null)
{
var image = form.Image;
if (!image.StartsWith("data:image/"))
{
if (string.IsNullOrEmpty(form.ImageText))
form.ImageText = StringUtils.BreakIntoWords(System.IO.Path.GetFileNameWithoutExtension(image));
html = $"})";
}
else
{
var id = "image_ref_" + DataUtils.GenerateUniqueId();
object pos = await Editor.EditorHandler.JsInterop.GetCursorPosition();
object scroll = await Editor.EditorHandler.JsInterop.GetScrollTop();
// the ID tag
html = $"{mmApp.NewLine}{mmApp.NewLine}[{id}]: {image}{mmApp.NewLine}";
// set selction position to bottom of document
await Editor.GotoBottom();
await Editor.SetSelection(html);
WindowUtilities.DoEvents();
// reset the selection point
await Editor.EditorHandler.JsInterop.SetCursorPosition(pos);
if (scroll != null)
await Editor.EditorHandler.JsInterop.SetScrollTop(scroll);
WindowUtilities.DoEvents();
html = $"{mmApp.NewLine}![{form.ImageText}][{id}]";
}
}
form?.Close();
}
else if (action == "image2")
{
html = "![" + input + "]()";
cursorMovement = -1;
}
else if (action == "code")
{
var form = new PasteCode { Owner = Window };
if (!string.IsNullOrEmpty(input))
form.Code = input;
else
{
// use clipboard text if we think it contains code
string clipText = ClipboardHelper.GetText();
if (!string.IsNullOrEmpty(clipText) &&
clipText.Contains("{") ||
clipText.Contains("/>") ||
clipText.Contains("="))
form.Code = clipText;
}
form.CodeLanguage = mmApp.Configuration.ToolWindows.LastCodeSyntaxUsed;
var code = form.Code?.Trim() ?? string.Empty;
if (code.StartsWith("```") && code.EndsWith("```"))
form.CodeLanguage = "markdown";
bool? res = await Window.Dispatcher.InvokeAsync(() => form.ShowDialog());
if (res.HasValue && res.Value)
{
var delims = "```";
code = form.Code?.Trim() ?? string.Empty;
if (code.StartsWith("```") && code.EndsWith("```"))
delims = "~~~";
var lang = form.CodeLanguage;
if (lang == "none")
lang = string.Empty;
html = delims + lang + mmApp.NewLine +
code + mmApp.NewLine +
$"{delims}{mmApp.NewLine}";
if (string.IsNullOrEmpty(code))
cursorMovement = -5;
}
if (Editor != null)
Editor?.SetEditorFocus().FireAndForget();
}
else if (action == "subscript")
{
html = WrapValue(input, "<sub>", "</sub>", stripExtraSpaces: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -6;
}
else if (action == "superscript")
{
html = WrapValue(input, "<sup>", "</sup>", stripExtraSpaces: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -6;
}
else if (action == "bolditalic")
{
html = WrapValue(input, "<b>" + mmApp.Configuration.Markdown.ItalicSymbol, mmApp.Configuration.Markdown.ItalicSymbol + "</b>", stripExtraSpaces: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -8;
}
else if (action == "smallitalic")
{
html = WrapValue(input, "<small>" + mmApp.Configuration.Markdown.ItalicSymbol, mmApp.Configuration.Markdown.ItalicSymbol + "</small>", stripExtraSpaces: true);
if (string.IsNullOrEmpty(input))
cursorMovement = -9;
}
else if (action == "mermaid")
{
if (string.IsNullOrEmpty(input))
{
input = ClipboardHelper.GetText();
}
html = WrapValue(input, "```mermaid\n", "\n```", stripExtraSpaces: true);
}
else if (action == "plantuml")
{
if (string.IsNullOrEmpty(input))
{
input = ClipboardHelper.GetText()?.Trim();
if (!string.IsNullOrEmpty(input) && !input.Contains("@startuml"))
input = string.Empty;
}
if (!string.IsNullOrEmpty(input))
{
if (input.Trim().StartsWith("```plantuml"))
html = input;
else
html = WrapValue(input, "```plantuml\n", "\n```", stripExtraSpaces: true);
}
else
{
html = WrapValue(" ", "```plantuml\n@startuml\n", "\n@enduml\n```");
cursorMovement = -12;
}
}
// casing
else if (action == "uppercase")
{
html = input?.ToUpper();
}
else if (action == "lowercase")
{
html = input?.ToLower();
}
else if (action == "propercase")
{
html = StringUtils.ProperCase(input);
}
else if (action == "camelcase")
{
html = StringUtils.ToCamelCase(input);
}
else if (action == "htmlencode")
{
html = WebUtility.HtmlEncode(input);
}
else if (action == "urlencode")
{
html = WebUtility.UrlEncode(input);
}
else if (action == "normalizeindentation")
{
html = StringUtils.NormalizeIndentation(input).TrimEnd();
}
// Custom HTML commands like
// html|mark html|small html|custom
// creates wrapped element with start/end tag wrapped around selected text
// https://markdownmonster.west-wind.com/docs/_5im10bjpw.htm#template-content
else if (action.StartsWith("html|"))
{
// format is `html|markup|optionalTitle`
var tokens = action.Split('|');
action = tokens[1];
if (action.Contains("{0}"))
// html|<b><i>{0}</i></b> // {0} is text selection
html = action.Replace("{0}", input);
else
// html|cite (html keyword)
html = WrapValue(input, $"<{action}>", $"</{action}>", stripExtraSpaces: true);
cursorMovement = (action.Length + 3) * -1;
}
else
{
// allow addins to handle custom actions
string addinAction = await AddinManager.Current.RaiseOnEditorCommand(action, input);
if (!string.IsNullOrEmpty(addinAction))
html = addinAction;
}
result.CursorMovement = cursorMovement;
result.Html = html;
return result;
}
