Merge pull request #14653 from overleaf/mj-column-spacing-parsing

[visual] Support cell spacing declarations

GitOrigin-RevId: 16b4ddc196558679301010378912b14f6295e05f
This commit is contained in:
Mathias Jakobsen 2023-09-05 09:27:12 +01:00 committed by Copybot
parent d98288605a
commit ad38ac233b
5 changed files with 101 additions and 30 deletions

View file

@ -29,6 +29,8 @@ export type ColumnDefinition = {
borderLeft: number borderLeft: number
borderRight: number borderRight: number
content: string content: string
cellSpacingLeft: string
cellSpacingRight: string
} }
export type CellData = { export type CellData = {

View file

@ -71,7 +71,9 @@ export const setBorders = (
], ],
}) })
} else if (theme === BorderTheme.FULLY_BORDERED) { } else if (theme === BorderTheme.FULLY_BORDERED) {
const newSpec = addColumnBordersToSpecification(specification) const newSpec = generateColumnSpecification(
addColumnBordersToSpecification(table.columns)
)
const insertColumns = view.state.changes({ const insertColumns = view.state.changes({
from: positions.columnDeclarations.from, from: positions.columnDeclarations.from,
@ -110,14 +112,14 @@ export const setBorders = (
for (const row of table.rows) { for (const row of table.rows) {
for (const cell of row.cells) { for (const cell of row.cells) {
if (cell.multiColumn) { if (cell.multiColumn) {
const specification = view.state.sliceDoc(
cell.multiColumn.columns.from,
cell.multiColumn.columns.to
)
addMulticolumnBorders.push({ addMulticolumnBorders.push({
from: cell.multiColumn.columns.from, from: cell.multiColumn.columns.from,
to: cell.multiColumn.columns.to, to: cell.multiColumn.columns.to,
insert: addColumnBordersToSpecification(specification), insert: generateColumnSpecification(
addColumnBordersToSpecification(
cell.multiColumn.columns.specification
)
),
}) })
} }
} }
@ -129,24 +131,13 @@ export const setBorders = (
} }
} }
const addColumnBordersToSpecification = (specification: string) => { const addColumnBordersToSpecification = (specification: ColumnDefinition[]) => {
let newSpec = '|' const newSpec = specification.map(column => ({
let consumingBrackets = 0 ...column,
for (const char of specification) { borderLeft: 1,
if (char === '{') { borderRight: 0,
consumingBrackets++ }))
} newSpec[newSpec.length - 1].borderRight = 1
if (char === '}' && consumingBrackets > 0) {
consumingBrackets--
}
if (consumingBrackets) {
newSpec += char
}
if (char === '|') {
continue
}
newSpec += char + '|'
}
return newSpec return newSpec
} }
@ -216,8 +207,18 @@ export const setAlignment = (
const generateColumnSpecification = (columns: ColumnDefinition[]) => { const generateColumnSpecification = (columns: ColumnDefinition[]) => {
return columns return columns
.map( .map(
({ borderLeft, borderRight, content }) => ({
`${'|'.repeat(borderLeft)}${content}${'|'.repeat(borderRight)}` borderLeft,
borderRight,
content,
cellSpacingLeft,
cellSpacingRight,
}) =>
`${'|'.repeat(
borderLeft
)}${cellSpacingLeft}${content}${cellSpacingRight}${'|'.repeat(
borderRight
)}`
) )
.join('') .join('')
} }
@ -427,6 +428,8 @@ export const insertColumn = (
borderLeft: 0, borderLeft: 0,
borderRight, borderRight,
content: 'l', content: 'l',
cellSpacingLeft: '',
cellSpacingRight: '',
})) }))
) )
if (targetIndex === 0 && borderTheme === BorderTheme.FULLY_BORDERED) { if (targetIndex === 0 && borderTheme === BorderTheme.FULLY_BORDERED) {

View file

@ -12,6 +12,24 @@ export type RowPosition = {
hlines: { from: number; to: number }[] hlines: { from: number; to: number }[]
} }
function parseArgument(spec: string, startIndex: number): number {
if (spec.charAt(startIndex) !== '{') {
throw new Error('Missing opening brace')
}
let depth = 0
for (let i = startIndex; i < spec.length; i++) {
if (spec.charAt(i) === '{') {
depth++
} else if (spec.charAt(i) === '}') {
depth--
}
if (depth === 0) {
return i
}
}
throw new Error('Missing closing brace')
}
export function parseColumnSpecifications( export function parseColumnSpecifications(
specification: string specification: string
): ColumnDefinition[] { ): ColumnDefinition[] {
@ -20,6 +38,8 @@ export function parseColumnSpecifications(
let currentBorderLeft = 0 let currentBorderLeft = 0
let currentBorderRight = 0 let currentBorderRight = 0
let currentContent = '' let currentContent = ''
let currentCellSpacingLeft = ''
let currentCellSpacingRight = ''
function maybeCommit() { function maybeCommit() {
if (currentAlignment !== undefined) { if (currentAlignment !== undefined) {
columns.push({ columns.push({
@ -27,11 +47,15 @@ export function parseColumnSpecifications(
borderLeft: currentBorderLeft, borderLeft: currentBorderLeft,
borderRight: currentBorderRight, borderRight: currentBorderRight,
content: currentContent, content: currentContent,
cellSpacingLeft: currentCellSpacingLeft,
cellSpacingRight: currentCellSpacingRight,
}) })
currentAlignment = undefined currentAlignment = undefined
currentBorderLeft = 0 currentBorderLeft = 0
currentBorderRight = 0 currentBorderRight = 0
currentContent = '' currentContent = ''
currentCellSpacingLeft = ''
currentCellSpacingRight = ''
} }
} }
for (let i = 0; i < specification.length; i++) { for (let i = 0; i < specification.length; i++) {
@ -65,12 +89,31 @@ export function parseColumnSpecifications(
currentAlignment = 'paragraph' currentAlignment = 'paragraph'
currentContent += 'p' currentContent += 'p'
// TODO: Parse these details // TODO: Parse these details
while (i < specification.length && specification.charAt(i) !== '}') { const argumentEnd = parseArgument(specification, i + 1)
i++ // Don't include the p twice
currentContent += specification.charAt(i) currentContent += specification.slice(i + 1, argumentEnd + 1)
i = argumentEnd
break
}
case '@':
case '!': {
const argumentEnd = parseArgument(specification, i + 1)
// Include the @/!
const argument = specification.slice(i, argumentEnd + 1)
i = argumentEnd
if (currentAlignment) {
// We have a cell, so this is right cell spacing
currentCellSpacingRight = argument
} else {
currentCellSpacingLeft = argument
} }
break break
} }
case ' ':
case '\n':
case '\t':
currentContent += char
break
} }
} }
maybeCommit() maybeCommit()
@ -279,7 +322,6 @@ function parseTabularBody(
} else if (isHLine(currentChild)) { } else if (isHLine(currentChild)) {
const lastCell = getLastCell() const lastCell = getLastCell()
if (lastCell?.content.trim()) { if (lastCell?.content.trim()) {
console.error(lastCell)
throw new Error('\\hline must be at the start of a row') throw new Error('\\hline must be at the start of a row')
} }
// push start of cell past the hline // push start of cell past the hline

View file

@ -22,6 +22,7 @@ export class TabularWidget extends WidgetType {
try { try {
this.parseResult = generateTable(tabularNode, state) this.parseResult = generateTable(tabularNode, state)
} catch (e) { } catch (e) {
console.error(e)
this.parseResult = null this.parseResult = null
} }
} }

View file

@ -422,5 +422,28 @@ cell 3 & cell 4 \\\\
cy.get('.ol-cm-command-caption').should('not.exist') cy.get('.ol-cm-command-caption').should('not.exist')
cy.get('.ol-cm-command-label').should('not.exist') cy.get('.ol-cm-command-label').should('not.exist')
}) })
it('Renders a table with custom column spacing', function () {
mountEditor(`
\\begin{tabular}{@{}c@{}l!{}}
cell 1 & cell 2 \\\\
cell 3 & cell 4 \\\\
\\end{tabular}`)
checkTable([
['cell 1', 'cell 2'],
['cell 3', 'cell 4'],
])
cy.get('.table-generator-cell').first().click()
cy.get('.table-generator-floating-toolbar').as('toolbar').should('exist')
cy.get('@toolbar').findByText('No borders').click()
cy.get('.table-generator').findByText('All borders').click()
// The element is partially covered, but we can still click it
cy.get('.cm-line').first().click({ force: true })
checkTable([
['cell 1', 'cell 2'],
['cell 3', 'cell 4'],
])
checkBordersWithNoMultiColumn([true, true, true], [true, true, true])
})
}) })
}) })