You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-27 20:29:45 +02:00
Compare commits
8 Commits
server-v2.
...
table_edit
Author | SHA1 | Date | |
---|---|---|---|
|
b519d55abf | ||
|
5a862443d8 | ||
|
30e191663d | ||
|
70cd2395fb | ||
|
2f1b6fbee1 | ||
|
8c0d4a0f71 | ||
|
bc08c6dcc3 | ||
|
a06365039d |
@@ -421,6 +421,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
|
|||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
|
||||||
@@ -688,6 +691,9 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
|
|||||||
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
||||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||||
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
||||||
|
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
|
||||||
|
packages/app-desktop/gui/TableEditorDialog/Dialog.js
|
||||||
|
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
|
||||||
packages/app-desktop/gui/TagList.d.ts
|
packages/app-desktop/gui/TagList.d.ts
|
||||||
packages/app-desktop/gui/TagList.js
|
packages/app-desktop/gui/TagList.js
|
||||||
packages/app-desktop/gui/TagList.js.map
|
packages/app-desktop/gui/TagList.js.map
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -411,6 +411,9 @@ packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/styles/index.js.map
|
|||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/index.js.map
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.d.ts
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js
|
||||||
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/tables.js.map
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.d.ts
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js
|
||||||
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
|
packages/app-desktop/gui/NoteEditor/NoteBody/CodeMirror/utils/types.js.map
|
||||||
@@ -678,6 +681,9 @@ packages/app-desktop/gui/StyleSheets/StyleSheetContainer.js.map
|
|||||||
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
packages/app-desktop/gui/SyncWizard/Dialog.d.ts
|
||||||
packages/app-desktop/gui/SyncWizard/Dialog.js
|
packages/app-desktop/gui/SyncWizard/Dialog.js
|
||||||
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
packages/app-desktop/gui/SyncWizard/Dialog.js.map
|
||||||
|
packages/app-desktop/gui/TableEditorDialog/Dialog.d.ts
|
||||||
|
packages/app-desktop/gui/TableEditorDialog/Dialog.js
|
||||||
|
packages/app-desktop/gui/TableEditorDialog/Dialog.js.map
|
||||||
packages/app-desktop/gui/TagList.d.ts
|
packages/app-desktop/gui/TagList.d.ts
|
||||||
packages/app-desktop/gui/TagList.js
|
packages/app-desktop/gui/TagList.js
|
||||||
packages/app-desktop/gui/TagList.js.map
|
packages/app-desktop/gui/TagList.js.map
|
||||||
|
@@ -38,6 +38,7 @@ import ErrorBoundary from '../../../ErrorBoundary';
|
|||||||
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
||||||
import eventManager from '@joplin/lib/eventManager';
|
import eventManager from '@joplin/lib/eventManager';
|
||||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
||||||
|
import { checkTableIsUnderCursor, readTableAroundCursor } from './utils/tables';
|
||||||
|
|
||||||
const menuUtils = new MenuUtils(CommandService.instance());
|
const menuUtils = new MenuUtils(CommandService.instance());
|
||||||
|
|
||||||
@@ -753,7 +754,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
|||||||
|
|
||||||
const menu = new Menu();
|
const menu = new Menu();
|
||||||
|
|
||||||
const hasSelectedText = editorRef.current && !!editorRef.current.getSelection() ;
|
const cm = editorRef.current;
|
||||||
|
|
||||||
|
const hasSelectedText = cm && !!cm.getSelection() ;
|
||||||
|
|
||||||
|
const tableIsUnderCursor = checkTableIsUnderCursor(cm);
|
||||||
|
let tableUnderCursor: string = null;
|
||||||
|
|
||||||
|
if (tableIsUnderCursor) tableUnderCursor = readTableAroundCursor(cm);
|
||||||
|
|
||||||
menu.append(
|
menu.append(
|
||||||
new MenuItem({
|
new MenuItem({
|
||||||
@@ -785,6 +793,27 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (tableUnderCursor) {
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({ type: 'separator' })
|
||||||
|
);
|
||||||
|
|
||||||
|
menu.append(
|
||||||
|
new MenuItem({
|
||||||
|
label: _('Edit table...'),
|
||||||
|
click: async () => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'DIALOG_OPEN',
|
||||||
|
name: 'tableEditor',
|
||||||
|
props: {
|
||||||
|
markdownTable: tableUnderCursor,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
const spellCheckerMenuItems = SpellCheckerService.instance().contextMenuItems(params.misspelledWord, params.dictionarySuggestions);
|
||||||
|
|
||||||
for (const item of spellCheckerMenuItems) {
|
for (const item of spellCheckerMenuItems) {
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
function findElementWithClass(element: any, className: string): any {
|
||||||
|
if (element.classList && element.classList.contains(className)) return element;
|
||||||
|
|
||||||
|
for (const child of element.childNodes) {
|
||||||
|
const hasClass = findElementWithClass(child, className);
|
||||||
|
if (hasClass) return hasClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const checkTableIsUnderCursor = (cm: any) => {
|
||||||
|
if (!cm) return false;
|
||||||
|
|
||||||
|
const coords = cm.cursorCoords(cm.getCursor());
|
||||||
|
const element = document.elementFromPoint(coords.left, coords.top);
|
||||||
|
if (!element) return false;
|
||||||
|
return !!findElementWithClass(element, 'cm-jn-table-item');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readTableAroundCursor = (cm: any) => {
|
||||||
|
const idxAtCursor = cm.doc.getCursor().line;
|
||||||
|
const lineCount = cm.lineCount();
|
||||||
|
|
||||||
|
const lines: string[] = [];
|
||||||
|
|
||||||
|
for (let i = idxAtCursor - 1; i >= 0; i--) {
|
||||||
|
const line: string = cm.doc.getLine(i);
|
||||||
|
if (line.startsWith('|')) {
|
||||||
|
lines.splice(0, 0, line);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.push(cm.doc.getLine(idxAtCursor));
|
||||||
|
|
||||||
|
for (let i = idxAtCursor + 1; i < lineCount; i++) {
|
||||||
|
const line: string = cm.doc.getLine(i);
|
||||||
|
if (line.startsWith('|')) {
|
||||||
|
lines.push(line);
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines.join('\n');
|
||||||
|
};
|
@@ -165,6 +165,7 @@ export default function useJoplinMode(CodeMirror: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isMonospace) { token = `${token} jn-monospace`; }
|
if (isMonospace) { token = `${token} jn-monospace`; }
|
||||||
|
if (state.inTable) { token = `${token} jn-table-item`; }
|
||||||
// //////// End Monospace //////////
|
// //////// End Monospace //////////
|
||||||
|
|
||||||
return token;
|
return token;
|
||||||
|
@@ -22,6 +22,7 @@ import Dialog from './Dialog';
|
|||||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||||
|
import TableEditorDialog from './TableEditorDialog/Dialog';
|
||||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||||
@@ -38,6 +39,7 @@ interface Props {
|
|||||||
zoomFactor: number;
|
zoomFactor: number;
|
||||||
needApiAuth: boolean;
|
needApiAuth: boolean;
|
||||||
dialogs: AppStateDialog[];
|
dialogs: AppStateDialog[];
|
||||||
|
dialogContentMaxSize: Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModalDialogProps {
|
interface ModalDialogProps {
|
||||||
@@ -51,6 +53,7 @@ interface RegisteredDialogProps {
|
|||||||
themeId: number;
|
themeId: number;
|
||||||
key: string;
|
key: string;
|
||||||
dispatch: Function;
|
dispatch: Function;
|
||||||
|
dialogContentMaxSize: Size;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RegisteredDialog {
|
interface RegisteredDialog {
|
||||||
@@ -60,19 +63,25 @@ interface RegisteredDialog {
|
|||||||
const registeredDialogs: Record<string, RegisteredDialog> = {
|
const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||||
syncWizard: {
|
syncWizard: {
|
||||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||||
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
return <SyncWizardDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
masterPassword: {
|
masterPassword: {
|
||||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||||
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
return <MasterPasswordDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
editFolder: {
|
editFolder: {
|
||||||
render: (props: RegisteredDialogProps, customProps: any) => {
|
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||||
return <EditFolderDialog key={props.key} dispatch={props.dispatch} themeId={props.themeId} {...customProps}/>;
|
return <EditFolderDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
tableEditor: {
|
||||||
|
render: (props: RegisteredDialogProps, customProps: any) => {
|
||||||
|
return <TableEditorDialog key={props.key} dispatch={props.dispatch} dialogContentMaxSize={props.dialogContentMaxSize} themeId={props.themeId} {...customProps}/>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -195,10 +204,12 @@ class RootComponent extends React.Component<Props, any> {
|
|||||||
for (const dialog of props.dialogs) {
|
for (const dialog of props.dialogs) {
|
||||||
const md = registeredDialogs[dialog.name];
|
const md = registeredDialogs[dialog.name];
|
||||||
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
|
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
|
||||||
|
|
||||||
output.push(md.render({
|
output.push(md.render({
|
||||||
key: dialog.name,
|
key: dialog.name,
|
||||||
themeId: props.themeId,
|
themeId: props.themeId,
|
||||||
dispatch: props.dispatch,
|
dispatch: props.dispatch,
|
||||||
|
dialogContentMaxSize: props.dialogContentMaxSize,
|
||||||
}, dialog.props));
|
}, dialog.props));
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
@@ -245,6 +256,11 @@ const mapStateToProps = (state: AppState) => {
|
|||||||
themeId: state.settings.theme,
|
themeId: state.settings.theme,
|
||||||
needApiAuth: state.needApiAuth,
|
needApiAuth: state.needApiAuth,
|
||||||
dialogs: state.dialogs,
|
dialogs: state.dialogs,
|
||||||
|
dialogContentMaxSize: {
|
||||||
|
// Minus padding, margins and dialog header and button bar.
|
||||||
|
width: state.windowContentSize.width - 36 * 2,
|
||||||
|
height: state.windowContentSize.height - 36 * 2 - 28 - 30 - 20,
|
||||||
|
},
|
||||||
profileConfigCurrentProfileId: state.profileConfig.currentProfileId,
|
profileConfigCurrentProfileId: state.profileConfig.currentProfileId,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
101
packages/app-desktop/gui/TableEditorDialog/Dialog.tsx
Normal file
101
packages/app-desktop/gui/TableEditorDialog/Dialog.tsx
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { _ } from '@joplin/lib/locale';
|
||||||
|
import DialogButtonRow, { ClickEvent } from '../DialogButtonRow';
|
||||||
|
import Dialog from '../Dialog';
|
||||||
|
import DialogTitle from '../DialogTitle';
|
||||||
|
import { parseMarkdownTable } from '../../../lib/markdownUtils';
|
||||||
|
import { Size } from '../ResizableLayout/utils/types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
themeId: number;
|
||||||
|
dispatch: Function;
|
||||||
|
markdownTable: string;
|
||||||
|
dialogContentMaxSize: Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdownTableToObject = (markdownTable: string): any => {
|
||||||
|
const table = parseMarkdownTable(markdownTable);
|
||||||
|
|
||||||
|
return {
|
||||||
|
columns: table.headers.map(h => {
|
||||||
|
return {
|
||||||
|
title: h.label,
|
||||||
|
field: h.name,
|
||||||
|
hozAlign: h.justify,
|
||||||
|
editor: 'input',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
|
data: table.rows.map(row => {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function(props: Props) {
|
||||||
|
const elementId = `tabulator_${Math.floor(Math.random() * 1000000)}`;
|
||||||
|
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
props.dispatch({
|
||||||
|
type: 'DIALOG_CLOSE',
|
||||||
|
name: 'tableEditor',
|
||||||
|
});
|
||||||
|
}, [props.dispatch]);
|
||||||
|
|
||||||
|
const onButtonRowClick = useCallback(async (event: ClickEvent) => {
|
||||||
|
if (event.buttonName === 'cancel') {
|
||||||
|
onClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.buttonName === 'ok') {
|
||||||
|
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}, [onClose]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const table = markdownTableToObject(props.markdownTable);
|
||||||
|
const Tabulator = (window as any).Tabulator;
|
||||||
|
|
||||||
|
// TODO: probably doesn't need to be called every time
|
||||||
|
// TODO: Load CSS/JS dynamically?
|
||||||
|
// TODO: Clean up on exit
|
||||||
|
Tabulator.extendModule('edit', 'editors', {});
|
||||||
|
|
||||||
|
new Tabulator(`#${elementId}`, {
|
||||||
|
...table,
|
||||||
|
height: props.dialogContentMaxSize.height,
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function renderContent() {
|
||||||
|
return (
|
||||||
|
<div className="dialog-content">
|
||||||
|
<div id={elementId}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDialogWrapper() {
|
||||||
|
return (
|
||||||
|
<div className="dialog-root">
|
||||||
|
<DialogTitle title={_('Edit table')}/>
|
||||||
|
{renderContent()}
|
||||||
|
<DialogButtonRow
|
||||||
|
themeId={props.themeId}
|
||||||
|
onClick={onButtonRowClick}
|
||||||
|
okButtonLabel={_('Save')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog onClose={onClose} renderContent={renderDialogWrapper}/>
|
||||||
|
);
|
||||||
|
}
|
@@ -7,6 +7,10 @@
|
|||||||
uses 'eval'.
|
uses 'eval'.
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
|
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' 'unsafe-eval'">
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
To add files below, then need to be in the "vendor" directory. To make this happen, use copyApplicationAssets.js
|
||||||
|
-->
|
||||||
<title>Joplin</title>
|
<title>Joplin</title>
|
||||||
<link rel="stylesheet" href="style.min.css">
|
<link rel="stylesheet" href="style.min.css">
|
||||||
<link rel="stylesheet" href="style/icons/style.css">
|
<link rel="stylesheet" href="style/icons/style.css">
|
||||||
@@ -15,6 +19,9 @@
|
|||||||
<link rel="stylesheet" href="vendor/lib/smalltalk/css/smalltalk.css">
|
<link rel="stylesheet" href="vendor/lib/smalltalk/css/smalltalk.css">
|
||||||
<link rel="stylesheet" href="vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css">
|
<link rel="stylesheet" href="vendor/lib/roboto-fontface/css/roboto/roboto-fontface.css">
|
||||||
<link rel="stylesheet" href="vendor/lib/codemirror/lib/codemirror.css">
|
<link rel="stylesheet" href="vendor/lib/codemirror/lib/codemirror.css">
|
||||||
|
<link rel="stylesheet" href="vendor/lib/tabulator-tables/dist/css/tabulator.min.css">
|
||||||
|
|
||||||
|
<script type="text/javascript" src="vendor/lib/tabulator-tables/dist/js/tabulator.min.js"></script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.smalltalk {
|
.smalltalk {
|
||||||
|
@@ -174,6 +174,7 @@
|
|||||||
"styled-components": "5.1.1",
|
"styled-components": "5.1.1",
|
||||||
"styled-system": "5.1.5",
|
"styled-system": "5.1.5",
|
||||||
"taboverride": "^4.0.3",
|
"taboverride": "^4.0.3",
|
||||||
|
"tabulator-tables": "^5.1.4",
|
||||||
"tinymce": "^5.2.0"
|
"tinymce": "^5.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -83,6 +83,8 @@ async function main() {
|
|||||||
'codemirror/addon/dialog/dialog.css',
|
'codemirror/addon/dialog/dialog.css',
|
||||||
'@joeattardi/emoji-button/dist/index.js',
|
'@joeattardi/emoji-button/dist/index.js',
|
||||||
'mark.js/dist/mark.min.js',
|
'mark.js/dist/mark.min.js',
|
||||||
|
'tabulator-tables/dist/css/tabulator.min.css',
|
||||||
|
'tabulator-tables/dist/js/tabulator.min.js',
|
||||||
{
|
{
|
||||||
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
|
src: resolve(__dirname, '../../lib/services/plugins/sandboxProxy.js'),
|
||||||
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,
|
dest: `${buildLibDir}/@joplin/lib/services/plugins/sandboxProxy.js`,
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import markdownUtils from './markdownUtils';
|
import markdownUtils, { parseMarkdownTable } from './markdownUtils';
|
||||||
|
|
||||||
describe('Should detect list items', () => {
|
describe('Should detect list items', () => {
|
||||||
test('should detect `- lorem ipsum` as list item ', () => {
|
test('should detect `- lorem ipsum` as list item ', () => {
|
||||||
@@ -91,4 +91,51 @@ describe('Should detect list items', () => {
|
|||||||
expect(markdownUtils.isEmptyListItem('+ [x]')).toBe(false);
|
expect(markdownUtils.isEmptyListItem('+ [x]')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should parse a Markdown table', () => {
|
||||||
|
const table = parseMarkdownTable(`
|
||||||
|
| Name | Town | Comment |
|
||||||
|
|----------:|:-------:|----|
|
||||||
|
| John | London | None |
|
||||||
|
| Paul | Liverpool | **test bold** |
|
||||||
|
| Ringo | Sheffield | <a href="#">link</a>  |
|
||||||
|
`.trim().split('\n').map(l => l.trim()).join('\n'));
|
||||||
|
|
||||||
|
expect(table).toEqual({
|
||||||
|
'headers': [
|
||||||
|
{
|
||||||
|
'label': 'Name',
|
||||||
|
'name': 'c0',
|
||||||
|
'justify': 'right',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Town',
|
||||||
|
'name': 'c1',
|
||||||
|
'justify': 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': 'Comment',
|
||||||
|
'name': 'c2',
|
||||||
|
'justify': 'left',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'rows': [
|
||||||
|
{
|
||||||
|
'c0': 'John',
|
||||||
|
'c1': 'London',
|
||||||
|
'c2': 'None',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'c0': 'Paul',
|
||||||
|
'c1': 'Liverpool',
|
||||||
|
'c2': '**test bold**',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'c0': 'Ringo',
|
||||||
|
'c1': 'Sheffield',
|
||||||
|
'c2': '<a href="#">link</a> ',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -10,7 +10,7 @@ const emptyListRegex = /^(\s*)([*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s+)$/;
|
|||||||
export enum MarkdownTableJustify {
|
export enum MarkdownTableJustify {
|
||||||
Left = 'left',
|
Left = 'left',
|
||||||
Center = 'center',
|
Center = 'center',
|
||||||
Right = 'right,',
|
Right = 'right',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownTableHeader {
|
export interface MarkdownTableHeader {
|
||||||
@@ -25,6 +25,11 @@ export interface MarkdownTableRow {
|
|||||||
[key: string]: string;
|
[key: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MarkdownTable {
|
||||||
|
headers: MarkdownTableHeader[];
|
||||||
|
rows: MarkdownTableRow[];
|
||||||
|
}
|
||||||
|
|
||||||
const markdownUtils = {
|
const markdownUtils = {
|
||||||
// Titles for markdown links only need escaping for [ and ]
|
// Titles for markdown links only need escaping for [ and ]
|
||||||
escapeTitleText(text: string) {
|
escapeTitleText(text: string) {
|
||||||
@@ -206,4 +211,95 @@ const markdownUtils = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const parseMarkdownTable = (tableMarkdown: string): MarkdownTable => {
|
||||||
|
interface Token {
|
||||||
|
type: string;
|
||||||
|
content: string;
|
||||||
|
attrGet: (name: string)=> string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getJustifyFromStyle = (token: Token): MarkdownTableJustify => {
|
||||||
|
const style = token.attrGet('style');
|
||||||
|
if (!style) return MarkdownTableJustify.Left;
|
||||||
|
if (style.includes('text-align:right')) return MarkdownTableJustify.Right;
|
||||||
|
if (style.includes('text-align:left')) return MarkdownTableJustify.Left;
|
||||||
|
if (style.includes('text-align:center')) return MarkdownTableJustify.Center;
|
||||||
|
return MarkdownTableJustify.Left;
|
||||||
|
};
|
||||||
|
|
||||||
|
const env = {};
|
||||||
|
const markdownIt = new MarkdownIt();
|
||||||
|
const tokens: Token[] = markdownIt.parse(tableMarkdown, env);
|
||||||
|
const headers: MarkdownTableHeader[] = [];
|
||||||
|
const rows: MarkdownTableRow[] = [];
|
||||||
|
|
||||||
|
let state = 'start';
|
||||||
|
let headerIndex = 0;
|
||||||
|
let rowIndex = -1;
|
||||||
|
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (state === 'start') {
|
||||||
|
if (token.type !== 'table_open') {
|
||||||
|
throw new Error('Expected table_open token');
|
||||||
|
} else {
|
||||||
|
state = 'open';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'thead_open') {
|
||||||
|
state = 'header';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'header') {
|
||||||
|
if (token.type === 'th_open') {
|
||||||
|
headers.push({
|
||||||
|
label: '',
|
||||||
|
name: `c${headerIndex}`,
|
||||||
|
justify: getJustifyFromStyle(token),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'inline') {
|
||||||
|
headers[headerIndex].label += token.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'th_close') {
|
||||||
|
headerIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'thead_close') {
|
||||||
|
state = 'content';
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'content') {
|
||||||
|
if (token.type === 'tr_open') {
|
||||||
|
state = 'row';
|
||||||
|
rows.push({});
|
||||||
|
rowIndex++;
|
||||||
|
headerIndex = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state === 'row') {
|
||||||
|
if (token.type === 'inline') {
|
||||||
|
rows[rowIndex][`c${headerIndex}`] = token.content;
|
||||||
|
headerIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.type === 'tr_close') {
|
||||||
|
state = 'content';
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { headers, rows };
|
||||||
|
};
|
||||||
|
|
||||||
export default markdownUtils;
|
export default markdownUtils;
|
||||||
|
@@ -3298,6 +3298,7 @@ __metadata:
|
|||||||
styled-components: 5.1.1
|
styled-components: 5.1.1
|
||||||
styled-system: 5.1.5
|
styled-system: 5.1.5
|
||||||
taboverride: ^4.0.3
|
taboverride: ^4.0.3
|
||||||
|
tabulator-tables: ^5.1.4
|
||||||
tinymce: ^5.2.0
|
tinymce: ^5.2.0
|
||||||
typescript: 4.0.5
|
typescript: 4.0.5
|
||||||
dependenciesMeta:
|
dependenciesMeta:
|
||||||
@@ -29096,6 +29097,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"tabulator-tables@npm:^5.1.4":
|
||||||
|
version: 5.1.4
|
||||||
|
resolution: "tabulator-tables@npm:5.1.4"
|
||||||
|
checksum: f77f9e975502253ec945d66d5d2f1fb615743824a05c6e8b67013c8f53976edb3c734ef1286c24253d85dd3990e596e3f04ecc1fbd0682541b5ed1a5a6c0d762
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"taketalk@npm:^1.0.0":
|
"taketalk@npm:^1.0.0":
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
resolution: "taketalk@npm:1.0.0"
|
resolution: "taketalk@npm:1.0.0"
|
||||||
|
Reference in New Issue
Block a user