fix: new line render issue in formula

This commit is contained in:
Ramesh Mane
2025-12-27 18:19:24 +00:00
parent bcd55e9277
commit bce7880d95
5 changed files with 90 additions and 49 deletions

View File

@@ -105,6 +105,7 @@ export const LongTextCellRenderer: CellRenderer = {
fillStyle: pv ? '#3366FF' : textColor,
height,
cellRenderStore: props.cellRenderStore,
renderAsPreTag: true,
})
if (!props.tag?.renderAsTag && selected) {

View File

@@ -2,7 +2,7 @@ import { renderMultiLineText, renderTagLabel } from '../utils/canvas'
export const SingleLineTextCellRenderer: CellRenderer = {
render: (ctx, props) => {
const { value, x, y, width, height, pv, padding, textColor = '#4a5268' } = props
const { value, x, y, width, height, pv, padding, textColor = themeV4Colors.gray['600'], getColor, formula } = props
const text = (Array.isArray(value) ? value.join(',') : value?.toString()) ?? ''
if (!text) {
@@ -23,6 +23,7 @@ export const SingleLineTextCellRenderer: CellRenderer = {
fontFamily: `${pv ? 600 : 500} 13px Inter`,
fillStyle: pv ? '#3366FF' : textColor,
height,
renderAsPreTag: formula,
})
return {

View File

@@ -470,68 +470,94 @@ export const wrapTextToLines = (
maxWidth,
maxLines,
firstLineMaxWidth,
}: { text: string; maxWidth: number; maxLines: number; firstLineMaxWidth?: number },
renderAsPreTag,
}: { text: string; maxWidth: number; maxLines: number; firstLineMaxWidth?: number; renderAsPreTag?: boolean },
): string[] => {
if (maxLines === 0) return [] // If maxLines is 0, return an empty array
const lines: string[] = [] // Stores the wrapped lines
let remainingText = text // Keep track of unprocessed text
// First, split the text by explicit newlines (\n)
const textSegments = renderAsPreTag ? text.split('\n') : [text]
// Determine the max width for the first line
let currentMaxWidth = firstLineMaxWidth ?? maxWidth
while (remainingText.length > 0 && lines.length < maxLines) {
let start = 0
let end = remainingText.length
let line = ''
let width = 0
for (let segmentIndex = 0; segmentIndex < textSegments.length && lines.length < maxLines; segmentIndex++) {
const segment = textSegments[segmentIndex] || ''
// Binary search to find the max substring that fits within currentMaxWidth
while (start < end) {
const mid = Math.floor((start + end) / 2)
const testText = remainingText.slice(0, mid + 1)
const testWidth = ctx.measureText(testText).width
if (testWidth <= currentMaxWidth) {
line = testText // Store the longest valid substring
width = testWidth
start = mid + 1 // Try a longer substring
// If the segment is empty (from consecutive \n\n), add a blank line
if (segment === '') {
// If this would be the last line, show ellipsis instead of blank line
if (lines.length >= maxLines - 1) {
lines.push('...')
} else {
end = mid // Reduce the search space
lines.push('')
}
continue
}
// Handle word breaking: Prevent splitting words mid-way
const lastSpaceIndex = line.lastIndexOf(' ')
if (
lastSpaceIndex !== -1 && // There is at least one space in the line
remainingText[line.length] !== ' ' && // The line ends mid-word
remainingText.length > line.length && // There is more text left
lines.length < maxLines - 1 // We are not on the last line
) {
// If the line ends mid-word, break at the last space
line = line.slice(0, lastSpaceIndex)
width = ctx.measureText(line).width
}
let remainingText = segment // Current text segment to process
// Handle truncation with ellipsis for the last line
if (lines.length === maxLines - 1 && remainingText.length > line.length) {
const ellipsis = '...'
const ellipsisWidth = ctx.measureText(ellipsis).width
while (remainingText.length > 0 && lines.length < maxLines) {
let start = 0
let end = remainingText.length
let line = ''
let width = 0
if (width + ellipsisWidth > currentMaxWidth && line.length > 0) {
line = truncateText(ctx, line, currentMaxWidth - ellipsisWidth, false, false) // Truncate the line to fit within maxWidth
// Binary search to find the max substring that fits within currentMaxWidth
while (start < end) {
const mid = Math.floor((start + end) / 2)
const testText = remainingText.slice(0, mid + 1)
const testWidth = ctx.measureText(testText).width
if (testWidth <= currentMaxWidth) {
line = testText // Store the longest valid substring
width = testWidth
start = mid + 1 // Try a longer substring
} else {
end = mid // Reduce the search space
}
}
// Handle word breaking: Prevent splitting words mid-way
const lastSpaceIndex = line.lastIndexOf(' ')
if (
lastSpaceIndex !== -1 && // There is at least one space in the line
remainingText[line.length] !== ' ' && // The line ends mid-word
remainingText.length > line.length && // There is more text left
lines.length < maxLines - 1 // We are not on the last line
) {
// If the line ends mid-word, break at the last space
line = line.slice(0, lastSpaceIndex)
width = ctx.measureText(line).width
}
line += ellipsis // Add ellipsis to indicate truncation
// Handle truncation with ellipsis for the last line
if (lines.length === maxLines - 1 && remainingText.length > line.length) {
const ellipsis = '...'
const ellipsisWidth = ctx.measureText(ellipsis).width
if (width + ellipsisWidth > currentMaxWidth && line.length > 0) {
line = truncateText(ctx, line, currentMaxWidth - ellipsisWidth, false, false) // Truncate the line to fit within maxWidth
width = ctx.measureText(line).width
}
line += ellipsis // Add ellipsis to indicate truncation
}
lines.push(line) // Store the current line
remainingText = remainingText.slice(line.length).trimStart() // Remove the rendered part and trim leading spaces
// After the first line, all lines use maxWidth for consistency
currentMaxWidth = maxWidth
// If this is not the last segment and we haven't reached maxLines yet,
// move to the next segment to represent the explicit newline
if (segmentIndex < textSegments.length - 1 && lines.length < maxLines && remainingText.length === 0) {
break // Exit the inner while loop to process the next segment
}
}
lines.push(line) // Store the current line
remainingText = remainingText.slice(line.length).trimStart() // Remove the rendered part and trim leading spaces
// After the first line, all lines use maxWidth for consistency
currentMaxWidth = maxWidth
}
return lines
@@ -857,12 +883,17 @@ export function renderMultiLineText(
py = 10,
firstLineMaxWidth, // Allows different width for the first line
} = params
let { maxWidth = Infinity, maxLines } = params
let { maxWidth = Infinity, maxLines, renderAsPreTag = false } = params
if (maxWidth < 0) {
maxWidth = 0
}
if (rowHeightInPx['1'] === height) {
renderAsPreTag = false
}
if (ncIsUndefined(maxLines)) {
if (rowHeightInPx['1'] === height) {
maxLines = 1 // Only one line if rowHeightInPx['1'] matches height
@@ -889,7 +920,8 @@ export function renderMultiLineText(
lines = cachedText.lines
width = cachedText.width
} else {
lines = wrapTextToLines(ctx, { text, maxWidth, maxLines, firstLineMaxWidth })
lines = wrapTextToLines(ctx, { text, maxWidth, maxLines, firstLineMaxWidth, renderAsPreTag })
console.log('lines', lines)
width = Math.min(Math.max(...lines.map((line) => ctx.measureText(line).width)), maxWidth)
multiLineTextCache.set(cacheKey, { lines, width })

View File

@@ -34,6 +34,7 @@ export interface RenderMultiLineTextProps extends RenderSingleLineTextProps {
firstLineMaxWidth?: number
yOffset?: number
selected?: boolean
renderAsPreTag?: boolean
}
export interface RenderRectangleProps {

View File

@@ -157,11 +157,17 @@ export function jsepTreeToFormula(node, isCallExpId = false) {
function escapeLiteral(v: string) {
return (
v
// replace \ to \\, escape only unescaped \
// 1⃣ Temporarily protect real newline
.replace(/\\n/g, '__NC_NEWLINE__')
// 2⃣ Escape unescaped backslashes (replace \ to \\, escape only unescaped \)
.replace(/([^\\]|^)\\(?!\\)/g, `$1\\\\`)
// replace " to \"
// 3⃣ Escape quotes (replace " to \" and ' to \')
.replace(/([^\\]|^)"/g, `$1\\"`)
// replace ' to \'
.replace(/([^\\]|^)'/g, `$1\\'`)
// 4⃣ Restore protected newline and tab
.replace(/__NC_NEWLINE__/g, '\\n')
);
}