@wenyan-md/core 3.0.3 → 3.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/core.js CHANGED
@@ -524,9 +524,9 @@ function createMathJaxParser(options = {}) {
524
524
  fontCache: options.fontCache ?? "none"
525
525
  });
526
526
  function addContainer(math, doc) {
527
+ const container = math.typesetRoot;
527
528
  const tag = math.display ? "section" : "span";
528
529
  const cls = math.display ? "block-equation" : "inline-equation";
529
- const container = math.typesetRoot;
530
530
  if (math.math) {
531
531
  doc.adaptor.setAttribute(container, "math", math.math);
532
532
  }
@@ -1087,7 +1087,7 @@ function transformUl(ulElement) {
1087
1087
  function tableToAsciiArt(table) {
1088
1088
  const rowsElements = Array.from(table.querySelectorAll("tr"));
1089
1089
  const rows = rowsElements.map(
1090
- (tr) => Array.from(tr.querySelectorAll("th, td")).map((td) => td.innerText.trim())
1090
+ (tr) => Array.from(tr.querySelectorAll("th, td")).map((td) => (td?.textContent || "").trim())
1091
1091
  );
1092
1092
  if (rows.length === 0) return "";
1093
1093
  const columnWidths = rows[0].map(
package/dist/publish.js CHANGED
@@ -107,6 +107,57 @@ class UploadCacheStore {
107
107
  return this.adapter.calcHash(buffer);
108
108
  }
109
109
  }
110
+ const defaultCredential = {};
111
+ class CredentialStore {
112
+ credential = { ...defaultCredential };
113
+ adapter;
114
+ initPromise;
115
+ constructor(adapter) {
116
+ this.adapter = adapter;
117
+ this.initPromise = this.load();
118
+ }
119
+ async load() {
120
+ try {
121
+ const loadedData = await this.adapter.loadCredential();
122
+ if (loadedData) {
123
+ this.credential = loadedData;
124
+ }
125
+ } catch (error) {
126
+ throw new Error(`无法加载凭据: ${error instanceof Error ? error.message : String(error)}`);
127
+ }
128
+ }
129
+ async save() {
130
+ try {
131
+ await this.adapter.saveCredential(this.credential);
132
+ } catch (error) {
133
+ throw new Error(`无法保存凭据: ${error instanceof Error ? error.message : String(error)}`);
134
+ }
135
+ }
136
+ async _getWechatCredential() {
137
+ await this.initPromise;
138
+ return this.credential.wechat ?? {};
139
+ }
140
+ async getWechatCredential(appId) {
141
+ const wechat = await this._getWechatCredential();
142
+ if (!wechat) return null;
143
+ const appSecret = wechat[appId];
144
+ if (!appSecret) return null;
145
+ return { appId, appSecret };
146
+ }
147
+ async saveWechatCredential(appId, appSecret) {
148
+ await this.initPromise;
149
+ this.credential.wechat ??= {};
150
+ this.credential.wechat[appId] = appSecret;
151
+ await this.save();
152
+ }
153
+ async deleteWechatCredential(appId) {
154
+ await this.initPromise;
155
+ if (this.credential.wechat) {
156
+ delete this.credential.wechat[appId];
157
+ await this.save();
158
+ }
159
+ }
160
+ }
110
161
  class WechatPublisher {
111
162
  tokenStore;
112
163
  uploadCacheStore;
@@ -134,12 +185,13 @@ class WechatPublisher {
134
185
  await this.tokenStore.setToken(appId, result.access_token, result.expires_in);
135
186
  return result.access_token;
136
187
  }
137
- async uploadImage(file, filename, accessToken) {
188
+ async uploadImage(file, filename, accessToken, appId) {
138
189
  let hash;
139
190
  if (this.uploadCacheStore) {
140
191
  const arrayBuffer = await file.arrayBuffer();
141
192
  hash = await this.uploadCacheStore.calcHash(arrayBuffer);
142
- const cached = await this.uploadCacheStore.get(hash);
193
+ const cacheKey = appId ? `${hash}:${appId}` : hash;
194
+ const cached = await this.uploadCacheStore.get(cacheKey);
143
195
  if (cached) {
144
196
  return {
145
197
  media_id: cached.media_id,
@@ -149,7 +201,8 @@ class WechatPublisher {
149
201
  }
150
202
  const data = await this.uploadMaterial("image", file, filename, accessToken);
151
203
  if (this.uploadCacheStore && hash) {
152
- await this.uploadCacheStore.set(hash, data.media_id, data.url);
204
+ const cacheKey = appId ? `${hash}:${appId}` : hash;
205
+ await this.uploadCacheStore.set(cacheKey, data.media_id, data.url);
153
206
  }
154
207
  return data;
155
208
  }
@@ -166,8 +219,10 @@ class WechatPublisher {
166
219
  }
167
220
  }
168
221
  export {
222
+ CredentialStore,
169
223
  TokenStore,
170
224
  UploadCacheStore,
171
225
  WechatPublisher,
226
+ defaultCredential,
172
227
  defaultTokenCache
173
228
  };
@@ -0,0 +1,24 @@
1
+ export interface WenyanCredential {
2
+ wechat?: Record<string, string>;
3
+ }
4
+ export declare const defaultCredential: WenyanCredential;
5
+ export interface CredentialStorageAdapter {
6
+ loadCredential(): Promise<WenyanCredential | null>;
7
+ saveCredential(credential: WenyanCredential): Promise<void>;
8
+ clearCredential(): Promise<void>;
9
+ }
10
+ export declare class CredentialStore {
11
+ private credential;
12
+ private adapter;
13
+ private initPromise;
14
+ constructor(adapter: CredentialStorageAdapter);
15
+ private load;
16
+ private save;
17
+ private _getWechatCredential;
18
+ getWechatCredential(appId: string): Promise<{
19
+ appId: string;
20
+ appSecret: string;
21
+ } | null>;
22
+ saveWechatCredential(appId: string, appSecret: string): Promise<void>;
23
+ deleteWechatCredential(appId: string): Promise<void>;
24
+ }
@@ -7,7 +7,6 @@ export interface ThemeConfigOptions {
7
7
  description?: string;
8
8
  path: string;
9
9
  }
10
- export declare const configDir: string;
11
10
  export declare const configPath: string;
12
11
  declare class ConfigStore {
13
12
  private config;
@@ -0,0 +1,7 @@
1
+ import { CredentialStorageAdapter, WenyanCredential } from "../credentialStore.js";
2
+ export declare const credentialPath: string;
3
+ export declare class NodeCredentialStorageAdapter implements CredentialStorageAdapter {
4
+ loadCredential(): Promise<WenyanCredential | null>;
5
+ saveCredential(credential: WenyanCredential): Promise<void>;
6
+ clearCredential(): Promise<void>;
7
+ }
@@ -1,6 +1,8 @@
1
1
  import { WechatPublishResponse } from "../wechat.js";
2
2
  import { ArticleOptions, WechatPublisher } from "../publish.js";
3
+ import { CredentialStore } from "../credentialStore.js";
3
4
  export declare const wechatPublisher: WechatPublisher;
5
+ export declare const credentialStore: CredentialStore;
4
6
  interface PublishOptions {
5
7
  appId?: string;
6
8
  appSecret?: string;
@@ -7,8 +7,9 @@ export interface RenderOptions {
7
7
  footnote: boolean;
8
8
  }
9
9
  export interface PublishOptions extends RenderOptions {
10
+ appId?: string;
10
11
  }
11
- export interface ClientPublishOptions extends RenderOptions {
12
+ export interface ClientPublishOptions extends PublishOptions {
12
13
  server?: string;
13
14
  apiKey?: string;
14
15
  clientVersion?: string;
@@ -1,4 +1,5 @@
1
1
  import crypto from "node:crypto";
2
+ export declare const configDir: string;
2
3
  export declare function readFileContent(filePath: string): Promise<string>;
3
4
  export declare function readBinaryFile(filePath: string): Promise<Buffer>;
4
5
  export declare function safeReadJson<T>(file: string, fallback: T): Promise<T>;
@@ -17,9 +17,10 @@ export declare class WechatPublisher {
17
17
  private fetchAccessToken;
18
18
  constructor(httpAdapter: HttpAdapter, tokenStoreAdapter?: TokenStorageAdapter, uploadCacheStoreAdapter?: UploadCacheStorageAdapter);
19
19
  getAccessTokenWithCache(appId: string, appSecret: string): Promise<string>;
20
- uploadImage(file: Blob, filename: string, accessToken: string): Promise<WechatUploadResponse>;
20
+ uploadImage(file: Blob, filename: string, accessToken: string, appId?: string): Promise<WechatUploadResponse>;
21
21
  publishToDraft(accessToken: string, options: WechatPublishOptions): Promise<WechatPublishResponse>;
22
22
  clearCache(): Promise<void>;
23
23
  }
24
24
  export * from "./tokenStore.js";
25
25
  export * from "./uploadCacheStore.js";
26
+ export * from "./credentialStore.js";
package/dist/wrapper.js CHANGED
@@ -4,13 +4,22 @@ import https from "node:https";
4
4
  import { JSDOM } from "jsdom";
5
5
  import fs, { stat } from "node:fs/promises";
6
6
  import crypto from "node:crypto";
7
+ import os from "node:os";
7
8
  import { fileFromPath } from "formdata-node/file-from-path";
8
9
  import { FormDataEncoder } from "form-data-encoder";
9
10
  import { FormData } from "formdata-node";
10
11
  import { Readable } from "node:stream";
11
- import os from "node:os";
12
- import { defaultTokenCache, WechatPublisher } from "./publish.js";
12
+ import { defaultTokenCache, defaultCredential, WechatPublisher, CredentialStore } from "./publish.js";
13
13
  import { createWenyanCore, getAllGzhThemes } from "./core.js";
14
+ const configDir = (() => {
15
+ if (process.env.XDG_CONFIG_HOME) {
16
+ return path.join(process.env.XDG_CONFIG_HOME, "wenyan-md");
17
+ }
18
+ if (process.env.APPDATA) {
19
+ return path.join(process.env.APPDATA, "wenyan-md");
20
+ }
21
+ return path.join(os.homedir(), ".config", "wenyan-md");
22
+ })();
14
23
  async function readFileContent(filePath) {
15
24
  return await fs.readFile(filePath, "utf-8");
16
25
  }
@@ -233,7 +242,7 @@ async function uploadStyledContent(gzhContent, serverUrl, headers) {
233
242
  return mdFileId;
234
243
  }
235
244
  async function requestServerPublish(mdFileId, serverUrl, headers, options) {
236
- const { theme, customTheme, highlight, macStyle, footnote } = options;
245
+ const { theme, customTheme, highlight, macStyle, footnote, appId } = options;
237
246
  const publishRes = await fetch(`${serverUrl}/publish`, {
238
247
  method: "POST",
239
248
  headers: {
@@ -246,7 +255,8 @@ async function requestServerPublish(mdFileId, serverUrl, headers, options) {
246
255
  highlight,
247
256
  customTheme,
248
257
  macStyle,
249
- footnote
258
+ footnote,
259
+ appId
250
260
  })
251
261
  });
252
262
  const publishData = await publishRes.json();
@@ -330,80 +340,6 @@ const nodeHttpAdapter = {
330
340
  };
331
341
  }
332
342
  };
333
- const defaultConfig = {};
334
- const configDir = process.env.APPDATA ? path.join(process.env.APPDATA, "wenyan-md") : path.join(os.homedir(), ".config", "wenyan-md");
335
- const configPath = path.join(configDir, "config.json");
336
- class ConfigStore {
337
- config = { ...defaultConfig };
338
- initPromise;
339
- constructor() {
340
- this.initPromise = this.load();
341
- }
342
- async load() {
343
- await ensureDir(configDir);
344
- this.config = await safeReadJson(configPath, defaultConfig);
345
- }
346
- async save() {
347
- try {
348
- await ensureDir(configDir);
349
- await safeWriteJson(configPath, this.config);
350
- } catch (error) {
351
- throw new Error(`无法保存配置文件: ${error instanceof Error ? error.message : String(error)}`);
352
- }
353
- }
354
- async getConfig() {
355
- await this.initPromise;
356
- return this.config;
357
- }
358
- async getThemes() {
359
- await this.initPromise;
360
- return Object.values(this.config.themes ?? {});
361
- }
362
- async getThemeById(themeId) {
363
- await this.initPromise;
364
- const themeOption = this.config.themes?.[themeId];
365
- if (!themeOption) return void 0;
366
- const absoluteFilePath = path.join(configDir, themeOption.path);
367
- try {
368
- return await fs.readFile(absoluteFilePath, "utf-8");
369
- } catch {
370
- return void 0;
371
- }
372
- }
373
- async addThemeToConfig(name, content) {
374
- await this.initPromise;
375
- const savedPath = await this.addThemeFile(name, content);
376
- this.config.themes ??= {};
377
- this.config.themes[name] = {
378
- id: name,
379
- name,
380
- path: savedPath
381
- };
382
- await this.save();
383
- }
384
- async addThemeFile(themeId, themeContent) {
385
- const filePath = `themes/${themeId}.css`;
386
- const absoluteFilePath = path.join(configDir, filePath);
387
- await ensureDir(path.dirname(absoluteFilePath));
388
- await fs.writeFile(absoluteFilePath, themeContent, "utf-8");
389
- return filePath;
390
- }
391
- async deleteThemeFromConfig(themeId) {
392
- await this.initPromise;
393
- const theme = this.config.themes?.[themeId];
394
- if (!theme) return;
395
- await this.deleteThemeFile(theme.path);
396
- delete this.config.themes[themeId];
397
- await this.save();
398
- }
399
- async deleteThemeFile(filePath) {
400
- try {
401
- await fs.unlink(path.join(configDir, filePath));
402
- } catch {
403
- }
404
- }
405
- }
406
- const configStore = new ConfigStore();
407
343
  const tokenPath = path.join(configDir, "token.json");
408
344
  class NodeTokenStorageAdapter {
409
345
  async loadToken() {
@@ -447,9 +383,28 @@ class NodeUploadCacheAdapter {
447
383
  return crypto.createHash("md5").update(Buffer.from(buffer)).digest("hex");
448
384
  }
449
385
  }
386
+ const credentialPath = path.join(configDir, "credential.json");
387
+ class NodeCredentialStorageAdapter {
388
+ async loadCredential() {
389
+ await ensureDir(configDir);
390
+ return await safeReadJson(credentialPath, defaultCredential);
391
+ }
392
+ async saveCredential(credential) {
393
+ await ensureDir(configDir);
394
+ await safeWriteJson(credentialPath, credential);
395
+ }
396
+ async clearCredential() {
397
+ throw new Error("Method not implemented.");
398
+ }
399
+ }
450
400
  const mediaIdMapping = /* @__PURE__ */ new Map();
451
- const wechatPublisher = new WechatPublisher(nodeHttpAdapter, new NodeTokenStorageAdapter(), new NodeUploadCacheAdapter());
452
- async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
401
+ const wechatPublisher = new WechatPublisher(
402
+ nodeHttpAdapter,
403
+ new NodeTokenStorageAdapter(),
404
+ new NodeUploadCacheAdapter()
405
+ );
406
+ const credentialStore = new CredentialStore(new NodeCredentialStorageAdapter());
407
+ async function uploadImage(imageUrl, accessToken, fileName, relativePath, appId) {
453
408
  let fileData;
454
409
  let finalName;
455
410
  if (imageUrl.startsWith("http")) {
@@ -478,11 +433,11 @@ async function uploadImage(imageUrl, accessToken, fileName, relativePath) {
478
433
  const fileFromPathResult = await fileFromPath(resolvedPath);
479
434
  fileData = new Blob([await fileFromPathResult.arrayBuffer()], { type: fileFromPathResult.type });
480
435
  }
481
- const data = await wechatPublisher.uploadImage(fileData, finalName, accessToken);
436
+ const data = await wechatPublisher.uploadImage(fileData, finalName, accessToken, appId);
482
437
  mediaIdMapping.set(data.url, data.media_id);
483
438
  return data;
484
439
  }
485
- async function uploadImages(content, accessToken, relativePath) {
440
+ async function uploadImages(content, accessToken, relativePath, appId) {
486
441
  if (!content.includes("<img")) {
487
442
  return { html: content, firstImageId: "" };
488
443
  }
@@ -493,7 +448,7 @@ async function uploadImages(content, accessToken, relativePath) {
493
448
  const dataSrc = element.getAttribute("src");
494
449
  if (dataSrc) {
495
450
  if (!dataSrc.startsWith("https://mmbiz.qpic.cn")) {
496
- const resp = await uploadImage(dataSrc, accessToken, void 0, relativePath);
451
+ const resp = await uploadImage(dataSrc, accessToken, void 0, relativePath, appId);
497
452
  element.setAttribute("src", resp.url);
498
453
  return resp.media_id;
499
454
  } else {
@@ -510,20 +465,16 @@ async function uploadImages(content, accessToken, relativePath) {
510
465
  async function publishToWechatDraft(articleOptions, publishOptions = {}) {
511
466
  const { title, content, cover, author, source_url } = articleOptions;
512
467
  const { appId, appSecret, relativePath } = publishOptions;
513
- const appIdFinal = appId ?? process.env.WECHAT_APP_ID;
514
- const appSecretFinal = appSecret ?? process.env.WECHAT_APP_SECRET;
515
- if (!appIdFinal || !appSecretFinal) {
516
- throw new Error("请通过参数或环境变量 WECHAT_APP_ID / WECHAT_APP_SECRET 提供公众号凭据");
517
- }
468
+ const { appId: appIdFinal, appSecret: appSecretFinal } = await getAppIdAndSecret(appId, appSecret);
518
469
  const accessToken = await wechatPublisher.getAccessTokenWithCache(appIdFinal, appSecretFinal);
519
- const { html, firstImageId } = await uploadImages(content, accessToken, relativePath);
470
+ const { html, firstImageId } = await uploadImages(content, accessToken, relativePath, appIdFinal);
520
471
  let thumbMediaId;
521
472
  if (cover) {
522
473
  const cachedThumbMediaId = mediaIdMapping.get(cover);
523
474
  if (cachedThumbMediaId) {
524
475
  thumbMediaId = cachedThumbMediaId;
525
476
  } else {
526
- const resp = await uploadImage(cover, accessToken, "cover.jpg", relativePath);
477
+ const resp = await uploadImage(cover, accessToken, "cover.jpg", relativePath, appIdFinal);
527
478
  thumbMediaId = resp.media_id;
528
479
  }
529
480
  } else {
@@ -532,7 +483,7 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
532
483
  if (cachedThumbMediaId) {
533
484
  thumbMediaId = cachedThumbMediaId;
534
485
  } else {
535
- const resp = await uploadImage(firstImageId, accessToken, "cover.jpg", relativePath);
486
+ const resp = await uploadImage(firstImageId, accessToken, "cover.jpg", relativePath, appIdFinal);
536
487
  thumbMediaId = resp.media_id;
537
488
  }
538
489
  } else {
@@ -557,6 +508,97 @@ async function publishToWechatDraft(articleOptions, publishOptions = {}) {
557
508
  async function publishToDraft(title, content, cover = "", options = {}) {
558
509
  return publishToWechatDraft({ title, content, cover }, options);
559
510
  }
511
+ async function getAppIdAndSecret(appId, appSecret) {
512
+ if (appId && appSecret) {
513
+ return { appId, appSecret };
514
+ }
515
+ const envAppId = process.env.WECHAT_APP_ID;
516
+ const envAppSecret = process.env.WECHAT_APP_SECRET;
517
+ if (envAppId && envAppSecret && (envAppId === appId || !appId)) {
518
+ return { appId: envAppId, appSecret: envAppSecret };
519
+ }
520
+ if (!appId) {
521
+ throw new Error("未提供 AppID:请通过参数、环境变量或配置文件指定。");
522
+ }
523
+ const credential = await credentialStore.getWechatCredential(appId);
524
+ if (credential?.appId && credential?.appSecret) {
525
+ return { appId: credential.appId, appSecret: credential.appSecret };
526
+ }
527
+ throw new Error(`未能找到 AppID 为 "${appId}" 的公众号凭据,请检查配置文件。`);
528
+ }
529
+ const defaultConfig = {};
530
+ const configPath = path.join(configDir, "config.json");
531
+ class ConfigStore {
532
+ config = { ...defaultConfig };
533
+ initPromise;
534
+ constructor() {
535
+ this.initPromise = this.load();
536
+ }
537
+ async load() {
538
+ await ensureDir(configDir);
539
+ this.config = await safeReadJson(configPath, defaultConfig);
540
+ }
541
+ async save() {
542
+ try {
543
+ await ensureDir(configDir);
544
+ await safeWriteJson(configPath, this.config);
545
+ } catch (error) {
546
+ throw new Error(`无法保存配置文件: ${error instanceof Error ? error.message : String(error)}`);
547
+ }
548
+ }
549
+ async getConfig() {
550
+ await this.initPromise;
551
+ return this.config;
552
+ }
553
+ async getThemes() {
554
+ await this.initPromise;
555
+ return Object.values(this.config.themes ?? {});
556
+ }
557
+ async getThemeById(themeId) {
558
+ await this.initPromise;
559
+ const themeOption = this.config.themes?.[themeId];
560
+ if (!themeOption) return void 0;
561
+ const absoluteFilePath = path.join(configDir, themeOption.path);
562
+ try {
563
+ return await fs.readFile(absoluteFilePath, "utf-8");
564
+ } catch {
565
+ return void 0;
566
+ }
567
+ }
568
+ async addThemeToConfig(name, content) {
569
+ await this.initPromise;
570
+ const savedPath = await this.addThemeFile(name, content);
571
+ this.config.themes ??= {};
572
+ this.config.themes[name] = {
573
+ id: name,
574
+ name,
575
+ path: savedPath
576
+ };
577
+ await this.save();
578
+ }
579
+ async addThemeFile(themeId, themeContent) {
580
+ const filePath = `themes/${themeId}.css`;
581
+ const absoluteFilePath = path.join(configDir, filePath);
582
+ await ensureDir(path.dirname(absoluteFilePath));
583
+ await fs.writeFile(absoluteFilePath, themeContent, "utf-8");
584
+ return filePath;
585
+ }
586
+ async deleteThemeFromConfig(themeId) {
587
+ await this.initPromise;
588
+ const theme = this.config.themes?.[themeId];
589
+ if (!theme) return;
590
+ await this.deleteThemeFile(theme.path);
591
+ delete this.config.themes[themeId];
592
+ await this.save();
593
+ }
594
+ async deleteThemeFile(filePath) {
595
+ try {
596
+ await fs.unlink(path.join(configDir, filePath));
597
+ } catch {
598
+ }
599
+ }
600
+ }
601
+ const configStore = new ConfigStore();
560
602
  const wenyanCoreInstance = await createWenyanCore();
561
603
  async function renderWithTheme(markdownContent, options) {
562
604
  if (!markdownContent) {
@@ -683,6 +725,7 @@ async function renderAndPublish(inputContent, options, getInputContent) {
683
725
  source_url: gzhContent.source_url
684
726
  },
685
727
  {
728
+ appId: options.appId,
686
729
  relativePath: absoluteDirPath
687
730
  }
688
731
  );
@@ -710,6 +753,7 @@ export {
710
753
  configDir,
711
754
  configPath,
712
755
  configStore,
756
+ credentialStore,
713
757
  ensureDir,
714
758
  getGzhContent,
715
759
  getNormalizeFilePath,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wenyan-md/core",
3
- "version": "3.0.3",
3
+ "version": "3.0.5",
4
4
  "description": "Core library for Wenyan markdown rendering & publishing",
5
5
  "author": "Lei <caol64@gmail.com> (https://github.com/caol64)",
6
6
  "license": "Apache-2.0",
@@ -60,7 +60,7 @@
60
60
  "vitest": "^3.2.4"
61
61
  },
62
62
  "dependencies": {
63
- "css-tree": "^3.1.0",
63
+ "css-tree": "^3.2.1",
64
64
  "front-matter": "^4.0.2",
65
65
  "highlight.js": "11.10.0",
66
66
  "marked": "^15.0.12",
@@ -83,6 +83,9 @@
83
83
  "optional": true
84
84
  }
85
85
  },
86
+ "publishConfig": {
87
+ "registry": "https://registry.npmjs.org/"
88
+ },
86
89
  "scripts": {
87
90
  "typecheck": "tsc --noEmit",
88
91
  "build": "vite build && tsc",