Skip to content

Commit c57adbe

Browse files
authored
Merge pull request #127 from whale4113/feat/lujunji/span
支持合并单元格,当导出表格为 HTML
2 parents cb8b05f + c36fd6d commit c57adbe

19 files changed

Lines changed: 306 additions & 196 deletions

.changeset/flat-parts-think.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@dolphin/chrome-extension': patch
3+
---
4+
5+
fix: error info length to long

.changeset/shiny-drinks-strive.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@dolphin/chrome-extension': patch
3+
---
4+
5+
feat: support option to convert table to html

.changeset/twelve-socks-crash.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@dolphin/lark': patch
3+
---
4+
5+
feat: support rowspan/colspan

apps/chrome-extension/src/common/issue.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,24 +32,31 @@ enum Label {
3232
function generateIssueUrl(issue: Issue): string {
3333
const { title, body, labels = [], template } = issue
3434

35-
const baseUrl =
36-
'https://github.com/whale4113/cloud-document-converter/issues/new'
37-
const params = new URLSearchParams({
38-
title: title,
39-
body: body,
40-
labels: labels.join(','),
41-
template,
42-
})
35+
const url = new URL(
36+
'https://github.com/whale4113/cloud-document-converter/issues/new',
37+
)
38+
39+
if (title) url.searchParams.set('title', title)
40+
if (body) url.searchParams.set('body', body)
41+
if (labels.length > 0) url.searchParams.set('labels', labels.join(','))
42+
if (template) url.searchParams.set('template', template)
4343

44-
return `${baseUrl}?${params.toString()}`
44+
return url.toString()
4545
}
4646

4747
export const reportBug = (error: unknown): void => {
48+
let errorInfo = JSON.stringify(serializeError(error), null, 2)
49+
const MAX_ERROR_LENGTH = 1000
50+
if (errorInfo.length > MAX_ERROR_LENGTH) {
51+
errorInfo =
52+
errorInfo.slice(0, MAX_ERROR_LENGTH) + '\n...[truncated due to length]'
53+
}
54+
4855
const url = generateIssueUrl({
4956
title: '',
5057
body: i18next.t(CommonTranslationKey.ISSUE_TEMPLATE_BODY, {
5158
version,
52-
errorInfo: JSON.stringify(serializeError(error), null, 2),
59+
errorInfo,
5360
ns: Namespace.COMMON,
5461
interpolation: { escapeValue: false },
5562
}),

apps/chrome-extension/src/common/settings.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export enum SettingKey {
77
Locale = 'general.locale',
88
Theme = 'general.theme',
99
DownloadMethod = 'download.method',
10-
TableWithNonPhrasingContent = 'general.table_with_non_phrasing_content',
10+
Table = 'general.table',
1111
Grid = 'general.grid',
1212
TextHighlight = 'general.text_highlight',
1313
DownloadFileWithUniqueName = 'download.file_with_unique_name',
@@ -24,8 +24,9 @@ export enum DownloadMethod {
2424
ShowSaveFilePicker = 'showSaveFilePicker',
2525
}
2626

27-
export enum TableWithNonPhrasingContent {
27+
export enum Table {
2828
Filtered = 'filtered',
29+
NonPhrasingContentToHTML = 'nonPhrasingContentToHTML',
2930
ToHTML = 'toHTML',
3031
}
3132

@@ -39,7 +40,7 @@ export interface Settings {
3940
[SettingKey.Locale]: string
4041
[SettingKey.Theme]: (typeof Theme)[keyof typeof Theme]
4142
[SettingKey.DownloadMethod]: (typeof DownloadMethod)[keyof typeof DownloadMethod]
42-
[SettingKey.TableWithNonPhrasingContent]: (typeof TableWithNonPhrasingContent)[keyof typeof TableWithNonPhrasingContent]
43+
[SettingKey.Table]: (typeof Table)[keyof typeof Table]
4344
[SettingKey.Grid]: (typeof Grid)[keyof typeof Grid]
4445
[SettingKey.TextHighlight]: boolean
4546
[SettingKey.DownloadFileWithUniqueName]: boolean
@@ -51,7 +52,7 @@ export const fallbackSettings: Settings = {
5152
[SettingKey.DownloadMethod]: supported
5253
? DownloadMethod.ShowSaveFilePicker
5354
: DownloadMethod.Direct,
54-
[SettingKey.TableWithNonPhrasingContent]: TableWithNonPhrasingContent.ToHTML,
55+
[SettingKey.Table]: Table.NonPhrasingContentToHTML,
5556
[SettingKey.Grid]: Grid.Flatten,
5657
[SettingKey.TextHighlight]: true,
5758
[SettingKey.DownloadFileWithUniqueName]: false,

apps/chrome-extension/src/common/utils.ts

Lines changed: 162 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ import {
99
} from '@dolphin/lark'
1010
import { v4 as uuidv4 } from 'uuid'
1111
import { Second, waitForFunction } from '@dolphin/common'
12+
import {
13+
SettingKey,
14+
Table as TableSetting,
15+
Grid as GridSetting,
16+
type Settings,
17+
} from './settings'
1218

1319
interface Ref<T> {
1420
current: T
@@ -101,37 +107,150 @@ export class UniqueFileName {
101107
}
102108
}
103109

104-
export const transformInvalidTablesToHtml = (
105-
invalidTables: TableWithParent[],
110+
export const mapTableBySettings = (
111+
tables: TableWithParent[],
112+
settings: Pick<Settings, SettingKey.Table>,
113+
): TableWithParent[] => {
114+
if (settings[SettingKey.Table] === TableSetting.Filtered) {
115+
return []
116+
}
117+
118+
return tables
119+
.map(table => {
120+
if (!table.inner.data?.invalid) return table
121+
122+
const tableIndex = table.parent?.children.findIndex(
123+
child => child === table.inner,
124+
)
125+
126+
if (tableIndex !== undefined && tableIndex !== -1) {
127+
const inner = {
128+
...table.inner,
129+
children: table.inner.children.map(row => ({
130+
...row,
131+
children: row.children.map(cell => ({
132+
...cell,
133+
children: cell.data?.invalidChildren ?? cell.children,
134+
})),
135+
})),
136+
} as mdast.Table
137+
138+
table.parent?.children.splice(tableIndex, 1, inner)
139+
140+
return {
141+
...table,
142+
inner,
143+
}
144+
}
145+
146+
return table
147+
})
148+
.filter(table =>
149+
settings[SettingKey.Table] === TableSetting.NonPhrasingContentToHTML
150+
? table.inner.data?.invalid
151+
: true,
152+
)
153+
}
154+
155+
/**
156+
* Filters out redundant cells that are covered by rowSpan/colSpan
157+
* and adds appropriate HTML properties to the spanning cells.
158+
*
159+
* @param table The Markdown AST table to process
160+
*/
161+
const processTableSpans = (table: mdast.Table): void => {
162+
const occupied: boolean[][] = []
163+
164+
for (let rowIndex = 0; rowIndex < table.children.length; rowIndex++) {
165+
const row = table.children[rowIndex]
166+
const newCells: mdast.TableCell[] = []
167+
168+
for (
169+
let columnIndex = 0;
170+
columnIndex < row.children.length;
171+
columnIndex++
172+
) {
173+
// If this position is covered by a previous spanning cell, skip it
174+
if (occupied[rowIndex]?.[columnIndex]) continue
175+
176+
const cell = row.children[columnIndex]
177+
newCells.push(cell)
178+
179+
const rowSpan = cell.data?.rowSpan ?? 1
180+
const colSpan = cell.data?.colSpan ?? 1
181+
182+
if (rowSpan > 1 || colSpan > 1) {
183+
// Ensure data and hProperties objects exist
184+
cell.data ??= {}
185+
cell.data.hProperties ??= {}
186+
187+
// Add HTML span properties for correct rendering
188+
if (rowSpan > 1) cell.data.hProperties['rowSpan'] = rowSpan
189+
if (colSpan > 1) cell.data.hProperties['colSpan'] = colSpan
190+
191+
// Mark the area covered by this spanning cell as occupied
192+
for (let i = 0; i < rowSpan; i++) {
193+
for (let j = 0; j < colSpan; j++) {
194+
// Skip the current cell itself
195+
if (i === 0 && j === 0) continue
196+
const targetRow = rowIndex + i
197+
const targetCol = columnIndex + j
198+
occupied[targetRow] ??= []
199+
occupied[targetRow][targetCol] = true
200+
}
201+
}
202+
}
203+
}
204+
205+
// Update row children with only the non-redundant cells
206+
row.children = newCells
207+
}
208+
}
209+
210+
export const transformTableToHtml = (
211+
tables: TableWithParent[],
106212
options: { allowDangerousHtml: boolean } = { allowDangerousHtml: false },
107213
): void => {
108-
invalidTables.forEach(invalidTable => {
109-
const invalidTableIndex = invalidTable.parent?.children.findIndex(
110-
child => child === invalidTable.inner,
214+
tables.forEach(table => {
215+
const tableIndex = table.parent?.children.findIndex(
216+
child => child === table.inner,
111217
)
112-
if (invalidTableIndex !== undefined && invalidTableIndex !== -1) {
113-
invalidTable.parent?.children.splice(invalidTableIndex, 1, {
218+
if (tableIndex !== undefined && tableIndex !== -1) {
219+
processTableSpans(table.inner)
220+
221+
const hastTable = toHast(table.inner, {
222+
allowDangerousHtml: options.allowDangerousHtml,
223+
})
224+
225+
if (hastTable.type === 'element') {
226+
const hastColGroup: hast.Element = {
227+
type: 'element',
228+
tagName: 'colgroup',
229+
properties: {},
230+
children:
231+
table.inner.data?.colWidths?.map(width => ({
232+
type: 'element',
233+
tagName: 'col',
234+
properties: {
235+
width:
236+
table.inner.data?.type === BlockType.GRID
237+
? `${width.toFixed(2)}%`
238+
: width,
239+
},
240+
children: [],
241+
})) ?? [],
242+
}
243+
244+
hastTable.children = ([hastColGroup] as hast.ElementContent[]).concat(
245+
hastTable.children,
246+
)
247+
}
248+
249+
table.parent?.children.splice(tableIndex, 1, {
114250
type: 'html',
115-
value: toHtml(
116-
toHast(
117-
{
118-
...invalidTable.inner,
119-
children: invalidTable.inner.children.map(row => ({
120-
...row,
121-
children: row.children.map(cell => ({
122-
...cell,
123-
children: cell.data?.invalidChildren ?? cell.children,
124-
})),
125-
})),
126-
} as mdast.Table,
127-
{
128-
allowDangerousHtml: options.allowDangerousHtml,
129-
},
130-
),
131-
{
132-
allowDangerousHtml: options.allowDangerousHtml,
133-
},
134-
),
251+
value: toHtml(hastTable, {
252+
allowDangerousHtml: options.allowDangerousHtml,
253+
}),
135254
})
136255
}
137256
})
@@ -245,36 +364,28 @@ export const transformMentionUsers = async (
245364
}
246365
}
247366

248-
export interface TransformTableWithParentsOptions {
249-
transformGridToHtml: boolean
250-
transformInvalidTablesToHtml: boolean
251-
}
252-
253-
export const transformTableWithParents = (
254-
tableWithParents: TableWithParent[],
255-
options: TransformTableWithParentsOptions,
367+
export const transformTableBySettings = (
368+
tables: TableWithParent[],
369+
settings: Pick<Settings, SettingKey.Table | SettingKey.Grid>,
256370
): void => {
257-
if (options.transformGridToHtml) {
371+
if (settings[SettingKey.Grid] === GridSetting.ToHTML) {
258372
transformGridToHtml(
259-
tableWithParents.filter(item => item.inner.data?.type === BlockType.GRID),
373+
tables.filter(item => item.inner.data?.type === BlockType.GRID),
260374
{
261375
allowDangerousHtml: true,
262376
},
263377
)
264378
}
265379

266-
if (options.transformInvalidTablesToHtml) {
267-
transformInvalidTablesToHtml(
268-
tableWithParents.filter(
269-
item =>
270-
item.inner.data?.invalid &&
271-
(options.transformGridToHtml
272-
? item.inner.data.type !== BlockType.GRID
273-
: true),
274-
),
275-
{
276-
allowDangerousHtml: true,
277-
},
278-
)
279-
}
380+
transformTableToHtml(
381+
mapTableBySettings(
382+
settings[SettingKey.Grid] === GridSetting.ToHTML
383+
? tables.filter(item => item.inner.data?.type !== BlockType.GRID)
384+
: tables,
385+
settings,
386+
),
387+
{
388+
allowDangerousHtml: true,
389+
},
390+
)
280391
}

0 commit comments

Comments
 (0)