You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-24 20:19:10 +02:00
Compare commits
8 Commits
v3.4.4
...
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.js
|
||||
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.js
|
||||
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.js
|
||||
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.js
|
||||
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.js
|
||||
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.js
|
||||
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.js
|
||||
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.js
|
||||
packages/app-desktop/gui/TagList.js.map
|
||||
|
@@ -38,6 +38,7 @@ import ErrorBoundary from '../../../ErrorBoundary';
|
||||
import { MarkupToHtmlOptions } from '../../utils/useMarkupToHtml';
|
||||
import eventManager from '@joplin/lib/eventManager';
|
||||
import { EditContextMenuFilterObject } from '@joplin/lib/services/plugins/api/JoplinWorkspace';
|
||||
import { checkTableIsUnderCursor, readTableAroundCursor } from './utils/tables';
|
||||
|
||||
const menuUtils = new MenuUtils(CommandService.instance());
|
||||
|
||||
@@ -753,7 +754,14 @@ function CodeMirror(props: NoteBodyEditorProps, ref: any) {
|
||||
|
||||
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(
|
||||
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);
|
||||
|
||||
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 (state.inTable) { token = `${token} jn-table-item`; }
|
||||
// //////// End Monospace //////////
|
||||
|
||||
return token;
|
||||
|
@@ -22,6 +22,7 @@ import Dialog from './Dialog';
|
||||
import SyncWizardDialog from './SyncWizard/Dialog';
|
||||
import MasterPasswordDialog from './MasterPasswordDialog/Dialog';
|
||||
import EditFolderDialog from './EditFolderDialog/Dialog';
|
||||
import TableEditorDialog from './TableEditorDialog/Dialog';
|
||||
import StyleSheetContainer from './StyleSheets/StyleSheetContainer';
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ResourceScreen } = require('./ResourceScreen.js');
|
||||
@@ -38,6 +39,7 @@ interface Props {
|
||||
zoomFactor: number;
|
||||
needApiAuth: boolean;
|
||||
dialogs: AppStateDialog[];
|
||||
dialogContentMaxSize: Size;
|
||||
}
|
||||
|
||||
interface ModalDialogProps {
|
||||
@@ -51,6 +53,7 @@ interface RegisteredDialogProps {
|
||||
themeId: number;
|
||||
key: string;
|
||||
dispatch: Function;
|
||||
dialogContentMaxSize: Size;
|
||||
}
|
||||
|
||||
interface RegisteredDialog {
|
||||
@@ -60,19 +63,25 @@ interface RegisteredDialog {
|
||||
const registeredDialogs: Record<string, RegisteredDialog> = {
|
||||
syncWizard: {
|
||||
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: {
|
||||
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: {
|
||||
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) {
|
||||
const md = registeredDialogs[dialog.name];
|
||||
if (!md) throw new Error(`Unknown dialog: ${dialog.name}`);
|
||||
|
||||
output.push(md.render({
|
||||
key: dialog.name,
|
||||
themeId: props.themeId,
|
||||
dispatch: props.dispatch,
|
||||
dialogContentMaxSize: props.dialogContentMaxSize,
|
||||
}, dialog.props));
|
||||
}
|
||||
return output;
|
||||
@@ -245,6 +256,11 @@ const mapStateToProps = (state: AppState) => {
|
||||
themeId: state.settings.theme,
|
||||
needApiAuth: state.needApiAuth,
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
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'.
|
||||
<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>
|
||||
<link rel="stylesheet" href="style.min.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/roboto-fontface/css/roboto/roboto-fontface.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>
|
||||
.smalltalk {
|
||||
|
@@ -174,6 +174,7 @@
|
||||
"styled-components": "5.1.1",
|
||||
"styled-system": "5.1.5",
|
||||
"taboverride": "^4.0.3",
|
||||
"tabulator-tables": "^5.1.4",
|
||||
"tinymce": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
@@ -83,6 +83,8 @@ async function main() {
|
||||
'codemirror/addon/dialog/dialog.css',
|
||||
'@joeattardi/emoji-button/dist/index.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'),
|
||||
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', () => {
|
||||
test('should detect `- lorem ipsum` as list item ', () => {
|
||||
@@ -91,4 +91,51 @@ describe('Should detect list items', () => {
|
||||
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 {
|
||||
Left = 'left',
|
||||
Center = 'center',
|
||||
Right = 'right,',
|
||||
Right = 'right',
|
||||
}
|
||||
|
||||
export interface MarkdownTableHeader {
|
||||
@@ -25,6 +25,11 @@ export interface MarkdownTableRow {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface MarkdownTable {
|
||||
headers: MarkdownTableHeader[];
|
||||
rows: MarkdownTableRow[];
|
||||
}
|
||||
|
||||
const markdownUtils = {
|
||||
// Titles for markdown links only need escaping for [ and ]
|
||||
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;
|
||||
|
@@ -3298,6 +3298,7 @@ __metadata:
|
||||
styled-components: 5.1.1
|
||||
styled-system: 5.1.5
|
||||
taboverride: ^4.0.3
|
||||
tabulator-tables: ^5.1.4
|
||||
tinymce: ^5.2.0
|
||||
typescript: 4.0.5
|
||||
dependenciesMeta:
|
||||
@@ -29096,6 +29097,13 @@ __metadata:
|
||||
languageName: node
|
||||
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":
|
||||
version: 1.0.0
|
||||
resolution: "taketalk@npm:1.0.0"
|
||||
|
Reference in New Issue
Block a user