feat: export to word

This commit is contained in:
kangfenmao 2024-11-02 21:01:11 +08:00
parent e33d9ac0ae
commit 04af940144
13 changed files with 478 additions and 20 deletions

View File

@ -39,12 +39,14 @@
"@electron-toolkit/preload": "^3.0.0",
"@electron-toolkit/utils": "^3.0.0",
"archiver": "^7.0.1",
"docx": "^9.0.2",
"electron-log": "^5.1.5",
"electron-store": "^8.2.0",
"electron-updater": "^6.3.9",
"electron-window-state": "^5.0.3",
"fs-extra": "^11.2.0",
"html2canvas": "^1.4.1",
"markdown-it": "^14.1.0",
"officeparser": "^4.1.1",
"sharp": "^0.33.5",
"unzipper": "^0.12.3",
@ -61,6 +63,7 @@
"@reduxjs/toolkit": "^2.2.5",
"@types/fs-extra": "^11",
"@types/lodash": "^4.17.5",
"@types/markdown-it": "^14",
"@types/node": "^18.19.9",
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",

View File

@ -5,17 +5,18 @@ import { BrowserWindow, ipcMain, session, shell } from 'electron'
import { appConfig, titleBarOverlayDark, titleBarOverlayLight } from './config'
import AppUpdater from './services/AppUpdater'
import BackupManager from './services/BackupManager'
import { ExportService } from './services/ExportService'
import FileManager from './services/FileManager'
import { compress, decompress } from './utils/zip'
import { createMinappWindow } from './window'
const fileManager = new FileManager()
const backupManager = new BackupManager()
const exportService = new ExportService(fileManager)
export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
const { autoUpdater } = new AppUpdater(mainWindow)
// IPC
ipcMain.handle('app:info', () => ({
version: app.getVersion(),
isPackaged: app.isPackaged,
@ -33,13 +34,17 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('reload', () => mainWindow.reload())
// zip
ipcMain.handle('zip:compress', (_, text: string) => compress(text))
ipcMain.handle('zip:decompress', (_, text: Buffer) => decompress(text))
// backup
ipcMain.handle('backup:backup', backupManager.backup)
ipcMain.handle('backup:restore', backupManager.restore)
ipcMain.handle('backup:backupToWebdav', backupManager.backupToWebdav)
ipcMain.handle('backup:restoreFromWebdav', backupManager.restoreFromWebdav)
// file
ipcMain.handle('file:open', fileManager.open)
ipcMain.handle('file:save', fileManager.save)
ipcMain.handle('file:select', fileManager.selectFile)
@ -54,7 +59,9 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
ipcMain.handle('file:saveImage', fileManager.saveImage)
ipcMain.handle('file:base64Image', fileManager.base64Image)
ipcMain.handle('file:download', fileManager.downloadFile)
ipcMain.handle('file:copy', fileManager.copyFile)
// minapp
ipcMain.handle('minapp', (_, args) => {
createMinappWindow({
url: args.url,
@ -66,6 +73,7 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
})
})
// theme
ipcMain.handle('set-theme', (_, theme: 'light' | 'dark') => {
appConfig.set('theme', theme)
mainWindow?.setTitleBarOverlay &&
@ -79,4 +87,6 @@ export function registerIpc(mainWindow: BrowserWindow, app: Electron.App) {
update: await autoUpdater.checkForUpdates()
}
})
ipcMain.handle('export:word', exportService.exportToWord)
}

View File

@ -0,0 +1,222 @@
/* eslint-disable no-case-declarations */
// ExportService
import { AlignmentType, BorderStyle, Document, HeadingLevel, Packer, Paragraph, ShadingType, TextRun } from 'docx'
import { dialog } from 'electron'
import Logger from 'electron-log'
import MarkdownIt from 'markdown-it'
import FileManager from './FileManager'
export class ExportService {
private fileManager: FileManager
private md: MarkdownIt
constructor(fileManager: FileManager) {
this.fileManager = fileManager
this.md = new MarkdownIt()
}
private convertMarkdownToDocxElements(markdown: string) {
const tokens = this.md.parse(markdown, {})
const elements: any[] = []
let listLevel = 0
const processInlineTokens = (tokens: any[]): TextRun[] => {
const runs: TextRun[] = []
for (const token of tokens) {
switch (token.type) {
case 'text':
runs.push(new TextRun(token.content))
break
case 'strong':
runs.push(new TextRun({ text: token.content, bold: true }))
break
case 'em':
runs.push(new TextRun({ text: token.content, italics: true }))
break
case 'code_inline':
runs.push(new TextRun({ text: token.content, font: 'Consolas', size: 20 }))
break
}
}
return runs
}
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
switch (token.type) {
case 'heading_open':
// 获取标题级别 (h1 -> h6)
const level = parseInt(token.tag.slice(1)) as 1 | 2 | 3 | 4 | 5 | 6
const headingText = tokens[i + 1].content
elements.push(
new Paragraph({
text: headingText,
heading: HeadingLevel[`HEADING_${level}`],
spacing: {
before: 240,
after: 120
}
})
)
i += 2 // 跳过内容标记和闭合标记
break
case 'paragraph_open':
const inlineTokens = tokens[i + 1].children || []
elements.push(
new Paragraph({
children: processInlineTokens(inlineTokens),
spacing: {
before: 120,
after: 120
}
})
)
i += 2
break
case 'bullet_list_open':
listLevel++
break
case 'bullet_list_close':
listLevel--
break
case 'list_item_open':
const itemInlineTokens = tokens[i + 2].children || []
elements.push(
new Paragraph({
children: [
new TextRun({ text: '•', bold: true }),
new TextRun({ text: '\t' }),
...processInlineTokens(itemInlineTokens)
],
indent: {
left: listLevel * 720
}
})
)
i += 3
break
case 'fence': // 代码块
const codeLines = token.content.split('\n')
elements.push(
new Paragraph({
children: codeLines.map(
(line) =>
new TextRun({
text: line + '\n',
font: 'Consolas',
size: 20,
break: 1
})
),
shading: {
type: ShadingType.SOLID,
color: 'F5F5F5'
},
spacing: {
before: 120,
after: 120
},
border: {
top: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
bottom: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
left: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' },
right: { style: BorderStyle.SINGLE, size: 1, color: 'DDDDDD' }
}
})
)
break
case 'hr':
elements.push(
new Paragraph({
children: [new TextRun({ text: '─'.repeat(50), color: '999999' })],
alignment: AlignmentType.CENTER
})
)
break
case 'blockquote_open':
const quoteText = tokens[i + 2].content
elements.push(
new Paragraph({
children: [
new TextRun({
text: quoteText,
italics: true
})
],
indent: {
left: 720
},
border: {
left: {
style: BorderStyle.SINGLE,
size: 3,
color: 'CCCCCC'
}
},
spacing: {
before: 120,
after: 120
}
})
)
i += 3
break
}
}
return elements
}
public exportToWord = async (_: Electron.IpcMainInvokeEvent, markdown: string, fileName: string): Promise<void> => {
try {
const elements = this.convertMarkdownToDocxElements(markdown)
const doc = new Document({
styles: {
paragraphStyles: [
{
id: 'Normal',
name: 'Normal',
run: {
size: 24,
font: 'Arial'
}
}
]
},
sections: [
{
properties: {},
children: elements
}
]
})
const buffer = await Packer.toBuffer(doc)
const filePath = dialog.showSaveDialogSync({
title: '保存文件',
filters: [{ name: 'Word Document', extensions: ['docx'] }],
defaultPath: fileName
})
if (filePath) {
await this.fileManager.writeFile(_, filePath, buffer)
Logger.info('[ExportService] Document exported successfully')
}
} catch (error) {
Logger.error('[ExportService] Export to Word failed:', error)
throw error
}
}
}

View File

@ -301,7 +301,7 @@ class FileManager {
fileName: string,
content: string,
options?: SaveDialogOptions
): Promise<void> => {
): Promise<string | null> => {
try {
const result: SaveDialogReturnValue = await dialog.showSaveDialog({
title: '保存文件',
@ -312,8 +312,11 @@ class FileManager {
if (!result.canceled && result.filePath) {
await writeFileSync(result.filePath, content, { encoding: 'utf-8' })
}
return result.filePath
} catch (err) {
logger.error('[IPC - Error]', 'An error occurred saving the file:', err)
return null
}
}
@ -431,6 +434,25 @@ class FileManager {
return mimeToExtension[mimeType] || '.bin'
}
public copyFile = async (_: Electron.IpcMainInvokeEvent, id: string, destPath: string): Promise<void> => {
try {
const sourcePath = path.join(this.storageDir, id)
// 确保目标目录存在
const destDir = path.dirname(destPath)
if (!fs.existsSync(destDir)) {
await fs.promises.mkdir(destDir, { recursive: true })
}
// 复制文件
await fs.promises.copyFile(sourcePath, destPath)
logger.info('[FileManager] File copied successfully:', { from: sourcePath, to: destPath })
} catch (error) {
logger.error('[FileManager] Copy file failed:', error)
throw error
}
}
}
export default FileManager

View File

@ -39,10 +39,18 @@ declare global {
create: (fileName: string) => Promise<string>
write: (filePath: string, data: Uint8Array | string) => Promise<void>
open: (options?: OpenDialogOptions) => Promise<{ fileName: string; filePath: string; content: Buffer } | null>
save: (path: string, content: string | NodeJS.ArrayBufferView, options?: SaveDialogOptions) => void
save: (
path: string,
content: string | NodeJS.ArrayBufferView,
options?: SaveDialogOptions
) => Promise<string | null>
saveImage: (name: string, data: string) => void
base64Image: (fileId: string) => Promise<{ mime: string; base64: string; data: string }>
download: (url: string) => Promise<FileType | null>
copy: (fileId: string, destPath: string) => Promise<void>
}
export: {
toWord: (markdown: string, fileName: string) => Promise<void>
}
}
}

View File

@ -36,7 +36,11 @@ const api = {
selectFolder: () => ipcRenderer.invoke('file:selectFolder'),
saveImage: (name: string, data: string) => ipcRenderer.invoke('file:saveImage', name, data),
base64Image: (fileId: string) => ipcRenderer.invoke('file:base64Image', fileId),
download: (url: string) => ipcRenderer.invoke('file:download', url)
download: (url: string) => ipcRenderer.invoke('file:download', url),
copy: (fileId: string, destPath: string) => ipcRenderer.invoke('file:copy', fileId, destPath)
},
export: {
toWord: (markdown: string, fileName: string) => ipcRenderer.invoke('export:word', markdown, fileName)
}
}

View File

@ -168,6 +168,7 @@ body,
body[os='mac'] {
#content-container {
border-top-left-radius: 10px;
border-bottom-left-radius: 10px;
border-left: 0.5px solid var(--color-border);
box-shadow: 0 0 15px 1px rgba(0, 0, 0, 0.05);
}

View File

@ -84,6 +84,7 @@
"topics.export.title": "Export",
"topics.export.image": "Export as image",
"topics.export.md": "Export as markdown",
"topics.export.word": "Export as Word",
"input.new_topic": "New Topic",
"input.topics": " Topics ",
"input.clear": "Clear",
@ -387,6 +388,16 @@
"words": {
"knowledgeGraph": "Knowledge Graph",
"visualization": "Visualization"
},
"export": {
"attached_files": "Attached Files",
"user": "User",
"assistant": "Assistant",
"created": "Created",
"last_updated": "Last Updated",
"messages": "Messages",
"conversation_details": "Conversation Details",
"conversation_history": "Conversation History"
}
}
}

View File

@ -84,6 +84,7 @@
"topics.export.title": "导出",
"topics.export.image": "导出为图片",
"topics.export.md": "导出为 Markdown",
"topics.export.word": "导出为 Word",
"input.new_topic": "新话题",
"input.topics": " 话题 ",
"input.clear": "清空消息",
@ -387,6 +388,16 @@
"words": {
"knowledgeGraph": "知识图谱",
"visualization": "可视化"
},
"export": {
"attached_files": "附件",
"user": "用户",
"assistant": "助手",
"created": "创建时间",
"last_updated": "最后更新",
"messages": "消息数",
"conversation_details": "会话详情",
"conversation_history": "会话历史"
}
}
}

View File

@ -84,6 +84,7 @@
"topics.export.title": "匯出",
"topics.export.image": "匯出為圖片",
"topics.export.md": "匯出為 Markdown",
"topics.export.word": "導出為 Word",
"input.new_topic": "新話題",
"input.topics": " 話題 ",
"input.clear": "清除",
@ -387,6 +388,16 @@
"words": {
"knowledgeGraph": "知識圖譜",
"visualization": "可視化"
},
"export": {
"attached_files": "附件",
"user": "用戶",
"assistant": "助手",
"created": "創建時間",
"last_updated": "最後更新",
"messages": "訊息數",
"conversation_details": "會話詳情",
"conversation_history": "會話歷史"
}
}
}

View File

@ -17,7 +17,7 @@ import { EVENT_NAMES, EventEmitter } from '@renderer/services/EventService'
import store, { useAppSelector } from '@renderer/store'
import { setGenerating } from '@renderer/store/runtime'
import { Assistant, Topic } from '@renderer/types'
import { exportTopicAsMarkdown } from '@renderer/utils/export'
import { exportTopicAsMarkdown, topicToMarkdown } from '@renderer/utils/export'
import { Dropdown, MenuProps } from 'antd'
import dayjs from 'dayjs'
import { findIndex } from 'lodash'
@ -141,6 +141,14 @@ const Topics: FC<Props> = ({ assistant: _assistant, activeTopic, setActiveTopic
label: t('chat.topics.export.md'),
key: 'markdown',
onClick: () => exportTopicAsMarkdown(topic)
},
{
label: t('chat.topics.export.word'),
key: 'word',
onClick: async () => {
const markdown = await topicToMarkdown(topic)
window.api.export.toWord(markdown, topic.name)
}
}
]
}

View File

@ -1,24 +1,31 @@
import db from '@renderer/databases'
import { Message, Topic } from '@renderer/types'
export const exportMessageAsMarkdown = (message: Message) => {
if (message.role === 'user') {
return `### User\n\n${message.content}`
}
export const messageToMarkdown = (message: Message) => {
const roleText = message.role === 'user' ? '🧑‍💻 User' : '🤖 Assistant'
const titleSection = `### ${roleText}`
const contentSection = message.content
return `### Assistant\n\n${message.content}`
return [titleSection, '', contentSection].join('\n')
}
export const exportMessagesAsMarkdown = (messages: Message[]) => {
return messages.map((message) => exportMessageAsMarkdown(message)).join('\n\n---\n\n')
export const messagesToMarkdown = (messages: Message[]) => {
return messages.map((message) => messageToMarkdown(message)).join('\n\n---\n\n')
}
export const topicToMarkdown = async (topic: Topic) => {
const topicName = `# ${topic.name}`
const topicMessages = await db.topics.get(topic.id)
if (topicMessages) {
return topicName + '\n\n' + messagesToMarkdown(topicMessages.messages)
}
return ''
}
export const exportTopicAsMarkdown = async (topic: Topic) => {
const fileName = topic.name + '.md'
const topicMessages = await db.topics.get(topic.id)
if (topicMessages) {
const title = '# ' + topic.name + `\n\n`
const markdown = exportMessagesAsMarkdown(topicMessages.messages)
window.api.file.save(fileName, title + markdown)
}
const markdown = await topicToMarkdown(topic)
window.api.file.save(fileName, markdown)
}

144
yarn.lock
View File

@ -2094,6 +2094,13 @@ __metadata:
languageName: node
linkType: hard
"@types/linkify-it@npm:^5":
version: 5.0.0
resolution: "@types/linkify-it@npm:5.0.0"
checksum: 10c0/7bbbf45b9dde17bf3f184fee585aef0e7342f6954f0377a24e4ff42ab5a85d5b806aaa5c8d16e2faf2a6b87b2d94467a196b7d2b85c9c7de2f0eaac5487aaab8
languageName: node
linkType: hard
"@types/lodash@npm:^4.17.5":
version: 4.17.7
resolution: "@types/lodash@npm:4.17.7"
@ -2101,6 +2108,16 @@ __metadata:
languageName: node
linkType: hard
"@types/markdown-it@npm:^14":
version: 14.1.2
resolution: "@types/markdown-it@npm:14.1.2"
dependencies:
"@types/linkify-it": "npm:^5"
"@types/mdurl": "npm:^2"
checksum: 10c0/34f709f0476bd4e7b2ba7c3341072a6d532f1f4cb6f70aef371e403af8a08a7c372ba6907ac426bc618d356dab660c5b872791ff6c1ead80c483e0d639c6f127
languageName: node
linkType: hard
"@types/mathjax@npm:^0.0.40":
version: 0.0.40
resolution: "@types/mathjax@npm:0.0.40"
@ -2117,6 +2134,13 @@ __metadata:
languageName: node
linkType: hard
"@types/mdurl@npm:^2":
version: 2.0.0
resolution: "@types/mdurl@npm:2.0.0"
checksum: 10c0/cde7bb571630ed1ceb3b92a28f7b59890bb38b8f34cd35326e2df43eebfc74985e6aa6fd4184e307393bad8a9e0783a519a3f9d13c8e03788c0f98e5ec869c5e
languageName: node
linkType: hard
"@types/minimatch@npm:*":
version: 5.1.2
resolution: "@types/minimatch@npm:5.1.2"
@ -2166,6 +2190,15 @@ __metadata:
languageName: node
linkType: hard
"@types/node@npm:^22.7.5":
version: 22.8.6
resolution: "@types/node@npm:22.8.6"
dependencies:
undici-types: "npm:~6.19.8"
checksum: 10c0/d3a11f2549234a91a4c5d0ff35ab4bdcb7ba34db4d3f1d189be39b8bd41c19aac98d117150a95a9c5a9d45b1014135477ea240b2b8317c86ae3d3cf1c3b3f8f4
languageName: node
linkType: hard
"@types/plist@npm:^3.0.1":
version: 3.0.5
resolution: "@types/plist@npm:3.0.5"
@ -2453,6 +2486,7 @@ __metadata:
"@reduxjs/toolkit": "npm:^2.2.5"
"@types/fs-extra": "npm:^11"
"@types/lodash": "npm:^4.17.5"
"@types/markdown-it": "npm:^14"
"@types/node": "npm:^18.19.9"
"@types/react": "npm:^18.2.48"
"@types/react-dom": "npm:^18.2.18"
@ -2466,6 +2500,7 @@ __metadata:
dayjs: "npm:^1.11.11"
dexie: "npm:^4.0.8"
dexie-react-hooks: "npm:^1.1.7"
docx: "npm:^9.0.2"
dotenv-cli: "npm:^7.4.2"
electron: "npm:^28.3.3"
electron-builder: "npm:^24.9.1"
@ -2489,6 +2524,7 @@ __metadata:
i18next: "npm:^23.11.5"
localforage: "npm:^1.10.0"
lodash: "npm:^4.17.21"
markdown-it: "npm:^14.1.0"
mime: "npm:^4.0.4"
officeparser: "npm:^4.1.1"
openai: "npm:^4.52.1"
@ -4464,6 +4500,20 @@ __metadata:
languageName: node
linkType: hard
"docx@npm:^9.0.2":
version: 9.0.2
resolution: "docx@npm:9.0.2"
dependencies:
"@types/node": "npm:^22.7.5"
hash.js: "npm:^1.1.7"
jszip: "npm:^3.10.1"
nanoid: "npm:^5.0.4"
xml: "npm:^1.0.1"
xml-js: "npm:^1.6.8"
checksum: 10c0/6065391bac9384084dd74f8ff1fb04ac781e8d10d0a0174bb62dc338e75130fac57f6413ff3e6b0f264d61850baddcccbfa7994be036c6fbc113127c4422d342
languageName: node
linkType: hard
"dom-walk@npm:^0.1.0":
version: 0.1.2
resolution: "dom-walk@npm:0.1.2"
@ -6111,6 +6161,16 @@ __metadata:
languageName: node
linkType: hard
"hash.js@npm:^1.1.7":
version: 1.1.7
resolution: "hash.js@npm:1.1.7"
dependencies:
inherits: "npm:^2.0.3"
minimalistic-assert: "npm:^1.0.1"
checksum: 10c0/41ada59494eac5332cfc1ce6b7ebdd7b88a3864a6d6b08a3ea8ef261332ed60f37f10877e0c825aaa4bddebf164fbffa618286aeeec5296675e2671cbfa746c4
languageName: node
linkType: hard
"hasha@npm:^2.2.0":
version: 2.2.0
resolution: "hasha@npm:2.2.0"
@ -7410,7 +7470,7 @@ __metadata:
languageName: node
linkType: hard
"jszip@npm:^3.1.0":
"jszip@npm:^3.1.0, jszip@npm:^3.10.1":
version: 3.10.1
resolution: "jszip@npm:3.10.1"
dependencies:
@ -7528,6 +7588,15 @@ __metadata:
languageName: node
linkType: hard
"linkify-it@npm:^5.0.0":
version: 5.0.0
resolution: "linkify-it@npm:5.0.0"
dependencies:
uc.micro: "npm:^2.0.0"
checksum: 10c0/ff4abbcdfa2003472fc3eb4b8e60905ec97718e11e33cca52059919a4c80cc0e0c2a14d23e23d8c00e5402bc5a885cdba8ca053a11483ab3cc8b3c7a52f88e2d
languageName: node
linkType: hard
"load-bmfont@npm:^1.3.1, load-bmfont@npm:^1.4.0":
version: 1.4.2
resolution: "load-bmfont@npm:1.4.2"
@ -7735,6 +7804,22 @@ __metadata:
languageName: node
linkType: hard
"markdown-it@npm:^14.1.0":
version: 14.1.0
resolution: "markdown-it@npm:14.1.0"
dependencies:
argparse: "npm:^2.0.1"
entities: "npm:^4.4.0"
linkify-it: "npm:^5.0.0"
mdurl: "npm:^2.0.0"
punycode.js: "npm:^2.3.1"
uc.micro: "npm:^2.1.0"
bin:
markdown-it: bin/markdown-it.mjs
checksum: 10c0/9a6bb444181d2db7016a4173ae56a95a62c84d4cbfb6916a399b11d3e6581bf1cc2e4e1d07a2f022ae72c25f56db90fbe1e529fca16fbf9541659dc53480d4b4
languageName: node
linkType: hard
"markdown-table@npm:^3.0.0":
version: 3.0.3
resolution: "markdown-table@npm:3.0.3"
@ -8005,6 +8090,13 @@ __metadata:
languageName: node
linkType: hard
"mdurl@npm:^2.0.0":
version: 2.0.0
resolution: "mdurl@npm:2.0.0"
checksum: 10c0/633db522272f75ce4788440669137c77540d74a83e9015666a9557a152c02e245b192edc20bc90ae953bbab727503994a53b236b4d9c99bdaee594d0e7dd2ce0
languageName: node
linkType: hard
"memoize-one@npm:^6.0.0":
version: 6.0.0
resolution: "memoize-one@npm:6.0.0"
@ -8460,6 +8552,13 @@ __metadata:
languageName: node
linkType: hard
"minimalistic-assert@npm:^1.0.1":
version: 1.0.1
resolution: "minimalistic-assert@npm:1.0.1"
checksum: 10c0/96730e5601cd31457f81a296f521eb56036e6f69133c0b18c13fe941109d53ad23a4204d946a0d638d7f3099482a0cec8c9bb6d642604612ce43ee536be3dddd
languageName: node
linkType: hard
"minimatch@npm:9.0.3":
version: 9.0.3
resolution: "minimatch@npm:9.0.3"
@ -8675,6 +8774,15 @@ __metadata:
languageName: node
linkType: hard
"nanoid@npm:^5.0.4":
version: 5.0.8
resolution: "nanoid@npm:5.0.8"
bin:
nanoid: bin/nanoid.js
checksum: 10c0/0281d3cc0f3d03fb3010b479f28e8ddedfafb9b5d60e54315589ef0a54a0e9cfcb0bf382dd3b620fdad6b72b8c1f5b89a15d55c6b6a9134b58b16eb230b3a3d7
languageName: node
linkType: hard
"napi-build-utils@npm:^1.0.1":
version: 1.0.2
resolution: "napi-build-utils@npm:1.0.2"
@ -9648,6 +9756,13 @@ __metadata:
languageName: node
linkType: hard
"punycode.js@npm:^2.3.1":
version: 2.3.1
resolution: "punycode.js@npm:2.3.1"
checksum: 10c0/1d12c1c0e06127fa5db56bd7fdf698daf9a78104456a6b67326877afc21feaa821257b171539caedd2f0524027fa38e67b13dd094159c8d70b6d26d2bea4dfdb
languageName: node
linkType: hard
"punycode@npm:^2.1.0, punycode@npm:^2.1.1, punycode@npm:^2.3.1":
version: 2.3.1
resolution: "punycode@npm:2.3.1"
@ -12333,6 +12448,13 @@ __metadata:
languageName: node
linkType: hard
"uc.micro@npm:^2.0.0, uc.micro@npm:^2.1.0":
version: 2.1.0
resolution: "uc.micro@npm:2.1.0"
checksum: 10c0/8862eddb412dda76f15db8ad1c640ccc2f47cdf8252a4a30be908d535602c8d33f9855dfcccb8b8837855c1ce1eaa563f7fa7ebe3c98fd0794351aab9b9c55fa
languageName: node
linkType: hard
"unbox-primitive@npm:^1.0.2":
version: 1.0.2
resolution: "unbox-primitive@npm:1.0.2"
@ -12362,7 +12484,7 @@ __metadata:
languageName: node
linkType: hard
"undici-types@npm:~6.19.2":
"undici-types@npm:~6.19.2, undici-types@npm:~6.19.8":
version: 6.19.8
resolution: "undici-types@npm:6.19.8"
checksum: 10c0/078afa5990fba110f6824823ace86073b4638f1d5112ee26e790155f481f2a868cc3e0615505b6f4282bdf74a3d8caad715fd809e870c2bb0704e3ea6082f344
@ -13057,6 +13179,17 @@ __metadata:
languageName: node
linkType: hard
"xml-js@npm:^1.6.8":
version: 1.6.11
resolution: "xml-js@npm:1.6.11"
dependencies:
sax: "npm:^1.2.4"
bin:
xml-js: ./bin/cli.js
checksum: 10c0/c83631057f10bf90ea785cee434a8a1a0030c7314fe737ad9bf568a281083b565b28b14c9e9ba82f11fc9dc582a3a907904956af60beb725be1c9ad4b030bc5a
languageName: node
linkType: hard
"xml-name-validator@npm:^5.0.0":
version: 5.0.0
resolution: "xml-name-validator@npm:5.0.0"
@ -13081,6 +13214,13 @@ __metadata:
languageName: node
linkType: hard
"xml@npm:^1.0.1":
version: 1.0.1
resolution: "xml@npm:1.0.1"
checksum: 10c0/04bcc9b8b5e7b49392072fbd9c6b0f0958bd8e8f8606fee460318e43991349a68cbc5384038d179ff15aef7d222285f69ca0f067f53d071084eb14c7fdb30411
languageName: node
linkType: hard
"xmlbuilder@npm:>=11.0.1, xmlbuilder@npm:^15.1.1":
version: 15.1.1
resolution: "xmlbuilder@npm:15.1.1"