Skip to content

Add plugin md表格互转 v1.0.0#272

Open
7788jay wants to merge 3 commits into
ZToolsCenter:mainfrom
7788jay:plugin/md-table-convert
Open

Add plugin md表格互转 v1.0.0#272
7788jay wants to merge 3 commits into
ZToolsCenter:mainfrom
7788jay:plugin/md-table-convert

Conversation

@7788jay

@7788jay 7788jay commented Jun 25, 2026

Copy link
Copy Markdown

插件信息

  • 名称: md表格互转
  • 插件ID: md-table-convert
  • 版本: 1.0.0
  • 描述: Markdown 表格与 Excel 表格双向转换
  • 作者: 7788jay
  • 类型: 新增

本次变更

  • Initial commit

截图 / 演示

image image

自检清单

  • [ x ] plugin.json 的 name / title / version / description / author 字段均已检查
  • [ x ] 已移除调试日志、未使用文件、敏感信息(.env、token、密钥等)
  • [ x ] 本次 PR 的 diff 仅涉及 plugins/md-table-convert/ 目录
  • [ x ] 已在本地 ZTools 客户端实际加载并测试过此插件,主要功能正常
  • [ x ] 同意以仓库声明的开源协议发布此插件

此 PR 由 ztools-plugin-cli 自动管理:每次 ztools publish 在分支上追加一个 commit,PR 链接保持不变。

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new ZTools plugin, 'md-table-convert', built with Vue 3, Vite, and TypeScript to support bidirectional conversion between Markdown tables and Excel-compatible TSV format. Feedback on the implementation highlights several critical issues: the automatic downward filling of empty cells in TSV parsing can corrupt unmerged empty cells; cell parsing in Markdown tables fails to handle escaped pipes; TSV generation and Markdown conversion lack proper escaping for special characters (like pipes, tabs, newlines, and quotes); and redundant mapping operations exist in the Vue templates. Additionally, committing local configuration files with absolute paths poses a repository cleanliness issue, and exposing unused Node.js file system capabilities in the preload script introduces unnecessary security risks.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +88 to +101
// ---- 2. 合并单元格填充:空单元格向下填充 ----
const colCount = Math.max(...rows.map((r) => r.length))
for (let col = 0; col < colCount; col++) {
let lastValue = ''
for (let row = 0; row < rows.length; row++) {
while (rows[row].length < colCount) rows[row].push('')
const cell = rows[row][col]
if (cell !== '') {
lastValue = cell
} else if (lastValue !== '') {
rows[row][col] = lastValue
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

在解析 Excel 复制的内容时,将所有空单元格向下填充(rows[row][col] = lastValue)会导致数据错误。如果表格中原本就存在合法的空单元格(未合并),这种逻辑会错误地用上一行的值覆盖它,造成数据损坏。建议移除自动填充逻辑,仅进行列数补齐。

  // ---- 2. 补齐列数 ----
  const colCount = Math.max(...rows.map((r) => r.length))
  for (let row = 0; row < rows.length; row++) {
    while (rows[row].length < colCount) {
      rows[row].push('')
    }
  }

Comment on lines +34 to +41
// 解析单行单元格
const parseCells = (line: string): string[] => {
// 去掉首尾可能多余的 |
let trimmed = line
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1)
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1)
return trimmed.split('|').map((cell) => cell.trim())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

使用 split('|') 解析单元格时,如果单元格内容中包含转义的竖线(例如 \|),会被错误地分割成多个单元格。建议使用负向后行断言(Negative Lookbehind)来避免分割转义的竖线,并在解析后将 \| 还原为 |

Suggested change
// 解析单行单元格
const parseCells = (line: string): string[] => {
// 去掉首尾可能多余的 |
let trimmed = line
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1)
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1)
return trimmed.split('|').map((cell) => cell.trim())
}
// 解析单行单元格
const parseCells = (line: string): string[] => {
// 去掉首尾可能多余的 |
let trimmed = line
if (trimmed.startsWith('|')) trimmed = trimmed.slice(1)
if (trimmed.endsWith('|')) trimmed = trimmed.slice(0, -1)
return trimmed.split(/(?<!\\)\|/).map((cell) => cell.trim().replace(/\\\|/g, '|'))
}

Comment on lines +18 to +29
const markdownText = computed(() => {
if (!tableData.value) return ''
const { headers, rows } = tableData.value
const lines: string[] = []
lines.push('| ' + headers.join(' | ') + ' |')
lines.push('| ' + headers.map(() => '---').join(' | ') + ' |')
for (const row of rows) {
const cells = headers.map((_, i) => (i < row.length ? row[i] : ''))
lines.push('| ' + cells.join(' | ') + ' |')
}
return lines.join('\n')
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在将 Excel 数据转换为 Markdown 时,如果单元格内容本身包含竖线 |,直接拼接会破坏 Markdown 表格的结构。建议在拼接前对竖线进行转义(替换为 \|)。

const markdownText = computed(() => {
  if (!tableData.value) return ''
  const { headers, rows } = tableData.value
  const escapePipe = (str: string) => str.replace(/\|/g, '\\|')
  const lines: string[] = []
  lines.push('| ' + headers.map(escapePipe).join(' | ') + ' |')
  lines.push('| ' + headers.map(() => '---').join(' | ') + ' |')
  for (const row of rows) {
    const cells = headers.map((_, i) => (i < row.length ? escapePipe(row[i]) : ''))
    lines.push('| ' + cells.join(' | ') + ' |')
  }
  return lines.join('\n')
})

</thead>
<tbody>
<tr v-for="(row, ri) in tableData.rows" :key="'r-' + ri">
<td v-for="(cell, ci) in tableData.headers.map((_, idx) => (idx < row.length ? row[idx] : ''))" :key="'c-' + ri + '-' + ci">{{ cell }}</td>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

由于在 parseExcelPaste 中已经对所有行进行了列数补齐(填充为空字符串),因此 row 的长度与 headers 的长度是完全一致的。这里在 v-for 中进行复杂的 .map 转换是多余的,且在每次渲染时都会重新创建数组,影响性能。可以直接遍历 row

              <td v-for="(cell, ci) in row" :key="'c-' + ri + '-' + ci">{{ cell }}</td>

Comment on lines +41 to +45
/** 将表格数据转为 TSV 文本(可直接粘贴到 Excel) */
function tableToTSV(data: TableData): string {
const allRows = [data.headers, ...data.rows]
return allRows.map((row) => row.join('\t')).join('\n')
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

在生成 TSV 文本时,如果单元格内容包含制表符 \t、换行符 \n 或双引号 ",直接用 join('\t') 拼接会导致粘贴到 Excel 时格式错乱。建议按照 TSV 标准对这些特殊字符进行双引号包裹和转义处理。

/** 将表格数据转为 TSV 文本(可直接粘贴到 Excel) */
function tableToTSV(data: TableData): string {
  const escapeTSV = (val: string) => {
    if (val.includes('\t') || val.includes('\n') || val.includes('"') || val.includes('\r')) {
      return '"' + val.replace(/"/g, '""') + '"'
    }
    return val
  }
  const allRows = [data.headers, ...data.rows]
  return allRows.map((row) => row.map(escapeTSV).join('\t')).join('\n')
}

Comment on lines +1 to +15
{
"permissions": {
"allow": [
"Bash(npx vue-tsc *)",
"Bash(where node *)",
"Bash(where npx *)",
"Bash(winget install *)",
"Bash(mkdir -p \"E:/nodejs/node_cache\" 2>&1)",
"Bash(cmd //c dir \"E:\\\\\" 2>&1)"
],
"additionalDirectories": [
"E:\\nodejs"
]
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

.claude/settings.local.json 是本地 AI 助手(如 Claude Code)生成的本地配置文件,其中包含特定于你个人电脑的绝对路径(如 E:\\nodejs)。这类本地配置文件不应该提交到公共代码仓库中。建议将其从 Git 中移除,并添加到 .gitignore 中。

Comment on lines +5 to +27
window.services = {
// 读文件
readFile(file) {
return fs.readFileSync(file, { encoding: 'utf-8' })
},
// 文本写入到下载目录
writeTextFile(text) {
const filePath = path.join(window.ztools.getPath('downloads'), Date.now().toString() + '.txt')
fs.writeFileSync(filePath, text, { encoding: 'utf-8' })
return filePath
},
// 图片写入到下载目录
writeImageFile(base64Url) {
const matchs = /^data:image\/([a-z]{1,20});base64,/i.exec(base64Url)
if (!matchs) return
const filePath = path.join(
window.ztools.getPath('downloads'),
Date.now().toString() + '.' + matchs[1]
)
fs.writeFileSync(filePath, base64Url.substring(matchs[0].length), { encoding: 'base64' })
return filePath
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

services.js 中定义的 readFilewriteTextFilewriteImageFile 方法在当前插件的任何 Vue 组件中均未被调用。暴露未使用的 Node.js 文件系统读写能力会增加不必要的安全风险。如果该插件不需要任何 Node.js 底层能力,建议删除 public/preload/ 目录,并在 plugin.json 中移除 "preload" 配置。

@7788jay 7788jay marked this pull request as ready for review June 25, 2026 09:43
7788jay added 2 commits June 25, 2026 19:24
- Initial commit
- Enhance Markdown table generation by cleaning cell content to remove line breaks
- Initial commit
- Enhance Markdown table generation by cleaning cell content to remove line breaks
- Remove settings.local.json and update .gitignore to include .claude directory
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant