Skip to content

Instantly share code, notes, and snippets.

@eritbh
Last active March 12, 2018 00:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eritbh/0e2a3ce279ab565cba5bc75acdda3515 to your computer and use it in GitHub Desktop.
Save eritbh/0e2a3ce279ab565cba5bc75acdda3515 to your computer and use it in GitHub Desktop.
A bookmarklet to add Markdown support to /r/toolbox's personal notes.

Personal Notes Markdown Mode

A bookmarklet script that adds a custom CodeMirror-based Markdown view to the Personal Notes editor of toolbox. A product of boredom and a personal lesson in bookmarklets, dynamic script loading, and (on a note unrelated to the final product) the limitations of Chromebooks.

Features

  • Custom font sizes for headings 1 through 3, and bold/underline/italic for headings 4-6
  • When a list item wraps to a second line, include a hanging indent to align the wrapped portion of the line with the start of the text (unordered lists only at the moment)
  • Status bar with cursor position indicator and very simple settings pane, including an option to enable and disable line numbers
  • Roughly compatible with the Reddit redesign

Usage

Click the bookmarklet once while you have a note open to start the editor. Click the bookmarklet a second time to disable the editor.

Installation

This script can be installed in one of two ways: As a traditional bookmarklet that lives in your browser's bookmarks bar, or as a shortcut link in toolbox's built-in modbar. Using the latter method styles the link there as a toggle button which shows whether or no the module is enabled (as in the screenshot below), but otherwise the functionality is the same.

As a traditional bookmarklet

Simply copy the javascript: URL below into the URL field of a new bookmark, then move the bookmark to the bookmarks bar. The name of the bookmark doesn't matter. If you're unsure how to create a bookmark, consult your browser's help documentation.

As a modbar shortcut

Go to toolbox settings > Modbar > Shortcuts, click the plus button, and paste the URL below into the "url" field in the new table row. The name of the shortcut doesn't matter, but you may wish to call it "Markdown Mode" as that's what the name will be replaced with when you activate it.

URL

javascript:/*$ Personal Notes Markdown Mode $*/(function personalNotesMarkdownMode(){function main(){const $button=$('#tb-toolbarshortcuts a[href^=\'javascript:/*$ Personal Notes Markdown Mode $*/\']').toggleClass('pnmm-trigger',!0);const $defaultEditArea=$('#tb-personal-notes-editarea');if($defaultEditArea.length&&!$('#tb-personal-notes-landing').length){let editor=window.pnmmEditor;if(!editor){editor=CodeMirror.fromTextArea($defaultEditArea[0],{lineNumbers:!1,mode:'gfm',lineWrapping:!0,tabSize:2,indentWithTabs:!1,matchBrackets:!0,autoCloseBrackets:!0});const $editorStyle=$(`<style name='pnmmEditorStyle'>.mod-toolbox #tb-personal-notes-content .pnmmEditor{box-sizing:border-box;width:450px;height:300px;font-size:12px;padding-bottom:21px}.pnmmEditor .cm-header-1{font-size:2em}.pnmmEditor .cm-header-2{font-size:1.5em}.pnmmEditor .cm-header-3{font-size:1.33em}.pnmmEditor .cm-header-5{font-weight:400;text-decoration:underline}.pnmmEditor .cm-header-6{font-weight:400;font-style:italic}.pnmm-statusbar{position:absolute;bottom:0;right:0;background:#F7F7F7;height:21px;left:0;z-index:4;box-sizing:border-box;border-top:1px solid #DDD;display:flex;justify-content:space-between}.pnmm-statusbar-left,.pnmm-statusbar-right,.pnmm-statusbar-item{line-height:20px;margin:0 5px}.pnmm-settingspane{position:absolute;bottom:21px;right:5px;border:1px solid #DDD;background:#F7F7F7;z-index:4;border-bottom:0;padding:4px 0}.pnmm-setting{margin:0 5px;display:flex;justify-content:space-between;white-space:pre}.pnmm-setting-label:after{content:':\\A0'}.pnmm-setting-input[type=checkbox]{all:inherit;margin:0!important}.pnmm-setting-input[type=checkbox]::after{content:'Off';color:red}.pnmm-setting-input[type=checkbox]:checked::after{content:'\\A0 On';color:limegreen}.pnmm-setting.enabled .pnmm-setting-value{color:green!important}</style>`);const $statusBar=$(`<div class='pnmm-statusbar' />`);const $statusBarLeft=$(`<div class='pnmm-statusbar-left' />`).appendTo($statusBar);const $statusBarRight=$(`<div class='pnmm-statusbar-right' />`).appendTo($statusBar);const $settingsPane=$(`<div class='pnmm-settingspane' style='display:none'/>`);const $settingsButton=$(`<a href='javascript:;' class='pnmm-statusbar-item pnmm-settings'>Settings</a>`).on('click',function(){$settingsPane.toggle()}).appendTo($statusBarRight);const unorderedListLineRegExp=/^\s*[-+*]\s+/;editor.on('renderLine',function(cm,line,elt){const result=unorderedListLineRegExp.exec(line.text);if(!result)return;var off=editor.defaultCharWidth()*result[0].length;elt.style.textIndent='-'+off+'px';elt.style.paddingLeft=4+off+'px'});editor.setOption('extraKeys',{Tab:function(cm){cm.execCommand('insertSoftTab')}});$settingsPane.append($(`<div class='pnmm-setting pnmm-linenos' />`).append(`<span class='pnmm-setting-label'>Line Numbers</span>`).append($(`<input type=checkbox class='pnmm-setting-input'>`).on('change',function(){const $this=$(this);const $setting=$this.closest('.pnmm-setting');if($this.is(':checked')){$setting.toggleClass('enabled',!0);editor.setOption('lineNumbers',!0)}else{$setting.toggleClass('enabled',!1);editor.setOption('lineNumbers',!1)}})));const $ruler=$(`<span class='pnmm-statusbar-item pnmm-ruler'>1:1</span>`);$statusBarRight.prepend($ruler);editor.on('cursorActivity',function(){const{line,ch}=editor.getDoc().getCursor();$ruler.text(`${line + 1}:${ch + 1}`)});editor.on('change',function(){$defaultEditArea.val(editor.getDoc().getValue())});$('.tb-popup.personal-notes-popup').on('click','.tb-personal-note-link, .tb-personal-note-delete, #create-personal-note',function listener(){$('.tb-popup.personal-notes-popup').off('click',listener);$defaultEditArea.val(editor.getDoc().getValue());$('.pnmm-control, .pnmmEditor').remove();$button.html('Markdown Mode <span style=\'color:red\'>Disabled</span>').toggleClass('enabled',!1);window.pnmmEditor=null});$(editor.getWrapperElement()).toggleClass('pnmmEditor').append($editorStyle,$statusBar,$settingsPane).hide();window.pnmmEditor=editor}const $codeMirrorWrapper=$(editor.getWrapperElement());if($codeMirrorWrapper.is(':visible')){$codeMirrorWrapper.hide();$defaultEditArea.show();$button.html('Markdown Mode <span style=\'color:red\'>Disabled</span>').toggleClass('enabled',!1)}else{$defaultEditArea.hide();$codeMirrorWrapper.show();editor.setValue($defaultEditArea.val());editor.refresh();$button.html('Markdown Mode <span style=\'color:green\'>Enabled</span>').toggleClass('enabled',!0)}}else{window.alert('You don\'t have a note open! Please open a note before trying this.')}}function getScripts(arr,callback,index=0){if(!arr[index])return callback();let script=document.createElement('SCRIPT');script.src=arr[index];script.type='text/javascript';script.onload=getScripts.bind(this,arr,callback,index+1);document.getElementsByTagName('head')[0].appendChild(script)}let scripts=[];if(!window.jQuery){scripts.push('//code.jquery.com/jquery-3.2.1.min.js')}if(!window.CodeMirror){scripts.push('//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/codemirror.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/mode/overlay.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/mode/markdown/markdown.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/mode/gfm/gfm.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/edit/matchbrackets.min.js','//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/edit/closebrackets.min.js')}getScripts(scripts,main)})();undefined

Screenshot

The thing

Explanation

This bookmarklet adds a custom editor to the Personal Notes editor if one is present on the page. Triggering the bookmarklet a second time will disable the custom editor and restore the native textarea. It uses CodeMirror and the gfm (GitHub-flavored Markdown) editor mode along with some custom styles to display Markdown styles inline within the editor, allowing you to visualize how the note would be displayed if it were rendered as Markdown. This means that you can write in Markdown without having to have a second pane to view the rendered content - the markdown syntax is simply styled within the editor. It also includes bracket matching, because why not.

When the bookmarklet is triggered, it starts by checking for the existence of CodeMirror on the page. If it doesn't detect this, it will load the CodeMirror base script and the addons it depends on with calls to $.getScript(). After these scripts are loaded, or if CodeMirror was already loaded, it makes sure the Personal Notes editor is open. It then checks to see if a CodeMirror editor instance exists, and if not, creates one via calling CodeMirror.fromTextArea, passing the native editor as the target. It also configures the newly-created editor with modified behavior, custom styles, and extra elements to enable the features of the final editor.

Once the editor is known to exist, the script simply toggles its state. If the original editor is visible, it hides it, shows the custom editor, and copies the value of the original editor to the custom one. If the original editor is hidden, it hides the custom editor and shows the original one; the value of the custom editor does not need to be transferred at this time because it is actively updated whenever the custom editor's value changes.

The bookmarklet is intended to be added to the Toolbox modbar shortcuts area. When it is, a comment at the beginning of the bookmarklet's code allows the code to identify the clicked link. It then updates this link each time it's clicked with the status of the bookmarklet - either "On" if the custom editor is shown, or "Off" if it is hidden.

The style of the source file is reflective of its nature as a bookmarklet. The primary method of testing this script was by copying it and typing javascript: into the Chrome address bar before pasting the entire script in and then pressing Enter. On paste, the newlines in the script are turned into spaces, so all comments in the source are block comments to prevent them from interfering from the code in this way. Additionally, since semicolons are required without newlines, this file is required to contain them to work with this method of testing. Finally, since the final product can be stored in HTML within double quotes as part of the toolbox modbar markup, the entire script does not use double quotes, opting instead for single quotes and template literals.

/*$ Personal Notes Markdown Mode $*/
(function personalNotesMarkdownMode () {
function main () {
const $button = $('#tb-toolbarshortcuts a[href^=\'javascript:/*$ Personal Notes Markdown Mode $*/\']').toggleClass('pnmm-trigger', true);
const $defaultEditArea = $('#tb-personal-notes-editarea');
if ($defaultEditArea.length && !$('#tb-personal-notes-landing').length) {
/* We have a note open */
let editor = window.pnmmEditor;
if (!editor) {
/* There is no CodeMirror editor, add one */
editor = CodeMirror.fromTextArea($defaultEditArea[0], {
lineNumbers: false,
mode: 'gfm',
lineWrapping: true,
tabSize: 2,
indentWithTabs: false,
matchBrackets: true,
autoCloseBrackets: true
});
/* Custom styles */
const $editorStyle = $(`
<style name='pnmmEditorStyle'>
.mod-toolbox #tb-personal-notes-content .pnmmEditor {
box-sizing: border-box;
width: 450px;
height: 300px;
font-size: 12px;
padding-bottom: 21px;
}
.pnmmEditor .cm-header-1 { font-size: 2em; }
.pnmmEditor .cm-header-2 { font-size: 1.5em; }
.pnmmEditor .cm-header-3 { font-size: 1.33em; }
.pnmmEditor .cm-header-5 { font-weight: 400; text-decoration: underline; }
.pnmmEditor .cm-header-6 { font-weight: 400; font-style: italic; }
.pnmm-statusbar {
position: absolute;
bottom: 0;
right: 0;
background: #F7F7F7;
height: 21px;
left: 0;
z-index: 4;
box-sizing: border-box;
border-top: 1px solid #DDD;
display: flex;
justify-content: space-between;
}
.pnmm-statusbar-left, .pnmm-statusbar-right, .pnmm-statusbar-item {
line-height: 20px;
margin: 0 5px;
}
.pnmm-settingspane {
position: absolute;
bottom: 21px;
right: 5px;
border: 1px solid #DDD;
background: #F7F7F7;
z-index: 4;
border-bottom: 0;
padding: 4px 0;
}
.pnmm-setting {
margin: 0 5px;
display: flex;
justify-content: space-between;
white-space: pre;
}
.pnmm-setting-label:after {
content: ':\\A0';
}
.pnmm-setting-input[type=checkbox] {
all: inherit;
margin: 0 !important;
}
.pnmm-setting-input[type=checkbox]::after {
content: 'Off';
color: red;
}
.pnmm-setting-input[type=checkbox]:checked::after {
content: '\\A0 On';
color: limegreen;
}
.pnmm-setting.enabled .pnmm-setting-value {
color: green !important;
}
</style>
`);
/* Status bar and settings page */
const $statusBar = $(`
<div class='pnmm-statusbar' />
`);
const $statusBarLeft = $(`
<div class='pnmm-statusbar-left' />
`).appendTo($statusBar);
const $statusBarRight = $(`
<div class='pnmm-statusbar-right' />
`).appendTo($statusBar);
const $settingsPane = $(`
<div class='pnmm-settingspane' style='display:none'/>
`);
const $settingsButton = $(`
<a href='javascript:;' class='pnmm-statusbar-item pnmm-settings'>Settings</a>
`).on('click', function () {
$settingsPane.toggle();
}).appendTo($statusBarRight);
/* Unordered list hanging indent */
const unorderedListLineRegExp = /^\s*[-+*]\s+/;
editor.on('renderLine', function(cm, line, elt) {
const result = unorderedListLineRegExp.exec(line.text);
if (!result) return;
var off = editor.defaultCharWidth() * result[0].length;
elt.style.textIndent = '-' + off + 'px';
elt.style.paddingLeft = 4 + off + 'px';
});
/* On tab, insert spaces */
editor.setOption('extraKeys', {
Tab: function(cm) { cm.execCommand('insertSoftTab'); }
});
/* Status bar: Toggle line numbers */
$settingsPane.append($(`
<div class='pnmm-setting pnmm-linenos' />
`).append(`
<span class='pnmm-setting-label'>Line Numbers</span>
`).append(
$(`
<input type=checkbox class='pnmm-setting-input'>
`).on('change', function () {
const $this = $(this);
const $setting = $this.closest('.pnmm-setting');
if ($this.is(':checked')) {
$setting.toggleClass('enabled', true);
editor.setOption('lineNumbers', true);
} else {
$setting.toggleClass('enabled', false);
editor.setOption('lineNumbers', false);
}
})
));
/* Status bar: Show current position */
const $ruler = $(`<span class='pnmm-statusbar-item pnmm-ruler'>1:1</span>`);
$statusBarRight.prepend($ruler);
editor.on('cursorActivity', function () {
const {line, ch} = editor.getDoc().getCursor();
$ruler.text(`${line + 1}:${ch + 1}`);
});
/* Update the OG textarea constantly so saving works seamlessly */
editor.on('change', function () {
$defaultEditArea.val(editor.getDoc().getValue());
});
/* Clean up after ourselves when we do anything */
$('.tb-popup.personal-notes-popup').on('click', '.tb-personal-note-link, .tb-personal-note-delete, #create-personal-note', function listener () {
$('.tb-popup.personal-notes-popup').off('click', listener);
$defaultEditArea.val(editor.getDoc().getValue());
$('.pnmm-control, .pnmmEditor').remove();
$button.html('Markdown Mode <span style=\'color:red\'>Disabled</span>').toggleClass('enabled', false);
window.pnmmEditor = null;
});
/* Add our custom things to the editor */
$(editor.getWrapperElement()).toggleClass('pnmmEditor').append($editorStyle, $statusBar, $settingsPane).hide();
/* Hooray */
window.pnmmEditor = editor;
}
const $codeMirrorWrapper = $(editor.getWrapperElement());
if ($codeMirrorWrapper.is(':visible')) {
/* CodeMirror is already open */
/* Changes are synced this way constantly, no need to redo the value here */
$codeMirrorWrapper.hide();
$defaultEditArea.show();
$button.html('Markdown Mode <span style=\'color:red\'>Disabled</span>').toggleClass('enabled', false);
} else {
/* CodeMirror is not open */
$defaultEditArea.hide();
$codeMirrorWrapper.show();
editor.setValue($defaultEditArea.val()); /* sync isn't both ways, so we grab the value and update it */
editor.refresh(); /* idk if this does anything, TODO */
$button.html('Markdown Mode <span style=\'color:green\'>Enabled</span>').toggleClass('enabled', true);
}
} else {
/* There is no note open */
window.alert('You don\'t have a note open! Please open a note before trying this.');
}
}
/* Helper to recursively execute a list of scripts and call back when done */
function getScripts (arr, callback, index = 0) {
if (!arr[index]) return callback();
let script = document.createElement('SCRIPT');
script.src = arr[index];
script.type = 'text/javascript';
script.onload = getScripts.bind(this, arr, callback, index + 1);
document.getElementsByTagName('head')[0].appendChild(script);
}
/* Figure out what deps we're missing */
let scripts = [];
/* Reddit redesign doesn't have jQuery, so grab that to make it work */
if (!window.jQuery) {
scripts.push('//code.jquery.com/jquery-3.2.1.min.js');
}
/* CodeMirror has a bunch of extensions we use */
if (!window.CodeMirror) {
scripts.push(
'//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/codemirror.min.js',
'//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/mode/overlay.min.js',
'//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/mode/markdown/markdown.min.js',
'//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/mode/gfm/gfm.min.js',
'//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/edit/matchbrackets.min.js',
'//cdnjs.cloudflare.com/ajax/libs/codemirror/5.29.0/addon/edit/closebrackets.min.js'
);
}
/* Fetch missing dependencies and then start the actual script afterwards */
getScripts(scripts, main);
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment