Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/flat-parts-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dolphin/chrome-extension': patch
---

fix: error info length to long
5 changes: 5 additions & 0 deletions .changeset/shiny-drinks-strive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dolphin/chrome-extension': patch
---

feat: support option to convert table to html
5 changes: 5 additions & 0 deletions .changeset/twelve-socks-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@dolphin/lark': patch
---

feat: support rowspan/colspan
27 changes: 17 additions & 10 deletions apps/chrome-extension/src/common/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,24 +32,31 @@ enum Label {
function generateIssueUrl(issue: Issue): string {
const { title, body, labels = [], template } = issue

const baseUrl =
'https://github.com/whale4113/cloud-document-converter/issues/new'
const params = new URLSearchParams({
title: title,
body: body,
labels: labels.join(','),
template,
})
const url = new URL(
'https://github.com/whale4113/cloud-document-converter/issues/new',
)

if (title) url.searchParams.set('title', title)
if (body) url.searchParams.set('body', body)
if (labels.length > 0) url.searchParams.set('labels', labels.join(','))
if (template) url.searchParams.set('template', template)

return `${baseUrl}?${params.toString()}`
return url.toString()
}

export const reportBug = (error: unknown): void => {
let errorInfo = JSON.stringify(serializeError(error), null, 2)
const MAX_ERROR_LENGTH = 1000
if (errorInfo.length > MAX_ERROR_LENGTH) {
errorInfo =
errorInfo.slice(0, MAX_ERROR_LENGTH) + '\n...[truncated due to length]'
}

const url = generateIssueUrl({
title: '',
body: i18next.t(CommonTranslationKey.ISSUE_TEMPLATE_BODY, {
version,
errorInfo: JSON.stringify(serializeError(error), null, 2),
errorInfo,
ns: Namespace.COMMON,
interpolation: { escapeValue: false },
}),
Expand Down
9 changes: 5 additions & 4 deletions apps/chrome-extension/src/common/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export enum SettingKey {
Locale = 'general.locale',
Theme = 'general.theme',
DownloadMethod = 'download.method',
TableWithNonPhrasingContent = 'general.table_with_non_phrasing_content',
Table = 'general.table',
Grid = 'general.grid',
TextHighlight = 'general.text_highlight',
DownloadFileWithUniqueName = 'download.file_with_unique_name',
Expand All @@ -24,8 +24,9 @@ export enum DownloadMethod {
ShowSaveFilePicker = 'showSaveFilePicker',
}

export enum TableWithNonPhrasingContent {
export enum Table {
Filtered = 'filtered',
NonPhrasingContentToHTML = 'nonPhrasingContentToHTML',
ToHTML = 'toHTML',
}

Expand All @@ -39,7 +40,7 @@ export interface Settings {
[SettingKey.Locale]: string
[SettingKey.Theme]: (typeof Theme)[keyof typeof Theme]
[SettingKey.DownloadMethod]: (typeof DownloadMethod)[keyof typeof DownloadMethod]
[SettingKey.TableWithNonPhrasingContent]: (typeof TableWithNonPhrasingContent)[keyof typeof TableWithNonPhrasingContent]
[SettingKey.Table]: (typeof Table)[keyof typeof Table]
[SettingKey.Grid]: (typeof Grid)[keyof typeof Grid]
[SettingKey.TextHighlight]: boolean
[SettingKey.DownloadFileWithUniqueName]: boolean
Expand All @@ -51,7 +52,7 @@ export const fallbackSettings: Settings = {
[SettingKey.DownloadMethod]: supported
? DownloadMethod.ShowSaveFilePicker
: DownloadMethod.Direct,
[SettingKey.TableWithNonPhrasingContent]: TableWithNonPhrasingContent.ToHTML,
[SettingKey.Table]: Table.NonPhrasingContentToHTML,
[SettingKey.Grid]: Grid.Flatten,
[SettingKey.TextHighlight]: true,
[SettingKey.DownloadFileWithUniqueName]: false,
Expand Down
213 changes: 162 additions & 51 deletions apps/chrome-extension/src/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import {
} from '@dolphin/lark'
import { v4 as uuidv4 } from 'uuid'
import { Second, waitForFunction } from '@dolphin/common'
import {
SettingKey,
Table as TableSetting,
Grid as GridSetting,
type Settings,
} from './settings'

interface Ref<T> {
current: T
Expand Down Expand Up @@ -101,37 +107,150 @@ export class UniqueFileName {
}
}

export const transformInvalidTablesToHtml = (
invalidTables: TableWithParent[],
export const mapTableBySettings = (
tables: TableWithParent[],
settings: Pick<Settings, SettingKey.Table>,
): TableWithParent[] => {
if (settings[SettingKey.Table] === TableSetting.Filtered) {
return []
}

return tables
.map(table => {
if (!table.inner.data?.invalid) return table

const tableIndex = table.parent?.children.findIndex(
child => child === table.inner,
)

if (tableIndex !== undefined && tableIndex !== -1) {
const inner = {
...table.inner,
children: table.inner.children.map(row => ({
...row,
children: row.children.map(cell => ({
...cell,
children: cell.data?.invalidChildren ?? cell.children,
})),
})),
} as mdast.Table

table.parent?.children.splice(tableIndex, 1, inner)

return {
...table,
inner,
}
}

return table
})
.filter(table =>
settings[SettingKey.Table] === TableSetting.NonPhrasingContentToHTML
? table.inner.data?.invalid
: true,
)
}

/**
* Filters out redundant cells that are covered by rowSpan/colSpan
* and adds appropriate HTML properties to the spanning cells.
*
* @param table The Markdown AST table to process
*/
const processTableSpans = (table: mdast.Table): void => {
const occupied: boolean[][] = []

for (let rowIndex = 0; rowIndex < table.children.length; rowIndex++) {
const row = table.children[rowIndex]
const newCells: mdast.TableCell[] = []

for (
let columnIndex = 0;
columnIndex < row.children.length;
columnIndex++
) {
// If this position is covered by a previous spanning cell, skip it
if (occupied[rowIndex]?.[columnIndex]) continue

const cell = row.children[columnIndex]
newCells.push(cell)

const rowSpan = cell.data?.rowSpan ?? 1
const colSpan = cell.data?.colSpan ?? 1

if (rowSpan > 1 || colSpan > 1) {
// Ensure data and hProperties objects exist
cell.data ??= {}
cell.data.hProperties ??= {}

// Add HTML span properties for correct rendering
if (rowSpan > 1) cell.data.hProperties['rowSpan'] = rowSpan
if (colSpan > 1) cell.data.hProperties['colSpan'] = colSpan

// Mark the area covered by this spanning cell as occupied
for (let i = 0; i < rowSpan; i++) {
for (let j = 0; j < colSpan; j++) {
// Skip the current cell itself
if (i === 0 && j === 0) continue
const targetRow = rowIndex + i
const targetCol = columnIndex + j
occupied[targetRow] ??= []
occupied[targetRow][targetCol] = true
}
}
}
}

// Update row children with only the non-redundant cells
row.children = newCells
}
}

export const transformTableToHtml = (
tables: TableWithParent[],
options: { allowDangerousHtml: boolean } = { allowDangerousHtml: false },
): void => {
invalidTables.forEach(invalidTable => {
const invalidTableIndex = invalidTable.parent?.children.findIndex(
child => child === invalidTable.inner,
tables.forEach(table => {
const tableIndex = table.parent?.children.findIndex(
child => child === table.inner,
)
if (invalidTableIndex !== undefined && invalidTableIndex !== -1) {
invalidTable.parent?.children.splice(invalidTableIndex, 1, {
if (tableIndex !== undefined && tableIndex !== -1) {
processTableSpans(table.inner)

const hastTable = toHast(table.inner, {
allowDangerousHtml: options.allowDangerousHtml,
})

if (hastTable.type === 'element') {
const hastColGroup: hast.Element = {
type: 'element',
tagName: 'colgroup',
properties: {},
children:
table.inner.data?.colWidths?.map(width => ({
type: 'element',
tagName: 'col',
properties: {
width:
table.inner.data?.type === BlockType.GRID
? `${width.toFixed(2)}%`
: width,
},
children: [],
})) ?? [],
}

hastTable.children = ([hastColGroup] as hast.ElementContent[]).concat(
hastTable.children,
)
}

table.parent?.children.splice(tableIndex, 1, {
type: 'html',
value: toHtml(
toHast(
{
...invalidTable.inner,
children: invalidTable.inner.children.map(row => ({
...row,
children: row.children.map(cell => ({
...cell,
children: cell.data?.invalidChildren ?? cell.children,
})),
})),
} as mdast.Table,
{
allowDangerousHtml: options.allowDangerousHtml,
},
),
{
allowDangerousHtml: options.allowDangerousHtml,
},
),
value: toHtml(hastTable, {
allowDangerousHtml: options.allowDangerousHtml,
}),
})
}
})
Expand Down Expand Up @@ -245,36 +364,28 @@ export const transformMentionUsers = async (
}
}

export interface TransformTableWithParentsOptions {
transformGridToHtml: boolean
transformInvalidTablesToHtml: boolean
}

export const transformTableWithParents = (
tableWithParents: TableWithParent[],
options: TransformTableWithParentsOptions,
export const transformTableBySettings = (
tables: TableWithParent[],
settings: Pick<Settings, SettingKey.Table | SettingKey.Grid>,
): void => {
if (options.transformGridToHtml) {
if (settings[SettingKey.Grid] === GridSetting.ToHTML) {
transformGridToHtml(
tableWithParents.filter(item => item.inner.data?.type === BlockType.GRID),
tables.filter(item => item.inner.data?.type === BlockType.GRID),
{
allowDangerousHtml: true,
},
)
}

if (options.transformInvalidTablesToHtml) {
transformInvalidTablesToHtml(
tableWithParents.filter(
item =>
item.inner.data?.invalid &&
(options.transformGridToHtml
? item.inner.data.type !== BlockType.GRID
: true),
),
{
allowDangerousHtml: true,
},
)
}
transformTableToHtml(
mapTableBySettings(
settings[SettingKey.Grid] === GridSetting.ToHTML
? tables.filter(item => item.inner.data?.type !== BlockType.GRID)
: tables,
settings,
),
{
allowDangerousHtml: true,
},
)
}
Loading
Loading