1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-24 10:27:10 +02:00

Chore: Set up repository for testing/preparation for mobile markdown toolbar PR (#6650)

This commit is contained in:
Henry Heino 2022-07-22 02:44:19 -07:00 committed by GitHub
parent 11a1e1cb6b
commit 0e532fbaf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2152 additions and 298 deletions

View File

@ -46,7 +46,7 @@ packages/app-desktop/packageInfo.js
packages/app-desktop/services/electron-context-menu.js
packages/app-desktop/vendor/lib/
packages/app-mobile/android
packages/app-mobile/components/NoteEditor/CodeMirror.bundle.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.bundle.js
packages/app-mobile/ios
packages/app-mobile/lib/rnInjectedJs/
packages/app-mobile/locales
@ -853,9 +853,18 @@ packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js.ma
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.d.ts
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js.map
packages/app-mobile/components/NoteEditor/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map

15
.gitignore vendored
View File

@ -843,9 +843,18 @@ packages/app-mobile/components/NoteBodyViewer/hooks/useOnResourceLongPress.js.ma
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.d.ts
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js
packages/app-mobile/components/NoteBodyViewer/hooks/useSource.js.map
packages/app-mobile/components/NoteEditor/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js
packages/app-mobile/components/NoteEditor/CodeMirror/CodeMirror.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js
packages/app-mobile/components/NoteEditor/CodeMirror/markdownMathParser.test.js.map
packages/app-mobile/components/NoteEditor/CodeMirror/theme.d.ts
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js
packages/app-mobile/components/NoteEditor/CodeMirror/theme.js.map
packages/app-mobile/components/NoteEditor/NoteEditor.d.ts
packages/app-mobile/components/NoteEditor/NoteEditor.js
packages/app-mobile/components/NoteEditor/NoteEditor.js.map

View File

@ -20253,6 +20253,19 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"node_modules/typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
@ -37995,6 +38008,12 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"peer": true
},
"unicode-canonical-property-names-ecmascript": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

View File

@ -63,7 +63,7 @@ buck-out/
lib/csstojs/
lib/rnInjectedJs/
dist/
components/NoteEditor/CodeMirror.bundle.js
components/NoteEditor/CodeMirror.bundle.min.js
components/NoteEditor/CodeMirror/CodeMirror.bundle.js
components/NoteEditor/CodeMirror/CodeMirror.bundle.min.js
utils/fs-driver-android.js

View File

@ -9,11 +9,11 @@
// wrapper to access CodeMirror functionalities. Anything else should be done
// from NoteEditor.tsx.
import { EditorState, Extension } from '@codemirror/state';
import createTheme from './theme';
import { EditorState } from '@codemirror/state';
import { markdown } from '@codemirror/lang-markdown';
import { highlightSelectionMatches, search } from '@codemirror/search';
import { defaultHighlightStyle, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import { EditorView, drawSelection, highlightSpecialChars, ViewUpdate } from '@codemirror/view';
import { undo, redo, history, undoDepth, redoDepth } from '@codemirror/commands';
@ -21,6 +21,8 @@ import { keymap } from '@codemirror/view';
import { indentOnInput } from '@codemirror/language';
import { searchKeymap } from '@codemirror/search';
import { historyKeymap, defaultKeymap } from '@codemirror/commands';
import { MarkdownMathExtension } from './markdownMathParser';
import { GFM as GitHubFlavoredMarkdownExtension } from '@lezer/markdown';
interface CodeMirrorResult {
editor: EditorView;
@ -42,120 +44,6 @@ function logMessage(...msg: any[]) {
postMessage('onLog', { value: msg });
}
// For an example on how to customize the theme, see:
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
const createTheme = (theme: any): Extension[] => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = { };
const baseSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
}
const baseTheme = EditorView.baseTheme({
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': baseContentStyle,
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
});
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
};
const highlightingStyle = HighlightStyle.define([
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
...baseHeadingStyle,
tag: tags.heading1,
fontSize: '1.6em',
borderBottom: `1px solid ${theme.dividerColor}`,
},
{
...baseHeadingStyle,
tag: tags.heading2,
fontSize: '1.4em',
},
{
...baseHeadingStyle,
tag: tags.heading3,
fontSize: '1.3em',
},
{
...baseHeadingStyle,
tag: tags.heading4,
fontSize: '1.2em',
},
{
...baseHeadingStyle,
tag: tags.heading5,
fontSize: '1.1em',
},
{
...baseHeadingStyle,
tag: tags.heading6,
fontSize: '1.0em',
},
{
tag: tags.list,
fontFamily: theme.fontFamily,
},
]);
return [
baseTheme,
appearanceTheme,
syntaxHighlighting(highlightingStyle),
// If we haven't defined highlighting for tags, fall back
// to the default.
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
];
};
export function initCodeMirror(parentElement: any, initialText: string, theme: any): CodeMirrorResult {
logMessage('Initializing CodeMirror...');
@ -183,7 +71,12 @@ export function initCodeMirror(parentElement: any, initialText: string, theme: a
// See https://github.com/codemirror/basic-setup/blob/main/src/codemirror.ts
// for a sample configuration.
extensions: [
markdown(),
markdown({
extensions: [
MarkdownMathExtension,
GitHubFlavoredMarkdownExtension,
],
}),
...createTheme(theme),
history(),
search(),

View File

@ -0,0 +1,152 @@
import { markdown } from '@codemirror/lang-markdown';
import { ensureSyntaxTree } from '@codemirror/language';
import { SyntaxNode } from '@lezer/common';
import { EditorState } from '@codemirror/state';
import { blockMathTagName, inlineMathContentTagName, inlineMathTagName, MarkdownMathExtension } from './markdownMathParser';
import { GFM as GithubFlavoredMarkdownExt } from '@lezer/markdown';
const syntaxTreeCreateTimeout = 100; // ms
/** Create an EditorState with markdown extensions */
const createEditorState = (initialText: string): EditorState => {
return EditorState.create({
doc: initialText,
extensions: [
markdown({
extensions: [MarkdownMathExtension, GithubFlavoredMarkdownExt],
}),
],
});
};
/**
* Returns a list of all nodes with the given name in the given editor's syntax tree.
* Attempts to create the syntax tree if it doesn't exist.
*/
const findNodesWithName = (editor: EditorState, nodeName: string) => {
const result: SyntaxNode[] = [];
ensureSyntaxTree(editor, syntaxTreeCreateTimeout)?.iterate({
enter: (node) => {
if (node.name === nodeName) {
result.push(node.node);
}
},
});
return result;
};
describe('Inline parsing', () => {
it('Document with just a math region', () => {
const documentText = '$3 + 3$';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const inlineMathContentNodes = findNodesWithName(editor, inlineMathContentTagName);
// There should only be one inline node
expect(inlineMathNodes.length).toBe(1);
expect(inlineMathNodes[0].from).toBe(0);
expect(inlineMathNodes[0].to).toBe(documentText.length);
// The content tag should be replaced by the internal sTeX parser
expect(inlineMathContentNodes.length).toBe(0);
});
it('Inline math mixed with text', () => {
const beforeMath = '# Testing!\n\nThis is a test of ';
const mathRegion = '$\\TeX % TeX Comment!$';
const afterMath = ' formatting.';
const documentText = `${beforeMath}${mathRegion}${afterMath}`;
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
const commentNodes = findNodesWithName(editor, 'comment');
expect(inlineMathNodes.length).toBe(1);
expect(blockMathNodes.length).toBe(0);
expect(commentNodes.length).toBe(1);
expect(inlineMathNodes[0].from).toBe(beforeMath.length);
expect(inlineMathNodes[0].to).toBe(beforeMath.length + mathRegion.length);
});
it('Inline math with no ending $ in a block', () => {
const documentText = 'This is a $test\n\nof inline math$...';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
// Math should end if there is no matching '$'.
expect(inlineMathNodes.length).toBe(0);
});
it('Shouldn\'t start if block would have spaces just inside', () => {
const documentText = 'This is a $ test of inline math$...\n\n$Testing... $...';
const editor = createEditorState(documentText);
expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0);
});
it('Shouldn\'t start if $ is escaped', () => {
const documentText = 'This is a \\$test of inline math$...';
const editor = createEditorState(documentText);
expect(findNodesWithName(editor, inlineMathTagName).length).toBe(0);
});
});
describe('Block math tests', () => {
it('Document with just block math', () => {
const documentText = '$$\n\t\\{ 1, 1, 2, 3, 5, ... \\}\n$$';
const editor = createEditorState(documentText);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(inlineMathNodes.length).toBe(0);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
it('Block math with comment', () => {
const startingText = '$$ % Testing...\n\t\\text{Test.}\n$$';
const afterMath = '\nTest.';
const editor = createEditorState(startingText + afterMath);
const inlineMathNodes = findNodesWithName(editor, inlineMathTagName);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
const texParserComments = findNodesWithName(editor, 'comment');
expect(inlineMathNodes.length).toBe(0);
expect(blockMathNodes.length).toBe(1);
expect(texParserComments.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(startingText.length);
expect(texParserComments[0]).toMatchObject({
from: '$$ '.length,
to: '$$ % Testing...'.length,
});
});
it('Block math without an ending tag', () => {
const beforeMath = '# Testing...\n\n';
const documentText = `${beforeMath}$$\n\t\\text{Testing...}\n\n\t3 + 3 = 6`;
const editor = createEditorState(documentText);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(beforeMath.length);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
it('Single-line declaration of block math', () => {
const documentText = '$$ Test. $$';
const editor = createEditorState(documentText);
const blockMathNodes = findNodesWithName(editor, blockMathTagName);
expect(blockMathNodes.length).toBe(1);
expect(blockMathNodes[0].from).toBe(0);
expect(blockMathNodes[0].to).toBe(documentText.length);
});
});

View File

@ -0,0 +1,216 @@
/**
* Search for $s and $$s in markdown and mark the regions between them as math.
*
* Text between single $s is marked as InlineMath and text between $$s is marked
* as BlockMath.
*/
import { tags, Tag } from '@lezer/highlight';
import { parseMixed, SyntaxNodeRef, Input, NestedParse, ParseWrapper } from '@lezer/common';
// Extend the existing markdown parser
import {
MarkdownConfig, InlineContext,
BlockContext, Line, LeafBlock,
} from '@lezer/markdown';
// The existing stexMath parser is used to parse the text between the $s
import { stexMath } from '@codemirror/legacy-modes/mode/stex';
import { StreamLanguage } from '@codemirror/language';
const dollarSignCharcode = 36;
const backslashCharcode = 92;
// (?:[>]\s*)?: Optionally allow block math lines to start with '> '
const mathBlockStartRegex = /^(?:\s*[>]\s*)?\$\$/;
const mathBlockEndRegex = /\$\$\s*$/;
const texLanguage = StreamLanguage.define(stexMath);
export const blockMathTagName = 'BlockMath';
export const blockMathContentTagName = 'BlockMathContent';
export const inlineMathTagName = 'InlineMath';
export const inlineMathContentTagName = 'InlineMathContent';
export const mathTag = Tag.define(tags.monospace);
export const inlineMathTag = Tag.define(mathTag);
/**
* Wraps a TeX math-mode parser. This removes [nodeTag] from the syntax tree
* and replaces it with a region handled by the sTeXMath parser.
*
* @param nodeTag Name of the nodes to replace with regions parsed by the sTeX parser.
* @returns a wrapped sTeX parser.
*/
const wrappedTeXParser = (nodeTag: string): ParseWrapper => {
return parseMixed((node: SyntaxNodeRef, _input: Input): NestedParse => {
if (node.name !== nodeTag) {
return null;
}
return {
parser: texLanguage.parser,
};
});
};
// Markdown extension for recognizing inline code
const InlineMathConfig: MarkdownConfig = {
defineNodes: [
{
name: inlineMathTagName,
style: inlineMathTag,
},
{
name: inlineMathContentTagName,
},
],
parseInline: [{
name: inlineMathTagName,
after: 'InlineCode',
parse(cx: InlineContext, current: number, pos: number): number {
const prevCharCode = pos - 1 >= 0 ? cx.char(pos - 1) : -1;
const nextCharCode = cx.char(pos + 1);
if (current !== dollarSignCharcode
|| prevCharCode === dollarSignCharcode
|| nextCharCode === dollarSignCharcode) {
return -1;
}
// Don't match if there's a space directly after the '$'
if (/\s/.exec(String.fromCharCode(nextCharCode))) {
return -1;
}
const start = pos;
const end = cx.end;
let escaped = false;
pos ++;
// Scan ahead for the next '$' symbol
for (; pos < end && (escaped || cx.char(pos) !== dollarSignCharcode); pos++) {
if (!escaped && cx.char(pos) === backslashCharcode) {
escaped = true;
} else {
escaped = false;
}
}
// Don't match if the ending '$' is preceded by a space.
const prevChar = String.fromCharCode(cx.char(pos - 1));
if (/\s/.exec(prevChar)) {
return -1;
}
// It isn't a math region if there is no ending '$'
if (pos === end) {
return -1;
}
// Advance to just after the ending '$'
pos ++;
// Add a wraping inlineMathTagName node that contains an inlineMathContentTagName.
// The inlineMathContentTagName node can thus be safely removed and the region
// will still be marked as a math region.
const contentElem = cx.elt(inlineMathContentTagName, start + 1, pos - 1);
cx.addElement(cx.elt(inlineMathTagName, start, pos, [contentElem]));
return pos + 1;
},
}],
wrap: wrappedTeXParser(inlineMathContentTagName),
};
// Extension for recognising block code
const BlockMathConfig: MarkdownConfig = {
defineNodes: [
{
name: blockMathTagName,
style: mathTag,
},
{
name: blockMathContentTagName,
},
],
parseBlock: [{
name: blockMathTagName,
before: 'Blockquote',
parse(cx: BlockContext, line: Line): boolean {
const delimLen = 2;
// $$ delimiter? Start math!
const mathStartMatch = mathBlockStartRegex.exec(line.text);
if (mathStartMatch) {
const start = cx.lineStart + mathStartMatch[0].length;
let stop;
let endMatch = mathBlockEndRegex.exec(
line.text.substring(mathStartMatch[0].length)
);
// If the math region ends immediately (on the same line),
if (endMatch) {
const lineLength = line.text.length;
stop = cx.lineStart + lineLength - endMatch[0].length;
} else {
let hadNextLine = false;
// Otherwise, it's a multi-line block display.
// Consume lines until we reach the end.
do {
hadNextLine = cx.nextLine();
endMatch = hadNextLine ? mathBlockEndRegex.exec(line.text) : null;
}
while (hadNextLine && endMatch === null);
if (hadNextLine && endMatch) {
const lineLength = line.text.length;
// Remove the ending delimiter
stop = cx.lineStart + lineLength - endMatch[0].length;
} else {
stop = cx.lineStart;
}
}
const lineEnd = cx.lineStart + line.text.length;
// Label the region. Add two labels so that one can be removed.
const contentElem = cx.elt(blockMathContentTagName, start, stop);
const containerElement = cx.elt(
blockMathTagName,
start - delimLen,
// Math blocks don't need ending delimiters, so ensure we don't
// include text that doesn't exist.
Math.min(lineEnd, stop + delimLen),
// The child of the container element should be the content element
[contentElem]
);
cx.addElement(containerElement);
// Don't re-process the ending delimiter (it may look the same
// as the starting delimiter).
cx.nextLine();
return true;
}
return false;
},
// End paragraph-like blocks
endLeaf(_cx: BlockContext, line: Line, _leaf: LeafBlock): boolean {
// Leaf blocks (e.g. block quotes) end early if math starts.
return mathBlockStartRegex.exec(line.text) !== null;
},
}],
wrap: wrappedTeXParser(blockMathContentTagName),
};
/** Markdown configuration for block and inline math support. */
export const MarkdownMathExtension: MarkdownConfig[] = [
InlineMathConfig,
BlockMathConfig,
];

View File

@ -0,0 +1,126 @@
/**
* Create a set of Extensions that provide syntax highlighting.
*/
import { defaultHighlightStyle, syntaxHighlighting, HighlightStyle } from '@codemirror/language';
import { tags } from '@lezer/highlight';
import { EditorView } from '@codemirror/view';
import { Extension } from '@codemirror/state';
// For an example on how to customize the theme, see:
//
// https://github.com/codemirror/theme-one-dark/blob/main/src/one-dark.ts
//
// For a tutorial, see:
//
// https://codemirror.net/6/examples/styling/#themes
//
// Use Safari developer tools to view the content of the CodeMirror iframe while
// the app is running. It seems that what appears as ".ͼ1" in the CSS is the
// equivalent of "&" in the theme object. So to target ".ͼ1.cm-focused", you'd
// use '&.cm-focused' in the theme.
const createTheme = (theme: any): Extension[] => {
const isDarkTheme = theme.appearance === 'dark';
const baseGlobalStyle: Record<string, string> = {
color: theme.color,
backgroundColor: theme.backgroundColor,
fontFamily: theme.fontFamily,
fontSize: `${theme.fontSize}px`,
};
const baseCursorStyle: Record<string, string> = { };
const baseContentStyle: Record<string, string> = { };
const baseSelectionStyle: Record<string, string> = { };
// If we're in dark mode, the caret and selection are difficult to see.
// Adjust them appropriately
if (isDarkTheme) {
// Styling the caret requires styling both the caret itself
// and the CodeMirror caret.
// See https://codemirror.net/6/examples/styling/#themes
baseContentStyle.caretColor = 'white';
baseCursorStyle.borderLeftColor = 'white';
baseSelectionStyle.backgroundColor = '#6b6b6b';
}
const baseTheme = EditorView.baseTheme({
'&': baseGlobalStyle,
// These must be !important or more specific than CodeMirror's built-ins
'.cm-content': baseContentStyle,
'&.cm-focused .cm-cursor': baseCursorStyle,
'&.cm-focused .cm-selectionBackground, ::selection': baseSelectionStyle,
'&.cm-focused': {
outline: 'none',
},
});
const appearanceTheme = EditorView.theme({}, { dark: isDarkTheme });
const baseHeadingStyle = {
fontWeight: 'bold',
fontFamily: theme.fontFamily,
};
const highlightingStyle = HighlightStyle.define([
{
tag: tags.strong,
fontWeight: 'bold',
},
{
tag: tags.emphasis,
fontStyle: 'italic',
},
{
...baseHeadingStyle,
tag: tags.heading1,
fontSize: '1.6em',
borderBottom: `1px solid ${theme.dividerColor}`,
},
{
...baseHeadingStyle,
tag: tags.heading2,
fontSize: '1.4em',
},
{
...baseHeadingStyle,
tag: tags.heading3,
fontSize: '1.3em',
},
{
...baseHeadingStyle,
tag: tags.heading4,
fontSize: '1.2em',
},
{
...baseHeadingStyle,
tag: tags.heading5,
fontSize: '1.1em',
},
{
...baseHeadingStyle,
tag: tags.heading6,
fontSize: '1.0em',
},
{
tag: tags.list,
fontFamily: theme.fontFamily,
},
]);
return [
baseTheme,
appearanceTheme,
syntaxHighlighting(highlightingStyle),
// If we haven't defined highlighting for tags, fall back
// to the default.
syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
];
};
export default createTheme;

View File

@ -44,145 +44,6 @@ function fontFamilyFromSettings() {
return [f, 'sans-serif'].join(', ');
}
// Obsolete with CodeMirror 6. See ./CodeMirror.ts for styling.
// function useCss(themeId:number):string {
// const [css, setCss] = useState('');
// // useEffect(() => {
// // const theme = themeStyle(themeId);
// // // Selection in dark mode is hard to see so make it brighter.
// // // https://discourse.joplinapp.org/t/dragging-in-dark-theme/12433/4?u=laurent
// // const selectionColorCss = theme.appearance === ThemeAppearance.Dark ?
// // `.CodeMirror-selected {
// // background: #6b6b6b !important;
// // }` : '';
// // const monospaceFonts = [];
// // // if (Setting.value('style.editor.monospaceFontFamily')) monospaceFonts.push(`"${Setting.value('style.editor.monospaceFontFamily')}"`);
// // monospaceFonts.push('monospace');
// // const fontSize = 15;
// // const fontFamily = fontFamilyFromSettings();
// // // BUG: caret-color seems to be ignored for some reason
// // const caretColor = theme.appearance === ThemeAppearance.Dark ? "white" : 'black';
// // setCss(`
// // /* These must be important to prevent the codemirror defaults from taking over*/
// // .CodeMirror {
// // font-family: ${fontFamily};
// // font-size: ${fontSize}px;
// // height: 100% !important;
// // width: 100% !important;
// // color: ${theme.color};
// // background-color: ${theme.backgroundColor};
// // position: absolute !important;
// // -webkit-box-shadow: none !important; // Some themes add a box shadow for some reason
// // }
// // .CodeMirror-lines {
// // /* This is used to enable the scroll-past end behaviour. The same height should */
// // /* be applied to the viewer. */
// // padding-bottom: 400px !important;
// // }
// // /* Left padding is applied at the editor component level, so we should remove it from the lines */
// // .CodeMirror pre.CodeMirror-line,
// // .CodeMirror pre.CodeMirror-line-like {
// // padding-left: 0;
// // }
// // .CodeMirror-sizer {
// // /* Add a fixed right padding to account for the appearance (and disappearance) */
// // /* of the sidebar */
// // 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;
// // }
// // .cm-header-2 {
// // font-size: 1.3em;
// // }
// // .cm-header-3 {
// // font-size: 1.1em;
// // }
// // .cm-header-4, .cm-header-5, .cm-header-6 {
// // font-size: 1em;
// // }
// // .cm-header-1, .cm-header-2, .cm-header-3, .cm-header-4, .cm-header-5, .cm-header-6 {
// // line-height: 1.5em;
// // }
// // .cm-search-marker {
// // background: ${theme.searchMarkerBackgroundColor};
// // color: ${theme.searchMarkerColor} !important;
// // }
// // .cm-search-marker-selected {
// // background: ${theme.selectedColor2};
// // color: ${theme.color2} !important;
// // }
// // .cm-search-marker-scrollbar {
// // background: ${theme.searchMarkerBackgroundColor};
// // -moz-box-sizing: border-box;
// // box-sizing: border-box;
// // opacity: .5;
// // }
// // /* We need to use important to override theme specific values */
// // .cm-error {
// // color: inherit !important;
// // background-color: inherit !important;
// // border-bottom: 1px dotted #dc322f;
// // }
// // /* The default dark theme colors don't have enough contrast with the background */
// // .cm-s-nord span.cm-comment {
// // color: #9aa4b6 !important;
// // }
// // .cm-s-dracula span.cm-comment {
// // color: #a1abc9 !important;
// // }
// // .cm-s-monokai span.cm-comment {
// // color: #908b74 !important;
// // }
// // .cm-s-material-darker span.cm-comment {
// // color: #878787 !important;
// // }
// // .cm-s-solarized.cm-s-dark span.cm-comment {
// // color: #8ba1a7 !important;
// // }
// // /* MOBILE SPECIFIC */
// // .CodeMirror .cm-scroller,
// // .CodeMirror .cm-line {
// // font-family: ${fontFamily};
// // caret-color: ${caretColor};
// // }
// // ${selectionColorCss}
// // `);
// // }, [themeId]);
// return css;
// }
function useCss(themeId: number): string {
return useMemo(() => {
const theme = themeStyle(themeId);
@ -274,7 +135,6 @@ function NoteEditor(props: Props, ref: any) {
} catch (e) {
window.ReactNativeWebView.postMessage("error:" + e.message + ": " + JSON.stringify(e))
}
true;
`;

View File

@ -0,0 +1,25 @@
// Configuration file for rollup
const { dirname } = require('path');
import typescript from '@rollup/plugin-typescript';
import { nodeResolve } from '@rollup/plugin-node-resolve';
const rootDir = dirname(dirname(dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;
const codeMirrorDir = `${mobileDir}/components/NoteEditor/CodeMirror`;
const outputFile = `${codeMirrorDir}/CodeMirror.bundle.js`;
export default {
output: outputFile,
plugins: [
typescript({
// Exclude all .js files. Rollup will attempt to import a .js
// file if both a .ts and .js file are present, conflicting
// with our build setup. See
// https://discourse.joplinapp.org/t/importing-a-ts-file-from-a-rollup-bundled-ts-file/
exclude: `${codeMirrorDir}/*.js`,
}),
nodeResolve(),
],
};

View File

@ -0,0 +1,17 @@
// Test configuration
// See https://jestjs.io/docs/configuration#testenvironment-string
const config = {
preset: 'ts-jest',
// File extensions for imports, in order of precedence:
// prefer importing from .ts or .tsx to importing from .js
// files.
moduleFileExtensions: [
'ts',
'tsx',
'js',
],
};
module.exports = config;

View File

@ -9,6 +9,8 @@
"android": "react-native run-android",
"build": "gulp build",
"tsc": "tsc --project tsconfig.json",
"test": "jest",
"test-ci": "yarn test",
"watch": "tsc --watch --preserveWatchOutput --project tsconfig.json",
"clean": "node tools/clean.js",
"buildInjectedJs": "gulp buildInjectedJs",
@ -75,6 +77,7 @@
"@codemirror/commands": "^6.0.0",
"@codemirror/lang-markdown": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/legacy-modes": "^6.1.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0",
@ -82,6 +85,7 @@
"@lezer/highlight": "^1.0.0",
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@types/jest": "^28.1.3",
"@types/node": "^14.14.6",
"@types/react": "^16.9.55",
"@types/react-native": "^0.64.4",
@ -89,10 +93,13 @@
"execa": "^4.0.0",
"fs-extra": "^8.1.0",
"gulp": "^4.0.2",
"jest": "^28.1.1",
"jest-environment-jsdom": "^28.1.1",
"jetifier": "^1.6.5",
"metro-react-native-babel-preset": "^0.66.2",
"nodemon": "^2.0.12",
"rollup": "^2.53.1",
"ts-jest": "^28.0.5",
"typescript": "^4.0.5",
"uglify-js": "^3.13.10"
}

View File

@ -10,7 +10,8 @@ const execa = require('execa');
const rootDir = path.dirname(path.dirname(path.dirname(__dirname)));
const mobileDir = `${rootDir}/packages/app-mobile`;
const outputDir = `${mobileDir}/lib/rnInjectedJs`;
const codeMirrorBundleFile = `${mobileDir}/components/NoteEditor/CodeMirror.bundle.min.js`;
const codeMirrorDir = `${mobileDir}/components/NoteEditor/CodeMirror`;
const codeMirrorBundleFile = `${codeMirrorDir}/CodeMirror.bundle.min.js`;
async function copyJs(name, filePath) {
const outputPath = `${outputDir}/${name}.js`;
@ -23,17 +24,16 @@ async function copyJs(name, filePath) {
async function buildCodeMirrorBundle() {
console.info('Building CodeMirror bundle...');
const sourceFile = `${mobileDir}/components/NoteEditor/CodeMirror.ts`;
const fullBundleFile = `${mobileDir}/components/NoteEditor/CodeMirror.bundle.js`;
const sourceFile = `${codeMirrorDir}/CodeMirror.ts`;
const fullBundleFile = `${codeMirrorDir}/CodeMirror.bundle.js`;
await execa('yarn', [
'run', 'rollup',
sourceFile,
'--name', 'codeMirrorBundle',
'--config', `${mobileDir}/injectedJS.config.js`,
'-f', 'iife',
'-o', fullBundleFile,
'-p', '@rollup/plugin-node-resolve',
'-p', '@rollup/plugin-typescript',
]);
// await execa('./node_modules/uglify-js/bin/uglifyjs', [
@ -49,7 +49,7 @@ async function main() {
await fs.mkdirp(outputDir);
await buildCodeMirrorBundle();
await copyJs('webviewLib', `${mobileDir}/../lib/renderers/webviewLib.js`);
await copyJs('CodeMirror.bundle', `${mobileDir}/components/NoteEditor/CodeMirror.bundle.min.js`);
await copyJs('CodeMirror.bundle', `${codeMirrorDir}/CodeMirror.bundle.min.js`);
}
module.exports = main;

View File

@ -1,10 +1,12 @@
{
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
"extends": "../../tsconfig.json",
"include": [
"**/*.ts",
"**/*.tsx",
],
"exclude": [
"**/node_modules",
"**/*.test.ts",
"**/*.test.tsx",
],
}
}

1559
yarn.lock

File diff suppressed because it is too large Load Diff