merge-excalidrawlib.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { readFile, writeFile, readdir } from 'fs/promises';
  2. import path from 'path';
  3. import { fileURLToPath } from 'url';
  4. import { nanoid } from 'nanoid'
  5. import { LibraryItem_v1, LibraryItem, LibraryItems } from '@excalidraw/excalidraw/types'
  6. // 合并素材库
  7. // 由于官方不支持多素材库文件,只能将其合并成一个文件
  8. type LibraryItems_v1 = readonly LibraryItem_v1[];
  9. interface ImportedLibraryData extends Partial<ExportedLibraryData> {
  10. /** @compatible v1 */
  11. library?: LibraryItems;
  12. }
  13. interface ExportedLibraryData {
  14. type: string;
  15. version: number;
  16. source: string;
  17. libraryItems: LibraryItems;
  18. }
  19. /**
  20. * checks if library item does not exist already in current library
  21. */
  22. const isUniqueItem = (
  23. existingLibraryItems: LibraryItems,
  24. targetLibraryItem: LibraryItem,
  25. ) => {
  26. return !existingLibraryItems.find((libraryItem) => {
  27. if (libraryItem.elements.length !== targetLibraryItem.elements.length) {
  28. return false;
  29. }
  30. // detect z-index difference by checking the excalidraw elements
  31. // are in order
  32. return libraryItem.elements.every((libItemExcalidrawItem, idx) => {
  33. return (
  34. libItemExcalidrawItem.id === targetLibraryItem.elements[idx].id &&
  35. libItemExcalidrawItem.versionNonce ===
  36. targetLibraryItem.elements[idx].versionNonce
  37. );
  38. });
  39. });
  40. };
  41. const isUniqueItemV1 = (
  42. existingLibraryItems: LibraryItems,
  43. targetLibraryItem: LibraryItem_v1,
  44. ) => {
  45. return !existingLibraryItems.find((libraryItem) => {
  46. if (libraryItem.elements.length !== targetLibraryItem.length) {
  47. return false;
  48. }
  49. // detect z-index difference by checking the excalidraw elements
  50. // are in order
  51. return libraryItem.elements.every((libItemExcalidrawItem, idx) => {
  52. return (
  53. libItemExcalidrawItem.id === targetLibraryItem[idx].id &&
  54. libItemExcalidrawItem.versionNonce ===
  55. targetLibraryItem[idx].versionNonce
  56. );
  57. });
  58. });
  59. };
  60. /** Merges otherItems into localItems. Unique items in otherItems array are
  61. sorted first. */
  62. const mergeLibraryItems = (
  63. localItems: LibraryItems,
  64. otherItems: LibraryItems | LibraryItems_v1,
  65. version: number
  66. ): LibraryItems => {
  67. const newItems: LibraryItem[] = [];
  68. otherItems.map(item => {
  69. if (version === 1 && isUniqueItemV1(localItems, item)) {
  70. newItems.push({
  71. status: "published",
  72. elements: [...item],
  73. id: nanoid(),
  74. created: Date.now()
  75. });
  76. } else if (isUniqueItem(localItems, item)) {
  77. newItems.push({...item});
  78. }
  79. })
  80. return [...newItems, ...localItems];
  81. };
  82. const __filename = fileURLToPath(import.meta.url);
  83. const __dirname = path.dirname(__filename);
  84. const inputDir = path.resolve(__dirname, '../lib'); // 输入目录
  85. const outputFile = path.resolve(__dirname, '../library.excalidrawlib'); // 输出文件路径
  86. function isExcalidrawlibFile(fileName: string): boolean {
  87. return fileName.endsWith('.excalidrawlib');
  88. }
  89. function positionError(obj: any, msg?: string): false {
  90. console.error({
  91. error_msg: msg,
  92. error_obj: obj
  93. })
  94. return false
  95. }
  96. function isLibraryItem(obj: any): obj is LibraryItem {
  97. if (!obj) return positionError(obj, "LibraryItem obj does not exist")
  98. if (!obj.id || !obj.status || !obj.elements || !obj.created) return positionError(obj, "obj missing fields")
  99. if (typeof obj.id !== 'string') return positionError(obj, "obj.id type is not string")
  100. if (obj.status !== "published" && obj.status !== "unpublished") return positionError(obj, "obj.status value is wrong")
  101. if (!Array.isArray(obj.elements)) return positionError(obj, "obj.elements is not an array")
  102. if (typeof obj.created !== 'number') return positionError(obj, "obj.created type is not number")
  103. return true
  104. }
  105. function isLibrary(obj: any): obj is LibraryItem {
  106. if (!obj) return positionError(obj, "Library obj does not exist")
  107. if (!Array.isArray(obj)) return positionError(obj, "obj.elements is not an array")
  108. return true
  109. }
  110. function isLibraryItems(obj: any): obj is LibraryItems {
  111. if (!obj) return positionError(obj, "LibraryItems obj does not exist")
  112. if (!Array.isArray(obj)) return positionError(obj, "obj is not an array")
  113. for (const item of obj) {
  114. if (!isLibraryItem(item)) return positionError(obj, "LibraryItems is not an array")
  115. }
  116. return true
  117. }
  118. function isImportedLibraryData(obj: any): obj is ImportedLibraryData {
  119. if (!obj) return positionError(obj, "ImportedLibraryData obj does not exist")
  120. if (obj.type !== "excalidrawlib") return positionError(obj, "obj.type value is not excalidrawlib")
  121. if (typeof obj.version !== "number") return positionError(obj, "obj.version type is not number")
  122. if ((!obj.libraryItems || !isLibraryItems(obj.libraryItems))
  123. && (!obj.library || !isLibrary(obj.library))
  124. ) return positionError(obj, "The library array does not exist or is wrong")
  125. return true
  126. }
  127. async function readExcalidrawlibFile(filePath: string): Promise<ImportedLibraryData> {
  128. const content = await readFile(filePath, 'utf-8');
  129. const parsed = JSON.parse(content);
  130. if (!isImportedLibraryData(parsed)) {
  131. console.error({filePath, parsed})
  132. throw new Error(`Invalid excalidrawlib file: ${filePath}`);
  133. }
  134. return parsed;
  135. }
  136. async function mergeExcalidrawlibFiles(dir: string): Promise<ImportedLibraryData | undefined> {
  137. const files = (await readdir(dir)).filter(isExcalidrawlibFile);
  138. let libraryItems: LibraryItems = []
  139. let mianLibData: ExportedLibraryData | undefined = undefined
  140. for (const file of files) {
  141. const fullPath = path.join(dir, file);
  142. const data = await readExcalidrawlibFile(fullPath);
  143. if (file === "library.excalidrawlib") {
  144. mianLibData = {
  145. type: data.type ?? "excalidrawlib",
  146. version: data.version ?? 2,
  147. source: data.source ?? "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor",
  148. libraryItems: data.libraryItems ?? data.library ?? []
  149. }
  150. } else {
  151. libraryItems = mergeLibraryItems(libraryItems, data.libraryItems ?? data.library ?? [], data.version ?? 1)
  152. }
  153. }
  154. if (mianLibData === undefined) return undefined
  155. mianLibData.libraryItems = mergeLibraryItems(mianLibData.libraryItems, libraryItems)
  156. return mianLibData;
  157. }
  158. async function saveMergedExcalidrawlib(data: ImportedLibraryData, filePath: string) {
  159. const jsonString = JSON.stringify(data, null, 2); // 缩进美化
  160. await writeFile(filePath, jsonString, 'utf-8');
  161. console.log(`✅ 合并完成,已保存到 ${filePath}`);
  162. }
  163. // 执行流程
  164. const merged = await mergeExcalidrawlibFiles(inputDir);
  165. if (merged !== undefined) {
  166. saveMergedExcalidrawlib(merged, outputFile);
  167. }