Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions apps/chrome-extension/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,167 @@ export const transformGridToHtml = (
}
}

const readMergeInfoFromDom = (
cellBlockIds: number[],
): { rowSpan: number; colSpan: number }[] | null => {
const result: { rowSpan: number; colSpan: number }[] = []
let hasAnyMerge = false
let missingCount = 0

for (const id of cellBlockIds) {
const td = document.querySelector<HTMLTableCellElement>(
`td[data-block-id="${String(id)}"]`,
)

if (!td) {
missingCount++
result.push({ rowSpan: 1, colSpan: 1 })
continue
}

if (td.style.display === 'none') {
result.push({ rowSpan: 0, colSpan: 0 })
continue
}

const rowSpan = td.rowSpan
const colSpan = td.colSpan
result.push({ rowSpan, colSpan })
if (rowSpan > 1 || colSpan > 1) hasAnyMerge = true
}

if (missingCount === cellBlockIds.length) return null
return hasAnyMerge ? result : null
}

interface TableDataWithBlockInfo {
recordId?: string
cellBlockIds?: number[]
mergeInfo?: { rowSpan: number; colSpan: number }[]
type?: string
}

export const resolveMergedTablesFromDom = async (
tableWithParents: TableWithParent[],
): Promise<void> => {
for (const entry of tableWithParents) {
const table = entry.inner
const data = table.data as TableDataWithBlockInfo | undefined
if (data?.mergeInfo) continue
if (data?.type !== BlockType.TABLE) continue

const cellBlockIds = data.cellBlockIds
if (!cellBlockIds || cellBlockIds.length === 0) continue

let mergeInfo = readMergeInfoFromDom(cellBlockIds)

if (!mergeInfo && data.recordId) {
try {
await waitForFunction(
() =>
Docx.locateBlockWithRecordId(data.recordId ?? '').then(
isSuccess =>
isSuccess &&
document.querySelector(
`td[data-block-id="${String(cellBlockIds[0])}"]`,
) !== null,
),
{ timeout: 3 * Second },
)
mergeInfo = readMergeInfoFromDom(cellBlockIds)
} catch {
continue
}
}

if (!mergeInfo) continue

table.data = { ...table.data, mergeInfo }

const allCells = table.children.flatMap(row => row.children)
if (mergeInfo.length === allCells.length) {
allCells.forEach((cell, i) => {
cell.data = {
...cell.data,
rowSpan: mergeInfo[i].rowSpan,
colSpan: mergeInfo[i].colSpan,
}
})
}
}
}

const wrapConsecutiveImages = (html: string): string =>
html.replace(/(<img\b[^>]*>)(\s*<img\b[^>]*>)+/g, match => {
const imgs = match.match(/<img\b[^>]*>/g)
if (!imgs || imgs.length < 2) return match
const ths = imgs.map(img => `<th>${img}</th>`).join('')
return `<table><thead><tr>${ths}</tr></thead></table>`
})

const cellContentToHtml = (cell: mdast.TableCell): string => {
const children = cell.data?.invalidChildren ?? cell.children
if (children.length === 0) return ''

const paragraph: mdast.Paragraph = {
type: 'paragraph',
children: children as mdast.PhrasingContent[],
}

const html = toHtml(toHast(paragraph, { allowDangerousHtml: true }), {
allowDangerousHtml: true,
})
.replace(/^<p>/, '')
.replace(/<\/p>\s*$/, '')
.trim()

return wrapConsecutiveImages(html)
}

export const transformMergedTablesToHtml = (
mergedTables: TableWithParent[],
): void => {
for (const entry of mergedTables) {
const tableIndex = entry.parent?.children.findIndex(
child => child === entry.inner,
)
if (tableIndex === undefined || tableIndex === -1) continue

const table = entry.inner
const rows = table.children

const lines: string[] = ['<table>']

for (const row of rows) {
lines.push('<tr>')

for (const cell of row.children) {
const rowSpan = cell.data?.rowSpan ?? 1
const colSpan = cell.data?.colSpan ?? 1

if (rowSpan === 0 || colSpan === 0) continue

const attrs: string[] = []
if (rowSpan > 1) attrs.push(`rowspan="${String(rowSpan)}"`)
if (colSpan > 1) attrs.push(`colspan="${String(colSpan)}"`)

const attrStr = attrs.length > 0 ? ' ' + attrs.join(' ') : ''
const content = cellContentToHtml(cell)
lines.push(`<td${attrStr}>${content}</td>`)
}

lines.push('</tr>')
}

lines.push('</table>')

entry.parent?.children.splice(tableIndex, 1, {
type: 'html',
value: lines.join('\n'),
})
}
}

export const transformMentionUsers = async (
mentionUsers: mdast.InlineCode[],
): Promise<void> => {
Expand Down Expand Up @@ -254,6 +415,10 @@ export const transformTableWithParents = (
tableWithParents: TableWithParent[],
options: TransformTableWithParentsOptions,
): void => {
transformMergedTablesToHtml(
tableWithParents.filter(item => item.inner.data?.mergeInfo),
)

if (options.transformGridToHtml) {
transformGridToHtml(
tableWithParents.filter(item => item.inner.data?.type === BlockType.GRID),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { reportBug } from '../common/issue'
import {
transformMentionUsers,
transformTableWithParents,
resolveMergedTablesFromDom,
} from '../common/utils'
import {
getSettings,
Expand Down Expand Up @@ -93,6 +94,7 @@ const main = async () => {
)

await transformMentionUsers(mentionUsers)
await resolveMergedTablesFromDom(tableWithParents)

const tokens = images
.map(image => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
UniqueFileName,
withSignal,
transformTableWithParents,
resolveMergedTablesFromDom,
} from '../common/utils'
import {
getSettings,
Expand Down Expand Up @@ -565,6 +566,7 @@ const main = async (options: { signal?: AbortSignal } = {}) => {
})

await transformMentionUsers(mentionUsers)
await resolveMergedTablesFromDom(tableWithParents)

const recommendName = docx.pageTitle
? normalizeFileName(docx.pageTitle.slice(0, OneHundred))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { reportBug } from '../common/issue'
import {
transformMentionUsers,
transformTableWithParents,
resolveMergedTablesFromDom,
} from '../common/utils'
import {
getSettings,
Expand Down Expand Up @@ -98,6 +99,7 @@ const main = async () => {
)

await transformMentionUsers(mentionUsers)
await resolveMergedTablesFromDom(tableWithParents)

const tokens = images
.map(image => {
Expand Down
52 changes: 52 additions & 0 deletions packages/lark/src/docx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ declare module 'mdast' {
type?: BlockType.TABLE | BlockType.GRID
colWidths?: number[]
invalid?: boolean
mergeInfo?: { rowSpan: number; colSpan: number }[]
recordId?: string
cellBlockIds?: number[]
}

interface TableCellData {
width?: number
invalidChildren?: mdast.Nodes[]
rowSpan?: number
colSpan?: number
}

interface InlineCodeData {
Expand Down Expand Up @@ -265,12 +270,18 @@ interface ImageBlock extends Block {
}
}

interface MergeInfo {
row_span: number
col_span: number
}

interface TableBlock extends Block<TableCellBlock> {
type: BlockType.TABLE
snapshot: {
type: BlockType.TABLE
rows_id: string[]
columns_id: string[]
merge_info?: MergeInfo[]
}
}

Expand Down Expand Up @@ -1120,6 +1131,21 @@ export class Transformer {
},
) {}

private resolveMergeInfo(
block: TableBlock | Grid,
): { rowSpan: number; colSpan: number }[] | undefined {
if (block.type !== BlockType.TABLE) return undefined

if (block.snapshot.merge_info) {
return block.snapshot.merge_info.map(info => ({
rowSpan: info.row_span,
colSpan: info.col_span,
}))
}

return undefined
}

private normalizeImage(image: mdast.Image): mdast.Image | mdast.Paragraph {
return this.parent?.type === 'tableCell'
? image
Expand Down Expand Up @@ -1434,6 +1460,8 @@ export class Transformer {
}
case BlockType.TABLE:
case BlockType.GRID: {
const mergeInfo = this.resolveMergeInfo(block)

let table: mdast.Table = {
type: 'table',
children: [],
Expand All @@ -1456,11 +1484,35 @@ export class Transformer {
? widthCells.map(cell => cell.data.width)
: undefined

if (mergeInfo && mergeInfo.length === tableCells.length) {
tableCells.forEach((cell, i) => {
cell.data = {
...cell.data,
rowSpan: mergeInfo[i].rowSpan,
colSpan: mergeInfo[i].colSpan,
}
})
}

const hasMergedCells =
mergeInfo?.some(info => info.rowSpan > 1 || info.colSpan > 1) ??
false

const cellBlockIds =
block.type === BlockType.TABLE
? block.children.map(child => child.id)
: undefined
const recordId =
block.type === BlockType.TABLE ? block.record?.id : undefined

table.data = {
...table.data,
type: block.type,
...(colWidths ? { colWidths } : {}),
invalid: tableCells.some(cell => cell.data?.invalidChildren),
...(hasMergedCells ? { mergeInfo } : {}),
...(cellBlockIds ? { cellBlockIds } : {}),
...(recordId ? { recordId } : {}),
}

return (
Expand Down
Loading
Loading