mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-29 17:13:38 -05:00
Merge pull request #14548 from overleaf/mj-early-exit-table-rendering
[visual] Show code when table generator fails rendering GitOrigin-RevId: 6c1908b0c68cc965e445736f0c320f322d23c988
This commit is contained in:
parent
601365bcc6
commit
8e6d6f8689
7 changed files with 125 additions and 56 deletions
|
@ -3,9 +3,9 @@ import { Positions, TableData, TableRenderingError } from '../tabular'
|
||||||
import {
|
import {
|
||||||
CellPosition,
|
CellPosition,
|
||||||
CellSeparator,
|
CellSeparator,
|
||||||
|
ParsedTableData,
|
||||||
RowPosition,
|
RowPosition,
|
||||||
RowSeparator,
|
RowSeparator,
|
||||||
generateTable,
|
|
||||||
parseTableEnvironment,
|
parseTableEnvironment,
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
|
@ -34,34 +34,12 @@ const TableContext = createContext<
|
||||||
>(undefined)
|
>(undefined)
|
||||||
|
|
||||||
export const TableProvider: FC<{
|
export const TableProvider: FC<{
|
||||||
|
tableData: ParsedTableData
|
||||||
|
tableNode: SyntaxNode | null
|
||||||
tabularNode: SyntaxNode
|
tabularNode: SyntaxNode
|
||||||
view: EditorView
|
view: EditorView
|
||||||
tableNode: SyntaxNode | null
|
}> = ({ tableData, children, tableNode, tabularNode, view }) => {
|
||||||
}> = ({ tabularNode, view, children, tableNode }) => {
|
|
||||||
try {
|
try {
|
||||||
const tableData = generateTable(tabularNode, view.state)
|
|
||||||
|
|
||||||
// TODO: Validate better that the table matches the column definition
|
|
||||||
for (const row of tableData.table.rows) {
|
|
||||||
const rowLength = row.cells.reduce(
|
|
||||||
(acc, cell) => acc + (cell.multiColumn?.columnSpan ?? 1),
|
|
||||||
0
|
|
||||||
)
|
|
||||||
for (const cell of row.cells) {
|
|
||||||
if (
|
|
||||||
cell.multiColumn?.columns.specification &&
|
|
||||||
cell.multiColumn.columns.specification.length !== 1
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
'Multi-column cells must have exactly one column definition'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (rowLength !== tableData.table.columns.length) {
|
|
||||||
throw new Error('Row length does not match column definition')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const positions: Positions = {
|
const positions: Positions = {
|
||||||
cells: tableData.cellPositions,
|
cells: tableData.cellPositions,
|
||||||
columnDeclarations: tableData.specification,
|
columnDeclarations: tableData.specification,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { SyntaxNode } from '@lezer/common'
|
import { SyntaxNode } from '@lezer/common'
|
||||||
import { FC, useEffect } from 'react'
|
import { FC, useEffect } from 'react'
|
||||||
import { CellPosition, RowPosition } from './utils'
|
import { CellPosition, ParsedTableData, RowPosition } from './utils'
|
||||||
import { Toolbar } from './toolbar/toolbar'
|
import { Toolbar } from './toolbar/toolbar'
|
||||||
import { Table } from './table'
|
import { Table } from './table'
|
||||||
import {
|
import {
|
||||||
|
@ -182,7 +182,8 @@ export const Tabular: FC<{
|
||||||
tabularNode: SyntaxNode
|
tabularNode: SyntaxNode
|
||||||
view: EditorView
|
view: EditorView
|
||||||
tableNode: SyntaxNode | null
|
tableNode: SyntaxNode | null
|
||||||
}> = ({ tabularNode, view, tableNode }) => {
|
parsedTableData: ParsedTableData
|
||||||
|
}> = ({ tabularNode, view, tableNode, parsedTableData }) => {
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary
|
<ErrorBoundary
|
||||||
fallbackRender={() => (
|
fallbackRender={() => (
|
||||||
|
@ -193,8 +194,9 @@ export const Tabular: FC<{
|
||||||
<TabularProvider>
|
<TabularProvider>
|
||||||
<TableProvider
|
<TableProvider
|
||||||
tabularNode={tabularNode}
|
tabularNode={tabularNode}
|
||||||
view={view}
|
tableData={parsedTableData}
|
||||||
tableNode={tableNode}
|
tableNode={tableNode}
|
||||||
|
view={view}
|
||||||
>
|
>
|
||||||
<SelectionContextProvider>
|
<SelectionContextProvider>
|
||||||
<EditingContextProvider>
|
<EditingContextProvider>
|
||||||
|
|
|
@ -315,17 +315,19 @@ function parseTabularBody(
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateTable(
|
export type ParsedTableData = {
|
||||||
node: SyntaxNode,
|
|
||||||
state: EditorState
|
|
||||||
): {
|
|
||||||
table: TableData
|
table: TableData
|
||||||
cellPositions: CellPosition[][]
|
cellPositions: CellPosition[][]
|
||||||
specification: { from: number; to: number }
|
specification: { from: number; to: number }
|
||||||
rowPositions: RowPosition[]
|
rowPositions: RowPosition[]
|
||||||
rowSeparators: RowSeparator[]
|
rowSeparators: RowSeparator[]
|
||||||
cellSeparators: CellSeparator[][]
|
cellSeparators: CellSeparator[][]
|
||||||
} {
|
}
|
||||||
|
|
||||||
|
export function generateTable(
|
||||||
|
node: SyntaxNode,
|
||||||
|
state: EditorState
|
||||||
|
): ParsedTableData {
|
||||||
const specification = node
|
const specification = node
|
||||||
.getChild('BeginEnv')
|
.getChild('BeginEnv')
|
||||||
?.getChild('TextArgument')
|
?.getChild('TextArgument')
|
||||||
|
|
|
@ -65,6 +65,7 @@ import { IndicatorWidget } from './visual-widgets/indicator'
|
||||||
import { TabularWidget } from './visual-widgets/tabular'
|
import { TabularWidget } from './visual-widgets/tabular'
|
||||||
import { nextSnippetField, pickedCompletion } from '@codemirror/autocomplete'
|
import { nextSnippetField, pickedCompletion } from '@codemirror/autocomplete'
|
||||||
import { skipPreambleWithCursor } from './skip-preamble-cursor'
|
import { skipPreambleWithCursor } from './skip-preamble-cursor'
|
||||||
|
import { TableRenderingErrorWidget } from './visual-widgets/table-rendering-error'
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
fileTreeManager: {
|
fileTreeManager: {
|
||||||
|
@ -323,20 +324,33 @@ export const atomicDecorations = (options: Options) => {
|
||||||
nodeRef.node,
|
nodeRef.node,
|
||||||
'TableEnvironment'
|
'TableEnvironment'
|
||||||
)
|
)
|
||||||
decorations.push(
|
const tabularWidget = new TabularWidget(
|
||||||
Decoration.replace({
|
nodeRef.node,
|
||||||
widget: new TabularWidget(
|
state.doc.sliceString(
|
||||||
nodeRef.node,
|
(tableNode ?? nodeRef).from,
|
||||||
state.doc.sliceString(
|
(tableNode ?? nodeRef).to
|
||||||
(tableNode ?? nodeRef).from,
|
),
|
||||||
(tableNode ?? nodeRef).to
|
tableNode,
|
||||||
),
|
state
|
||||||
tableNode
|
|
||||||
),
|
|
||||||
block: true,
|
|
||||||
}).range(nodeRef.from, nodeRef.to)
|
|
||||||
)
|
)
|
||||||
return false
|
|
||||||
|
if (tabularWidget.isValid()) {
|
||||||
|
decorations.push(
|
||||||
|
Decoration.replace({
|
||||||
|
widget: tabularWidget,
|
||||||
|
block: true,
|
||||||
|
}).range(nodeRef.from, nodeRef.to)
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
// Show error message
|
||||||
|
decorations.push(
|
||||||
|
Decoration.widget({
|
||||||
|
widget: new TableRenderingErrorWidget(tableNode),
|
||||||
|
block: true,
|
||||||
|
}).range(nodeRef.from, nodeRef.from)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (nodeRef.type.is('BeginEnv')) {
|
} else if (nodeRef.type.is('BeginEnv')) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { EditorView } from '@codemirror/view'
|
import { EditorView } from '@codemirror/view'
|
||||||
|
|
||||||
export const tableGeneratorTheme = EditorView.baseTheme({
|
export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
'&dark .table-generator': {
|
'&dark .table-generator-container': {
|
||||||
'--table-generator-active-border-color': '#ccc',
|
'--table-generator-active-border-color': '#ccc',
|
||||||
'--table-generator-coming-soon-background-color': '#41464f',
|
'--table-generator-coming-soon-background-color': '#41464f',
|
||||||
'--table-generator-coming-soon-color': '#fff',
|
'--table-generator-coming-soon-color': '#fff',
|
||||||
|
@ -23,6 +23,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
'rgba(125,125,125,0.3)',
|
'rgba(125,125,125,0.3)',
|
||||||
'--table-generator-toolbar-dropdown-disabled-color': '#999',
|
'--table-generator-toolbar-dropdown-disabled-color': '#999',
|
||||||
'--table-generator-toolbar-shadow-color': '#1e253029',
|
'--table-generator-toolbar-shadow-color': '#1e253029',
|
||||||
|
'--table-generator-error-background': '#F1F4F9',
|
||||||
},
|
},
|
||||||
|
|
||||||
'&light .table-generator': {
|
'&light .table-generator': {
|
||||||
|
@ -46,6 +47,7 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
'--table-generator-toolbar-dropdown-disabled-background': '#f2f2f2',
|
'--table-generator-toolbar-dropdown-disabled-background': '#f2f2f2',
|
||||||
'--table-generator-toolbar-dropdown-disabled-color': 'var(--neutral-40)',
|
'--table-generator-toolbar-dropdown-disabled-color': 'var(--neutral-40)',
|
||||||
'--table-generator-toolbar-shadow-color': '#1e253029',
|
'--table-generator-toolbar-shadow-color': '#1e253029',
|
||||||
|
'--table-generator-error-background': '#F1F4F9',
|
||||||
},
|
},
|
||||||
|
|
||||||
'.table-generator': {
|
'.table-generator': {
|
||||||
|
@ -401,20 +403,24 @@ export const tableGeneratorTheme = EditorView.baseTheme({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'.ol-cm-environment-table.table-generator-error-container, .ol-cm-environment-table.ol-cm-tabular':
|
||||||
|
{
|
||||||
|
background: 'rgba(125, 125, 125, 0.05)',
|
||||||
|
},
|
||||||
|
|
||||||
'.table-generator-error': {
|
'.table-generator-error': {
|
||||||
background:
|
background: 'var(--table-generator-error-background)',
|
||||||
'linear-gradient(0deg, #f9f1f1, #f9f1f1), linear-gradient(0deg, #f5beba, #f5beba)',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
'justify-content': 'space-between',
|
'justify-content': 'space-between',
|
||||||
color: 'black',
|
color: 'black',
|
||||||
border: '1px solid #f5beba',
|
border: '1px solid #C3D0E3',
|
||||||
'font-family': 'Lato',
|
'font-family': 'Lato',
|
||||||
'margin-bottom': '0',
|
margin: '0 16px 0 16px',
|
||||||
'& .table-generator-error-message': {
|
'& .table-generator-error-message': {
|
||||||
flex: '1 0 auto',
|
flex: '1 1 auto',
|
||||||
},
|
},
|
||||||
'& .table-generator-error-icon': {
|
'& .table-generator-error-icon': {
|
||||||
color: '#b83a33',
|
color: '#3265B2',
|
||||||
'margin-right': '12px',
|
'margin-right': '12px',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { WidgetType } from '@codemirror/view'
|
||||||
|
import { SyntaxNode } from '@lezer/common'
|
||||||
|
|
||||||
|
export class TableRenderingErrorWidget extends WidgetType {
|
||||||
|
private hasTableNode: boolean
|
||||||
|
constructor(tableNode: SyntaxNode | null | undefined) {
|
||||||
|
super()
|
||||||
|
this.hasTableNode = Boolean(tableNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDOM(): HTMLElement {
|
||||||
|
const warning = document.createElement('div')
|
||||||
|
warning.classList.add('table-generator-error', 'alert')
|
||||||
|
warning.role = 'alert'
|
||||||
|
const icon = document.createElement('span')
|
||||||
|
icon.classList.add('table-generator-error-icon')
|
||||||
|
const iconType = document.createElement('i')
|
||||||
|
iconType.classList.add('fa', 'fa-info-circle')
|
||||||
|
icon.appendChild(iconType)
|
||||||
|
warning.appendChild(icon)
|
||||||
|
const message = document.createElement('span')
|
||||||
|
message.classList.add('table-generator-error-message')
|
||||||
|
message.textContent =
|
||||||
|
'We couldn’t render your table.\nThis could be because some features of this table are not supported in the table preview yet, or due to a LaTeX error in the table code.'
|
||||||
|
warning.appendChild(message)
|
||||||
|
const element = document.createElement('div')
|
||||||
|
element.classList.add('table-generator', 'table-generator-error-container')
|
||||||
|
element.appendChild(warning)
|
||||||
|
if (this.hasTableNode) {
|
||||||
|
element.classList.add('ol-cm-environment-table')
|
||||||
|
}
|
||||||
|
return element
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +1,59 @@
|
||||||
import { EditorView, WidgetType } from '@codemirror/view'
|
import { EditorView, WidgetType } from '@codemirror/view'
|
||||||
|
import { EditorState } from '@codemirror/state'
|
||||||
import { SyntaxNode } from '@lezer/common'
|
import { SyntaxNode } from '@lezer/common'
|
||||||
import * as ReactDOM from 'react-dom'
|
import * as ReactDOM from 'react-dom'
|
||||||
import { Tabular } from '../../../components/table-generator/tabular'
|
import { Tabular } from '../../../components/table-generator/tabular'
|
||||||
|
import {
|
||||||
|
ParsedTableData,
|
||||||
|
generateTable,
|
||||||
|
} from '../../../components/table-generator/utils'
|
||||||
|
|
||||||
export class TabularWidget extends WidgetType {
|
export class TabularWidget extends WidgetType {
|
||||||
private element: HTMLElement | undefined
|
private element: HTMLElement | undefined
|
||||||
|
private readonly parseResult: ParsedTableData
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private tabularNode: SyntaxNode,
|
private tabularNode: SyntaxNode,
|
||||||
private content: string,
|
private content: string,
|
||||||
private tableNode: SyntaxNode | null
|
private tableNode: SyntaxNode | null,
|
||||||
|
state: EditorState
|
||||||
) {
|
) {
|
||||||
super()
|
super()
|
||||||
|
this.parseResult = generateTable(tabularNode, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
isValid() {
|
||||||
|
for (const row of this.parseResult.table.rows) {
|
||||||
|
const rowLength = row.cells.reduce(
|
||||||
|
(acc, cell) => acc + (cell.multiColumn?.columnSpan ?? 1),
|
||||||
|
0
|
||||||
|
)
|
||||||
|
for (const cell of row.cells) {
|
||||||
|
if (
|
||||||
|
cell.multiColumn?.columns.specification &&
|
||||||
|
cell.multiColumn.columns.specification.length !== 1
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (rowLength !== this.parseResult.table.columns.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
toDOM(view: EditorView) {
|
toDOM(view: EditorView) {
|
||||||
this.element = document.createElement('div')
|
this.element = document.createElement('div')
|
||||||
this.element.classList.add('ol-cm-tabular')
|
this.element.classList.add('ol-cm-tabular')
|
||||||
this.element.style.backgroundColor = 'rgba(125, 125, 125, 0.05)'
|
if (this.tableNode) {
|
||||||
|
this.element.classList.add('ol-cm-environment-table')
|
||||||
|
}
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<Tabular
|
<Tabular
|
||||||
view={view}
|
view={view}
|
||||||
tabularNode={this.tabularNode}
|
tabularNode={this.tabularNode}
|
||||||
|
parsedTableData={this.parseResult}
|
||||||
tableNode={this.tableNode}
|
tableNode={this.tableNode}
|
||||||
/>,
|
/>,
|
||||||
this.element
|
this.element
|
||||||
|
@ -44,6 +76,7 @@ export class TabularWidget extends WidgetType {
|
||||||
<Tabular
|
<Tabular
|
||||||
view={view}
|
view={view}
|
||||||
tabularNode={this.tabularNode}
|
tabularNode={this.tabularNode}
|
||||||
|
parsedTableData={this.parseResult}
|
||||||
tableNode={this.tableNode}
|
tableNode={this.tableNode}
|
||||||
/>,
|
/>,
|
||||||
this.element
|
this.element
|
||||||
|
|
Loading…
Reference in a new issue