Desktop: Add monospace enforcement for certain elements in Markdown editor (#4689)

pull/4738/head
Caleb John 2021-03-26 03:08:22 -06:00 committed by GitHub
parent e2db02887c
commit 81b3ddf0e7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 107 additions and 10 deletions

View File

@ -478,7 +478,7 @@ class Application extends BaseApplication {
updateEditorFont() {
const fontFamilies = [];
if (Setting.value('style.editor.fontFamily')) fontFamilies.push(`"${Setting.value('style.editor.fontFamily')}"`);
fontFamilies.push('monospace');
fontFamilies.push('Avenir, Arial, sans-serif');
// The '*' and '!important' parts are necessary to make sure Russian text is displayed properly
// https://github.com/laurent22/joplin/issues/155

View File

@ -377,6 +377,9 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
`.CodeMirror-selected {
background: #6b6b6b !important;
}` : '';
const monospaceFonts = [];
if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
monospaceFonts.push('monospace');
const element = document.createElement('style');
element.setAttribute('id', 'codemirrorStyle');
@ -412,6 +415,11 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
padding-right: 10px !important;
}
/* This enforces monospace for certain elements (code, tables, etc.) */
.cm-jn-monospace {
font-family: ${monospaceFonts.join(', ')} !important;
}
.cm-header-1 {
font-size: 1.5em;
}

View File

@ -1,7 +1,16 @@
import 'codemirror/addon/mode/multiplex';
import 'codemirror/mode/stex/stex';
import MarkdownUtils from '@joplin/lib/markdownUtils';
import Setting from '@joplin/lib/models/Setting';
interface JoplinModeState {
outer: any;
openCharacter: string;
inTable: boolean;
inner: any;
}
// Joplin markdown is a the same as markdown mode, but it has configured defaults
// and support for katex math blocks
export default function useJoplinMode(CodeMirror: any) {
@ -34,25 +43,29 @@ export default function useJoplinMode(CodeMirror: any) {
}
return {
startState: function(): { outer: any; openCharacter: string; inner: any } {
startState: function(): JoplinModeState {
return {
outer: CodeMirror.startState(markdownMode),
openCharacter: '',
inTable: false,
inner: CodeMirror.startState(stex),
};
},
copyState: function(state: any) {
copyState: function(state: JoplinModeState) {
return {
outer: CodeMirror.copyState(markdownMode, state.outer),
openCharacter: state.openCharacter,
inTable: state.inTable,
inner: CodeMirror.copyState(stex, state.inner),
};
},
token: function(stream: any, state: any) {
token: function(stream: any, state: JoplinModeState) {
let currentMode = markdownMode;
let currentState = state.outer;
// //////// KATEX //////////
let tokenLabel = 'katex-marker-open';
let nextTokenPos = stream.string.length;
let closing = false;
@ -86,34 +99,75 @@ export default function useJoplinMode(CodeMirror: any) {
return tokenLabel;
}
// //////// End KATEX //////////
// //////// Markdown //////////
// If we found a token in this stream but haven;t reached it yet, then we will
// pass all the characters leading up to our token to markdown mode
const oldString = stream.string;
stream.string = oldString.slice(0, nextTokenPos);
const token = currentMode.token(stream, currentState);
let token = currentMode.token(stream, currentState);
stream.string = oldString;
// //////// End Markdown //////////
// //////// Monospace //////////
let isMonospace = false;
// After being passed to the markdown mode we can check if the
// code state variables are set
// Code Block
if (state.outer.code || (state.outer.thisLine && state.outer.thisLine.fencedCodeEnd)) {
isMonospace = true;
}
// Indented Code
if (state.outer.indentedCode) {
isMonospace = true;
}
// Task lists
if (state.outer.taskList || state.outer.taskOpen || state.outer.taskClosed) {
isMonospace = true;
}
// Any line that contains a | is potentially a table row
if (stream.string.match(/\|/g)) {
// Check if the current and following line together make a valid
// markdown table header
if (MarkdownUtils.matchingTableDivider(stream.string, stream.lookAhead(1))) {
state.inTable = true;
}
// Treat all lines that start with | as a table row
if (state.inTable || stream.string[0] === '|') {
isMonospace = true;
}
} else {
state.inTable = false;
}
if (isMonospace) { token = `${token} jn-monospace`; }
// //////// End Monospace //////////
return token;
},
indent: function(state: any, textAfter: string, line: any) {
indent: function(state: JoplinModeState, textAfter: string, line: any) {
const mode = state.openCharacter ? stex : markdownMode;
if (!mode.indent) return CodeMirror.Pass;
return mode.indent(state.openCharacter ? state.inner : state.outer, textAfter, line);
},
blankLine: function(state: any) {
blankLine: function(state: JoplinModeState) {
const mode = state.openCharacter ? stex : markdownMode;
if (mode.blankLine) {
mode.blankLine(state.openCharacter ? state.inner : state.outer);
}
state.inTable = false;
},
electricChars: markdownMode.electricChars,
innerMode: function(state: any) {
innerMode: function(state: JoplinModeState) {
return state.openCharacter ? { state: state.inner, mode: stex } : { state: state.outer, mode: markdownMode };
},

View File

@ -137,6 +137,30 @@ const markdownUtils = {
return output.join('\n');
},
countTableColumns(line: string) {
if (!line) return 0;
const trimmed = line.trim();
let pipes = (line.match(/\|/g) || []).length;
if (trimmed[0] === '|') { pipes -= 1; }
if (trimmed[trimmed.length - 1] === '|') { pipes -= 1; }
return pipes + 1;
},
matchingTableDivider(header: string, divider: string) {
if (!header || !divider) return false;
const invalidChars = divider.match(/[^\s\-:|]/g);
if (invalidChars) { return false; }
const columns = markdownUtils.countTableColumns(header);
const cols = markdownUtils.countTableColumns(divider);
return cols > 0 && (cols >= columns);
},
titleFromBody(body: string) {
if (!body) return '';
const mdLinkRegex = /!?\[([^\]]+?)\]\(.+?\)/g;

View File

@ -866,6 +866,17 @@ class Setting extends BaseModel {
appTypes: ['desktop'],
section: 'appearance',
label: () => _('Editor font family'),
description: () =>
_('If the font is incorrect or empty, it will default to a generic monospace font.'),
storage: SettingStorage.File,
},
'style.editor.monospaceFontFamily': {
value: '',
type: SettingItemType.String,
public: true,
appTypes: ['desktop'],
section: 'appearance',
label: () => _('Editor monospace font family'),
description: () =>
_('This should be a *monospace* font or some elements will render incorrectly. If the font ' +
'is incorrect or empty, it will default to a generic monospace font.'),