@onozaty/growi-uploader 1.6.1 → 1.7.1
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/README.ja.md +44 -2
- package/README.md +44 -2
- package/dist/index.mjs +184 -85
- package/package.json +1 -1
package/README.ja.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# growi-uploader
|
|
2
2
|
|
|
3
|
-
[English](README.md) | 日本語
|
|
4
|
-
|
|
5
3
|
[](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
|
|
6
4
|
[](https://codecov.io/gh/onozaty/growi-uploader)
|
|
7
5
|
[](https://www.npmjs.com/package/@onozaty/growi-uploader)
|
|
8
6
|
[](https://opensource.org/licenses/MIT)
|
|
9
7
|
|
|
8
|
+
[English](README.md) | 日本語
|
|
9
|
+
|
|
10
10
|
ローカルのMarkdownファイルと添付ファイルを[GROWI](https://growi.org/) Wikiに一括アップロードするCLIツールです。
|
|
11
11
|
|
|
12
12
|
## 機能
|
|
@@ -274,6 +274,48 @@ Download the [documentation](/attachment/68f3a3fa794f665ad2c0d2b3).
|
|
|
274
274
|
|
|
275
275
|
両方の検出方法で、複数のリンク形式(`./`あり・なし)がサポートされています。
|
|
276
276
|
|
|
277
|
+
### クロスページ参照
|
|
278
|
+
|
|
279
|
+
命名規則に従うファイル(`BBB_attachment_*`)を別のページ(`AAA.md`)から参照している場合:
|
|
280
|
+
|
|
281
|
+
- 添付ファイルは所有者ページ(`/BBB`)にのみアップロードされます
|
|
282
|
+
- 参照元ページ(`AAA.md`)のリンクは添付ファイルのURL(`/attachment/{id}`)に置換されます
|
|
283
|
+
|
|
284
|
+
**例:**
|
|
285
|
+
```
|
|
286
|
+
ファイル構成:
|
|
287
|
+
AAA.md (内容: )
|
|
288
|
+
BBB.md
|
|
289
|
+
BBB_attachment_image.png
|
|
290
|
+
|
|
291
|
+
結果:
|
|
292
|
+
- BBB_attachment_image.png は /BBB ページにのみアップロード
|
|
293
|
+
- AAA.md 内のリンクは /attachment/{id} に置換
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## ページリンクの変換
|
|
297
|
+
|
|
298
|
+
他のMarkdownファイル(`.md`拡張子)へのリンクは自動的にGROWI形式に変換されます。
|
|
299
|
+
|
|
300
|
+
### 対応パターン
|
|
301
|
+
|
|
302
|
+
```markdown
|
|
303
|
+
# アップロード前(ローカル)
|
|
304
|
+
[ユーザーガイド](./guide.md)
|
|
305
|
+
[API概要](../api/overview.md)
|
|
306
|
+
[認証セクション](./api/auth.md#setup)
|
|
307
|
+
|
|
308
|
+
# アップロード後(GROWI上)
|
|
309
|
+
[ユーザーガイド](./guide)
|
|
310
|
+
[API概要](../api/overview)
|
|
311
|
+
[認証セクション](./api/auth#setup)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- `.md`拡張子が除去されます
|
|
315
|
+
- 相対パスのプレフィックス(`./`、`../`)は維持されます(GROWIは相対リンクをサポート)
|
|
316
|
+
- アンカーリンク(`#section`)は維持されます
|
|
317
|
+
- 外部URL(`http://`、`https://`)は変更されません
|
|
318
|
+
|
|
277
319
|
## 高度な使い方
|
|
278
320
|
|
|
279
321
|
### 既存ページの更新
|
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# growi-uploader
|
|
2
2
|
|
|
3
|
-
English | [日本語](README.ja.md)
|
|
4
|
-
|
|
5
3
|
[](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
|
|
6
4
|
[](https://codecov.io/gh/onozaty/growi-uploader)
|
|
7
5
|
[](https://www.npmjs.com/package/@onozaty/growi-uploader)
|
|
8
6
|
[](https://opensource.org/licenses/MIT)
|
|
9
7
|
|
|
8
|
+
English | [日本語](README.ja.md)
|
|
9
|
+
|
|
10
10
|
A CLI tool to batch upload local Markdown files and attachments to [GROWI](https://growi.org/) Wiki.
|
|
11
11
|
|
|
12
12
|
## Features
|
|
@@ -274,6 +274,48 @@ Download the [documentation](/attachment/68f3a3fa794f665ad2c0d2b3).
|
|
|
274
274
|
|
|
275
275
|
Both detection methods support multiple link formats (with or without `./`).
|
|
276
276
|
|
|
277
|
+
### Cross-Page References
|
|
278
|
+
|
|
279
|
+
When a file following the naming convention (`BBB_attachment_*`) is referenced from a different page (`AAA.md`):
|
|
280
|
+
|
|
281
|
+
- The attachment is uploaded only to its owner page (`/BBB`)
|
|
282
|
+
- Links in the referencing page (`AAA.md`) are replaced with the attachment URL (`/attachment/{id}`)
|
|
283
|
+
|
|
284
|
+
**Example:**
|
|
285
|
+
```
|
|
286
|
+
File structure:
|
|
287
|
+
AAA.md (contains: )
|
|
288
|
+
BBB.md
|
|
289
|
+
BBB_attachment_image.png
|
|
290
|
+
|
|
291
|
+
Result:
|
|
292
|
+
- BBB_attachment_image.png is uploaded to /BBB page only
|
|
293
|
+
- The link in AAA.md is replaced with /attachment/{id}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Page Link Conversion
|
|
297
|
+
|
|
298
|
+
Links to other Markdown files (`.md` extension) are automatically converted to GROWI format.
|
|
299
|
+
|
|
300
|
+
### Supported Patterns
|
|
301
|
+
|
|
302
|
+
```markdown
|
|
303
|
+
# Before upload (local)
|
|
304
|
+
[User Guide](./guide.md)
|
|
305
|
+
[API Overview](../api/overview.md)
|
|
306
|
+
[Auth Section](./api/auth.md#setup)
|
|
307
|
+
|
|
308
|
+
# After upload (on GROWI)
|
|
309
|
+
[User Guide](./guide)
|
|
310
|
+
[API Overview](../api/overview)
|
|
311
|
+
[Auth Section](./api/auth#setup)
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
- The `.md` extension is removed
|
|
315
|
+
- Relative path prefixes (`./`, `../`) are preserved (GROWI supports relative links)
|
|
316
|
+
- Anchor links (`#section`) are preserved
|
|
317
|
+
- External URLs (`http://`, `https://`) are not modified
|
|
318
|
+
|
|
277
319
|
## Advanced Usage
|
|
278
320
|
|
|
279
321
|
### Update Existing Pages
|
package/dist/index.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { lookup } from "mime-types";
|
|
|
8
8
|
import { glob } from "glob";
|
|
9
9
|
|
|
10
10
|
//#region package.json
|
|
11
|
-
var version = "1.
|
|
11
|
+
var version = "1.7.1";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/config.ts
|
|
@@ -293,9 +293,25 @@ const processLinkPath = (linkPath, originalLinkPath, markdownFilePath, sourceDir
|
|
|
293
293
|
else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), decodedPath);
|
|
294
294
|
if (!existsSync(absolutePath)) return null;
|
|
295
295
|
const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
|
|
296
|
+
const fileName = basename(normalizedPath);
|
|
297
|
+
const namingMatch = fileName.match(/^(.+)_attachment_/);
|
|
298
|
+
if (namingMatch && namingMatch[1]) {
|
|
299
|
+
const ownerPageName = namingMatch[1];
|
|
300
|
+
const attachmentDir = dirname(normalizedPath);
|
|
301
|
+
const currentMarkdownDir = dirname(markdownFilePath);
|
|
302
|
+
const currentPageName = basename(markdownFilePath, ".md");
|
|
303
|
+
return {
|
|
304
|
+
localPath: normalizedPath,
|
|
305
|
+
fileName,
|
|
306
|
+
detectionPattern: "link",
|
|
307
|
+
originalLinkPaths: [originalLinkPath],
|
|
308
|
+
isExternalReference: attachmentDir !== currentMarkdownDir || ownerPageName !== currentPageName,
|
|
309
|
+
ownerPageName
|
|
310
|
+
};
|
|
311
|
+
}
|
|
296
312
|
return {
|
|
297
313
|
localPath: normalizedPath,
|
|
298
|
-
fileName
|
|
314
|
+
fileName,
|
|
299
315
|
detectionPattern: "link",
|
|
300
316
|
originalLinkPaths: [originalLinkPath]
|
|
301
317
|
};
|
|
@@ -315,7 +331,7 @@ const extractLinkedAttachments = (content, markdownFilePath, sourceDir) => {
|
|
|
315
331
|
const attachments = [];
|
|
316
332
|
const linkRegex = new RegExp([
|
|
317
333
|
"!?",
|
|
318
|
-
"\\[(?<alt>[^\\]]*)\\]",
|
|
334
|
+
"\\[(?<alt>(?:[^\\]\\\\]|\\\\.)*)\\]",
|
|
319
335
|
"\\(",
|
|
320
336
|
"(?:",
|
|
321
337
|
"<(?<anglePath>[^>]+)>",
|
|
@@ -415,6 +431,56 @@ const escapeRegex = (str) => {
|
|
|
415
431
|
return str.replace(/[.*+?^${}()|[\]\\<>]/g, "\\$&");
|
|
416
432
|
};
|
|
417
433
|
/**
|
|
434
|
+
* Build regex patterns from originalLinkPaths
|
|
435
|
+
* Generates escaped patterns including variations (./path <-> path)
|
|
436
|
+
*/
|
|
437
|
+
const buildPatternsFromLinkPaths = (linkPaths) => {
|
|
438
|
+
const patterns = [];
|
|
439
|
+
for (const linkPath of linkPaths) {
|
|
440
|
+
const escapedPath = escapeRegex(linkPath);
|
|
441
|
+
patterns.push(escapedPath);
|
|
442
|
+
if (linkPath.startsWith("./")) {
|
|
443
|
+
const escapedWithoutDot = escapeRegex(linkPath.substring(2));
|
|
444
|
+
patterns.push(escapedWithoutDot);
|
|
445
|
+
} else if (!linkPath.startsWith("../") && !linkPath.startsWith("<")) patterns.push(`\\./${escapedPath}`);
|
|
446
|
+
}
|
|
447
|
+
return patterns;
|
|
448
|
+
};
|
|
449
|
+
/**
|
|
450
|
+
* Replace links in markdown content matching the given patterns with a target path
|
|
451
|
+
* Handles image links, regular links, and HTML img tags
|
|
452
|
+
*
|
|
453
|
+
* @param markdown Markdown content
|
|
454
|
+
* @param patterns Array of escaped regex patterns to match
|
|
455
|
+
* @param targetPath Path to replace with (e.g., /attachment/xxx)
|
|
456
|
+
* @returns Object with replaced content and whether any replacement occurred
|
|
457
|
+
*/
|
|
458
|
+
const replaceLinksWithPatterns = (markdown, patterns, targetPath) => {
|
|
459
|
+
let result = markdown;
|
|
460
|
+
let replaced = false;
|
|
461
|
+
for (const pattern of patterns) {
|
|
462
|
+
const imgRegex = new RegExp(`!\\[((?:[^\\]\\\\]|\\\\.)*)\\]\\(${pattern}\\)`, "g");
|
|
463
|
+
if (imgRegex.test(result)) {
|
|
464
|
+
replaced = true;
|
|
465
|
+
result = result.replace(imgRegex, ``);
|
|
466
|
+
}
|
|
467
|
+
const linkRegex = new RegExp(`(?<!!)\\[((?:[^\\]\\\\]|\\\\.)*)\\]\\(${pattern}\\)`, "g");
|
|
468
|
+
if (linkRegex.test(result)) {
|
|
469
|
+
replaced = true;
|
|
470
|
+
result = result.replace(linkRegex, `[$1](${targetPath})`);
|
|
471
|
+
}
|
|
472
|
+
const imgTagRegex = new RegExp(`(<img\\s+[^>]*src=)(["'])${pattern}\\2([^>]*>)`, "gi");
|
|
473
|
+
if (imgTagRegex.test(result)) {
|
|
474
|
+
replaced = true;
|
|
475
|
+
result = result.replace(imgTagRegex, `$1$2${targetPath}$2$3`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return {
|
|
479
|
+
content: result,
|
|
480
|
+
replaced
|
|
481
|
+
};
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
418
484
|
* Replace attachment links in Markdown content with GROWI format
|
|
419
485
|
*
|
|
420
486
|
* Supports two detection patterns:
|
|
@@ -437,30 +503,42 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
|
437
503
|
const escapedFileName = escapeRegex(`${pageName}_attachment_${attachment.fileName}`);
|
|
438
504
|
patterns.push(escapedFileName, `\\./${escapedFileName}`);
|
|
439
505
|
}
|
|
440
|
-
if (attachment.originalLinkPaths)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
patterns.push(escapedWithoutDot);
|
|
446
|
-
} else if (!linkPath.startsWith("../") && !linkPath.startsWith("<")) patterns.push(`\\./${escapedPath}`);
|
|
506
|
+
if (attachment.originalLinkPaths) patterns.push(...buildPatternsFromLinkPaths(attachment.originalLinkPaths));
|
|
507
|
+
const { content: replacedContent, replaced: wasReplaced } = replaceLinksWithPatterns(result, patterns, growiPath);
|
|
508
|
+
if (wasReplaced) {
|
|
509
|
+
result = replacedContent;
|
|
510
|
+
replaced = true;
|
|
447
511
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
content: result,
|
|
515
|
+
replaced
|
|
516
|
+
};
|
|
517
|
+
};
|
|
518
|
+
/**
|
|
519
|
+
* Replace external attachment links in Markdown content with GROWI format
|
|
520
|
+
*
|
|
521
|
+
* External attachments are files that belong to another page (detected via naming convention).
|
|
522
|
+
* This function resolves them using a global attachment map that contains all uploaded attachments.
|
|
523
|
+
*
|
|
524
|
+
* @param markdown Original Markdown content
|
|
525
|
+
* @param attachments List of external attachments (isExternalReference === true)
|
|
526
|
+
* @param attachmentMap Global map of localPath → attachmentId
|
|
527
|
+
* @returns Object with replaced content and whether any replacement occurred
|
|
528
|
+
*/
|
|
529
|
+
const replaceExternalAttachmentLinks = (markdown, attachments, attachmentMap) => {
|
|
530
|
+
let result = markdown;
|
|
531
|
+
let replaced = false;
|
|
532
|
+
for (const attachment of attachments) {
|
|
533
|
+
if (!attachment.isExternalReference) continue;
|
|
534
|
+
const attachmentId = attachmentMap.get(attachment.localPath);
|
|
535
|
+
if (!attachmentId) continue;
|
|
536
|
+
const growiPath = `/attachment/${attachmentId}`;
|
|
537
|
+
const patterns = attachment.originalLinkPaths ? buildPatternsFromLinkPaths(attachment.originalLinkPaths) : [];
|
|
538
|
+
const { content: replacedContent, replaced: wasReplaced } = replaceLinksWithPatterns(result, patterns, growiPath);
|
|
539
|
+
if (wasReplaced) {
|
|
540
|
+
result = replacedContent;
|
|
541
|
+
replaced = true;
|
|
464
542
|
}
|
|
465
543
|
}
|
|
466
544
|
return {
|
|
@@ -501,11 +579,16 @@ const replaceMarkdownExtension = (markdown, basePath = "/") => {
|
|
|
501
579
|
/**
|
|
502
580
|
* Upload Markdown files and their attachments to GROWI
|
|
503
581
|
*
|
|
504
|
-
* This function orchestrates a
|
|
505
|
-
*
|
|
506
|
-
*
|
|
507
|
-
*
|
|
508
|
-
*
|
|
582
|
+
* This function orchestrates a 2-pass upload process:
|
|
583
|
+
*
|
|
584
|
+
* Pass 1: Page creation and attachment upload
|
|
585
|
+
* - Create or update page with original Markdown content
|
|
586
|
+
* - Upload attachments (skip external references)
|
|
587
|
+
* - Build global attachment map for cross-page reference resolution
|
|
588
|
+
*
|
|
589
|
+
* Pass 2: Link replacement
|
|
590
|
+
* - Replace attachment links (both local and external references)
|
|
591
|
+
* - Replace .md extension in page links
|
|
509
592
|
*
|
|
510
593
|
* @param files List of Markdown files to upload
|
|
511
594
|
* @param sourceDir Source directory containing files
|
|
@@ -523,9 +606,10 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
523
606
|
attachmentErrors: 0,
|
|
524
607
|
linkReplacementErrors: 0
|
|
525
608
|
};
|
|
609
|
+
const attachmentMap = /* @__PURE__ */ new Map();
|
|
610
|
+
const pageResults = [];
|
|
526
611
|
for (const file of files) {
|
|
527
|
-
const
|
|
528
|
-
const result = await createOrUpdatePage(file, content, config.update);
|
|
612
|
+
const result = await createOrUpdatePage(file, readFileSync(join(sourceDir, file.localPath), "utf-8"), config.update);
|
|
529
613
|
if (result.action === "created") {
|
|
530
614
|
stats.pagesCreated++;
|
|
531
615
|
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (created)`);
|
|
@@ -541,62 +625,77 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
541
625
|
if (config.verbose && result.error) console.error(formatDetailedError(result.error));
|
|
542
626
|
}
|
|
543
627
|
if (result.pageId && (result.action === "created" || result.action === "updated")) {
|
|
544
|
-
let
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
if (attachmentResult.
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
if (attachmentResult.attachmentId) {
|
|
555
|
-
attachment.attachmentId = attachmentResult.attachmentId;
|
|
556
|
-
hasAttachments = true;
|
|
557
|
-
}
|
|
558
|
-
if (attachmentResult.revisionId) latestRevisionId = attachmentResult.revisionId;
|
|
559
|
-
} else {
|
|
560
|
-
stats.attachmentErrors++;
|
|
561
|
-
console.error(`[ERROR] ${attachment.localPath} → ${file.growiPath} (${attachmentResult.errorMessage || "failed to upload attachment"})`);
|
|
562
|
-
if (config.verbose && attachmentResult.error) console.error(formatDetailedError(attachmentResult.error));
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
if (hasAttachments && latestRevisionId) {
|
|
566
|
-
currentRevisionId = latestRevisionId;
|
|
567
|
-
const pageName = basename(file.localPath, ".md");
|
|
568
|
-
const { content: replacedContent, replaced } = replaceAttachmentLinks(currentContent, file.attachments, pageName);
|
|
569
|
-
if (replaced) {
|
|
570
|
-
const updateResult = await updatePageContent(result.pageId, currentRevisionId, replacedContent);
|
|
571
|
-
if (updateResult.success && updateResult.revisionId) {
|
|
572
|
-
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (attachment links replaced)`);
|
|
573
|
-
currentContent = replacedContent;
|
|
574
|
-
currentRevisionId = updateResult.revisionId;
|
|
575
|
-
} else {
|
|
576
|
-
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update attachment links: ${updateResult.errorMessage || "unknown error"})`);
|
|
577
|
-
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
578
|
-
stats.linkReplacementErrors++;
|
|
579
|
-
}
|
|
628
|
+
let latestRevisionId = result.revisionId;
|
|
629
|
+
for (const attachment of file.attachments) {
|
|
630
|
+
if (attachment.isExternalReference) continue;
|
|
631
|
+
const attachmentResult = await uploadAttachment(attachment, result.pageId, sourceDir);
|
|
632
|
+
if (attachmentResult.success) {
|
|
633
|
+
stats.attachmentsUploaded++;
|
|
634
|
+
console.log(`[SUCCESS] ${attachment.localPath} → ${file.growiPath} (attachment)`);
|
|
635
|
+
if (attachmentResult.attachmentId) {
|
|
636
|
+
attachment.attachmentId = attachmentResult.attachmentId;
|
|
637
|
+
attachmentMap.set(attachment.localPath, attachmentResult.attachmentId);
|
|
580
638
|
}
|
|
639
|
+
if (attachmentResult.revisionId) latestRevisionId = attachmentResult.revisionId;
|
|
640
|
+
} else {
|
|
641
|
+
stats.attachmentErrors++;
|
|
642
|
+
console.error(`[ERROR] ${attachment.localPath} → ${file.growiPath} (${attachmentResult.errorMessage || "failed to upload attachment"})`);
|
|
643
|
+
if (config.verbose && attachmentResult.error) console.error(formatDetailedError(attachmentResult.error));
|
|
581
644
|
}
|
|
582
645
|
}
|
|
583
|
-
if (
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
646
|
+
if (latestRevisionId) pageResults.push({
|
|
647
|
+
file,
|
|
648
|
+
pageId: result.pageId,
|
|
649
|
+
revisionId: latestRevisionId
|
|
650
|
+
});
|
|
651
|
+
} else if (file.attachments.length > 0) {
|
|
652
|
+
for (const attachment of file.attachments) if (!attachment.isExternalReference) {
|
|
653
|
+
console.log(`[SKIP] ${attachment.localPath} → ${file.growiPath} (attachment skipped)`);
|
|
654
|
+
stats.attachmentsSkipped++;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
for (const pageResult of pageResults) {
|
|
659
|
+
const { file, pageId } = pageResult;
|
|
660
|
+
let currentContent = readFileSync(join(sourceDir, file.localPath), "utf-8");
|
|
661
|
+
let currentRevisionId = pageResult.revisionId;
|
|
662
|
+
const pageName = basename(file.localPath, ".md");
|
|
663
|
+
const localAttachments = file.attachments.filter((att) => !att.isExternalReference && att.attachmentId);
|
|
664
|
+
const externalAttachments = file.attachments.filter((att) => att.isExternalReference);
|
|
665
|
+
let hasReplacements = false;
|
|
666
|
+
if (localAttachments.length > 0) {
|
|
667
|
+
const { content: replacedContent, replaced } = replaceAttachmentLinks(currentContent, localAttachments, pageName);
|
|
668
|
+
if (replaced) {
|
|
669
|
+
currentContent = replacedContent;
|
|
670
|
+
hasReplacements = true;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (externalAttachments.length > 0) {
|
|
674
|
+
const { content: replacedContent, replaced } = replaceExternalAttachmentLinks(currentContent, externalAttachments, attachmentMap);
|
|
675
|
+
if (replaced) {
|
|
676
|
+
currentContent = replacedContent;
|
|
677
|
+
hasReplacements = true;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
const { content: linkedContent, replaced: linkReplaced } = replaceMarkdownExtension(currentContent, config.basePath);
|
|
681
|
+
if (linkReplaced) {
|
|
682
|
+
currentContent = linkedContent;
|
|
683
|
+
hasReplacements = true;
|
|
684
|
+
}
|
|
685
|
+
if (hasReplacements) {
|
|
686
|
+
const updateResult = await updatePageContent(pageId, currentRevisionId, currentContent);
|
|
687
|
+
if (updateResult.success && updateResult.revisionId) {
|
|
688
|
+
const replacementTypes = [];
|
|
689
|
+
if (localAttachments.some((att) => att.attachmentId)) replacementTypes.push("attachment links");
|
|
690
|
+
if (externalAttachments.length > 0) replacementTypes.push("external references");
|
|
691
|
+
if (linkReplaced) replacementTypes.push("page links");
|
|
692
|
+
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (${replacementTypes.join(", ")} replaced)`);
|
|
693
|
+
currentRevisionId = updateResult.revisionId;
|
|
694
|
+
} else {
|
|
695
|
+
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update links: ${updateResult.errorMessage || "unknown error"})`);
|
|
696
|
+
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
697
|
+
stats.linkReplacementErrors++;
|
|
596
698
|
}
|
|
597
|
-
} else if (file.attachments.length > 0) for (const attachment of file.attachments) {
|
|
598
|
-
console.log(`[SKIP] ${attachment.localPath} → ${file.growiPath} (attachment skipped)`);
|
|
599
|
-
stats.attachmentsSkipped++;
|
|
600
699
|
}
|
|
601
700
|
}
|
|
602
701
|
return stats;
|