@onozaty/growi-uploader 1.6.0 → 1.7.0
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 +2 -2
- package/README.md +2 -2
- package/dist/index.mjs +184 -83
- package/package.json +2 -3
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
|
## 機能
|
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
|
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.0";
|
|
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,6 +606,8 @@ 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
612
|
const content = readFileSync(join(sourceDir, file.localPath), "utf-8");
|
|
528
613
|
const result = await createOrUpdatePage(file, content, config.update);
|
|
@@ -541,62 +626,78 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
541
626
|
if (config.verbose && result.error) console.error(formatDetailedError(result.error));
|
|
542
627
|
}
|
|
543
628
|
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
|
-
}
|
|
629
|
+
let latestRevisionId = result.revisionId;
|
|
630
|
+
for (const attachment of file.attachments) {
|
|
631
|
+
if (attachment.isExternalReference) continue;
|
|
632
|
+
const attachmentResult = await uploadAttachment(attachment, result.pageId, sourceDir);
|
|
633
|
+
if (attachmentResult.success) {
|
|
634
|
+
stats.attachmentsUploaded++;
|
|
635
|
+
console.log(`[SUCCESS] ${attachment.localPath} → ${file.growiPath} (attachment)`);
|
|
636
|
+
if (attachmentResult.attachmentId) {
|
|
637
|
+
attachment.attachmentId = attachmentResult.attachmentId;
|
|
638
|
+
attachmentMap.set(attachment.localPath, attachmentResult.attachmentId);
|
|
580
639
|
}
|
|
640
|
+
if (attachmentResult.revisionId) latestRevisionId = attachmentResult.revisionId;
|
|
641
|
+
} else {
|
|
642
|
+
stats.attachmentErrors++;
|
|
643
|
+
console.error(`[ERROR] ${attachment.localPath} → ${file.growiPath} (${attachmentResult.errorMessage || "failed to upload attachment"})`);
|
|
644
|
+
if (config.verbose && attachmentResult.error) console.error(formatDetailedError(attachmentResult.error));
|
|
581
645
|
}
|
|
582
646
|
}
|
|
583
|
-
if (
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
647
|
+
if (latestRevisionId) pageResults.push({
|
|
648
|
+
file,
|
|
649
|
+
pageId: result.pageId,
|
|
650
|
+
revisionId: latestRevisionId,
|
|
651
|
+
content
|
|
652
|
+
});
|
|
653
|
+
} else if (file.attachments.length > 0) {
|
|
654
|
+
for (const attachment of file.attachments) if (!attachment.isExternalReference) {
|
|
655
|
+
console.log(`[SKIP] ${attachment.localPath} → ${file.growiPath} (attachment skipped)`);
|
|
656
|
+
stats.attachmentsSkipped++;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
for (const pageResult of pageResults) {
|
|
661
|
+
const { file, pageId, content } = pageResult;
|
|
662
|
+
let currentContent = content;
|
|
663
|
+
let currentRevisionId = pageResult.revisionId;
|
|
664
|
+
const pageName = basename(file.localPath, ".md");
|
|
665
|
+
const localAttachments = file.attachments.filter((att) => !att.isExternalReference && att.attachmentId);
|
|
666
|
+
const externalAttachments = file.attachments.filter((att) => att.isExternalReference);
|
|
667
|
+
let hasReplacements = false;
|
|
668
|
+
if (localAttachments.length > 0) {
|
|
669
|
+
const { content: replacedContent, replaced } = replaceAttachmentLinks(currentContent, localAttachments, pageName);
|
|
670
|
+
if (replaced) {
|
|
671
|
+
currentContent = replacedContent;
|
|
672
|
+
hasReplacements = true;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (externalAttachments.length > 0) {
|
|
676
|
+
const { content: replacedContent, replaced } = replaceExternalAttachmentLinks(currentContent, externalAttachments, attachmentMap);
|
|
677
|
+
if (replaced) {
|
|
678
|
+
currentContent = replacedContent;
|
|
679
|
+
hasReplacements = true;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const { content: linkedContent, replaced: linkReplaced } = replaceMarkdownExtension(currentContent, config.basePath);
|
|
683
|
+
if (linkReplaced) {
|
|
684
|
+
currentContent = linkedContent;
|
|
685
|
+
hasReplacements = true;
|
|
686
|
+
}
|
|
687
|
+
if (hasReplacements) {
|
|
688
|
+
const updateResult = await updatePageContent(pageId, currentRevisionId, currentContent);
|
|
689
|
+
if (updateResult.success && updateResult.revisionId) {
|
|
690
|
+
const replacementTypes = [];
|
|
691
|
+
if (localAttachments.some((att) => att.attachmentId)) replacementTypes.push("attachment links");
|
|
692
|
+
if (externalAttachments.length > 0) replacementTypes.push("external references");
|
|
693
|
+
if (linkReplaced) replacementTypes.push("page links");
|
|
694
|
+
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (${replacementTypes.join(", ")} replaced)`);
|
|
695
|
+
currentRevisionId = updateResult.revisionId;
|
|
696
|
+
} else {
|
|
697
|
+
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (failed to update links: ${updateResult.errorMessage || "unknown error"})`);
|
|
698
|
+
if (config.verbose && updateResult.error) console.error(formatDetailedError(updateResult.error));
|
|
699
|
+
stats.linkReplacementErrors++;
|
|
596
700
|
}
|
|
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
701
|
}
|
|
601
702
|
}
|
|
602
703
|
return stats;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onozaty/growi-uploader",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "A content uploader for GROWI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
"dist/**/*"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "
|
|
14
|
-
"prepublishOnly": "pnpm run build",
|
|
13
|
+
"build": "tsdown",
|
|
15
14
|
"dev": "tsx src/index.ts",
|
|
16
15
|
"gen": "orval",
|
|
17
16
|
"lint": "eslint .",
|