@onozaty/growi-uploader 1.1.0 → 1.2.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.md +9 -1
- package/dist/index.mjs +45 -20
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -161,6 +161,8 @@ Files referenced in markdown links are automatically detected as attachments:
|
|
|
161
161
|
**Local Directory:**
|
|
162
162
|
```
|
|
163
163
|
guide.md
|
|
164
|
+
assets/
|
|
165
|
+
banner.png
|
|
164
166
|
images/
|
|
165
167
|
logo.png
|
|
166
168
|
screenshot.png
|
|
@@ -170,9 +172,15 @@ images/
|
|
|
170
172
|
```markdown
|
|
171
173
|

|
|
172
174
|

|
|
175
|
+

|
|
173
176
|
```
|
|
174
177
|
|
|
175
|
-
|
|
178
|
+
All referenced files (`logo.png`, `screenshot.png`, and `banner.png`) will be uploaded as attachments to the `/guide` page, even though they don't follow the `_attachment_` naming convention.
|
|
179
|
+
|
|
180
|
+
**Path resolution:**
|
|
181
|
+
- Relative paths (`./`, `../`, or no prefix): Resolved from the Markdown file's directory
|
|
182
|
+
- Absolute paths (starting with `/`): Resolved from the source directory root
|
|
183
|
+
- Example: `/assets/banner.png` → `<source-dir>/assets/banner.png`
|
|
176
184
|
|
|
177
185
|
**Excluded from detection:**
|
|
178
186
|
- `.md` files (treated as page links)
|
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.2.0";
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
14
|
//#region src/config.ts
|
|
@@ -103,10 +103,11 @@ const formatErrorMessage = (error) => {
|
|
|
103
103
|
* Create a new page or update an existing page in GROWI
|
|
104
104
|
*
|
|
105
105
|
* @param file Markdown file to upload
|
|
106
|
+
* @param content Markdown content
|
|
106
107
|
* @param shouldUpdate If true, update existing pages; if false, skip existing pages
|
|
107
108
|
* @returns Result containing page ID, revision ID, and action taken
|
|
108
109
|
*/
|
|
109
|
-
const createOrUpdatePage = async (file, shouldUpdate) => {
|
|
110
|
+
const createOrUpdatePage = async (file, content, shouldUpdate) => {
|
|
110
111
|
try {
|
|
111
112
|
const actualResponse = (await getPage({ path: file.growiPath })).data;
|
|
112
113
|
if (actualResponse.page) {
|
|
@@ -120,7 +121,7 @@ const createOrUpdatePage = async (file, shouldUpdate) => {
|
|
|
120
121
|
return {
|
|
121
122
|
pageId,
|
|
122
123
|
revisionId: (await putPage({
|
|
123
|
-
body:
|
|
124
|
+
body: content,
|
|
124
125
|
pageId,
|
|
125
126
|
revisionId
|
|
126
127
|
})).data.page?.revision,
|
|
@@ -138,7 +139,7 @@ const createOrUpdatePage = async (file, shouldUpdate) => {
|
|
|
138
139
|
try {
|
|
139
140
|
const response = await postPage({
|
|
140
141
|
path: file.growiPath,
|
|
141
|
-
body:
|
|
142
|
+
body: content
|
|
142
143
|
});
|
|
143
144
|
return {
|
|
144
145
|
pageId: response.data.page?._id,
|
|
@@ -226,19 +227,29 @@ const uploadAttachment = async (attachment, pageId, sourceDir) => {
|
|
|
226
227
|
*/
|
|
227
228
|
const extractLinkedAttachments = (content, markdownFilePath, sourceDir) => {
|
|
228
229
|
const attachments = [];
|
|
229
|
-
const linkRegex =
|
|
230
|
+
const linkRegex = new RegExp([
|
|
231
|
+
"!?",
|
|
232
|
+
"\\[(?<alt>[^\\]]*)\\]",
|
|
233
|
+
"\\(",
|
|
234
|
+
"(?:",
|
|
235
|
+
"<(?<anglePath>[^>]+)>",
|
|
236
|
+
"|",
|
|
237
|
+
"(?<regularPath>(?:[^)\\\\]+|\\\\.)*)",
|
|
238
|
+
")",
|
|
239
|
+
"\\)"
|
|
240
|
+
].join(""), "g");
|
|
230
241
|
let match;
|
|
231
242
|
while ((match = linkRegex.exec(content)) !== null) {
|
|
232
|
-
const
|
|
233
|
-
const
|
|
234
|
-
const linkPath = angleBracketPath || regularPath;
|
|
243
|
+
const { anglePath, regularPath } = match.groups;
|
|
244
|
+
const linkPath = anglePath || regularPath;
|
|
235
245
|
if (!linkPath) continue;
|
|
236
|
-
const originalLinkPath =
|
|
246
|
+
const originalLinkPath = anglePath ? `<${anglePath}>` : regularPath;
|
|
237
247
|
if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) continue;
|
|
238
248
|
if (linkPath.endsWith(".md")) continue;
|
|
239
|
-
if (linkPath.startsWith("/")) continue;
|
|
240
249
|
const unescapedPath = linkPath.replace(/\\([\\`*_{}[\]()#+\-.!])/g, "$1");
|
|
241
|
-
|
|
250
|
+
let absolutePath;
|
|
251
|
+
if (linkPath.startsWith("/")) absolutePath = resolve(sourceDir, unescapedPath.slice(1));
|
|
252
|
+
else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), unescapedPath);
|
|
242
253
|
if (!existsSync(absolutePath)) continue;
|
|
243
254
|
const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
|
|
244
255
|
attachments.push({
|
|
@@ -294,7 +305,6 @@ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
|
294
305
|
return {
|
|
295
306
|
localPath: file,
|
|
296
307
|
growiPath: growiPath.startsWith("/") ? growiPath : `/${growiPath}`,
|
|
297
|
-
content,
|
|
298
308
|
attachments
|
|
299
309
|
};
|
|
300
310
|
}));
|
|
@@ -303,6 +313,14 @@ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
|
303
313
|
//#endregion
|
|
304
314
|
//#region src/markdown.ts
|
|
305
315
|
/**
|
|
316
|
+
* Escape special regex characters for safe use in RegExp
|
|
317
|
+
* Escapes: . * + ? ^ $ { } ( ) | [ ] \ < >
|
|
318
|
+
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
|
|
319
|
+
*/
|
|
320
|
+
const escapeRegex = (str) => {
|
|
321
|
+
return str.replace(/[.*+?^${}()|[\]\\<>]/g, "\\$&");
|
|
322
|
+
};
|
|
323
|
+
/**
|
|
306
324
|
* Replace attachment links in Markdown content with GROWI format
|
|
307
325
|
*
|
|
308
326
|
* Supports two detection patterns:
|
|
@@ -322,14 +340,14 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
|
322
340
|
const growiPath = `/attachment/${attachment.attachmentId}`;
|
|
323
341
|
const patterns = [];
|
|
324
342
|
if (attachment.detectionPattern === "naming") {
|
|
325
|
-
const escapedFileName = `${pageName}_attachment_${attachment.fileName}
|
|
343
|
+
const escapedFileName = escapeRegex(`${pageName}_attachment_${attachment.fileName}`);
|
|
326
344
|
patterns.push(escapedFileName, `\\./${escapedFileName}`);
|
|
327
345
|
}
|
|
328
346
|
if (attachment.detectionPattern === "link" && attachment.originalLinkPaths) for (const linkPath of attachment.originalLinkPaths) {
|
|
329
|
-
const escapedPath = linkPath
|
|
347
|
+
const escapedPath = escapeRegex(linkPath);
|
|
330
348
|
patterns.push(escapedPath);
|
|
331
349
|
if (linkPath.startsWith("./")) {
|
|
332
|
-
const escapedWithoutDot = linkPath.substring(2)
|
|
350
|
+
const escapedWithoutDot = escapeRegex(linkPath.substring(2));
|
|
333
351
|
patterns.push(escapedWithoutDot);
|
|
334
352
|
} else if (!linkPath.startsWith("../")) patterns.push(`\\./${escapedPath}`);
|
|
335
353
|
}
|
|
@@ -355,18 +373,24 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
|
355
373
|
* Replace .md extension in page links with GROWI format
|
|
356
374
|
*
|
|
357
375
|
* Converts Markdown page links to GROWI-compatible format by removing .md extension.
|
|
376
|
+
* For absolute paths (starting with /), prepends basePath to the link.
|
|
358
377
|
* External URLs (http://, https://) are excluded from replacement.
|
|
359
378
|
*
|
|
360
379
|
* Supported patterns:
|
|
361
380
|
* - Relative path: [text](./page.md) → [text](./page)
|
|
362
381
|
* - Filename only: [text](page.md) → [text](page)
|
|
382
|
+
* - Absolute path: [text](/docs/page.md) → [text](/basePath/docs/page)
|
|
363
383
|
* - With anchor: [text](./page.md#section) → [text](./page#section)
|
|
364
384
|
*
|
|
365
385
|
* @param markdown Original Markdown content
|
|
386
|
+
* @param basePath Base path for GROWI pages (default: "/")
|
|
366
387
|
* @returns Object with replaced content and whether any replacement occurred
|
|
367
388
|
*/
|
|
368
|
-
const replaceMarkdownExtension = (markdown) => {
|
|
369
|
-
const result = markdown.replace(/(\[[^\]]*\]\((?!https?:\/\/)[^)]*?)\.md((?:#[^)]*)?\))/g,
|
|
389
|
+
const replaceMarkdownExtension = (markdown, basePath = "/") => {
|
|
390
|
+
const result = markdown.replace(/(\[[^\]]*\]\((?!https?:\/\/))([^)]*?)\.md((?:#[^)]*)?\))/g, (match, prefix, path, suffix) => {
|
|
391
|
+
if (path.startsWith("/") && basePath !== "/") return `${prefix}${basePath.endsWith("/") ? basePath.slice(0, -1) : basePath}${path}${suffix}`;
|
|
392
|
+
return `${prefix}${path}${suffix}`;
|
|
393
|
+
});
|
|
370
394
|
return {
|
|
371
395
|
content: result,
|
|
372
396
|
replaced: result !== markdown
|
|
@@ -401,7 +425,8 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
401
425
|
linkReplacementErrors: 0
|
|
402
426
|
};
|
|
403
427
|
for (const file of files) {
|
|
404
|
-
const
|
|
428
|
+
const content = readFileSync(join(sourceDir, file.localPath), "utf-8");
|
|
429
|
+
const result = await createOrUpdatePage(file, content, config.update);
|
|
405
430
|
if (result.action === "created") {
|
|
406
431
|
stats.pagesCreated++;
|
|
407
432
|
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (created)`);
|
|
@@ -416,7 +441,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
416
441
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${result.errorMessage || "unknown error"})`);
|
|
417
442
|
}
|
|
418
443
|
if (result.pageId && (result.action === "created" || result.action === "updated")) {
|
|
419
|
-
let currentContent =
|
|
444
|
+
let currentContent = content;
|
|
420
445
|
let currentRevisionId = result.revisionId;
|
|
421
446
|
if (file.attachments.length > 0) {
|
|
422
447
|
let hasAttachments = false;
|
|
@@ -454,7 +479,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
454
479
|
}
|
|
455
480
|
}
|
|
456
481
|
if (currentRevisionId) {
|
|
457
|
-
const { content: linkedContent, replaced: linkReplaced } = replaceMarkdownExtension(currentContent);
|
|
482
|
+
const { content: linkedContent, replaced: linkReplaced } = replaceMarkdownExtension(currentContent, config.basePath);
|
|
458
483
|
if (linkReplaced) {
|
|
459
484
|
const updateResult = await updatePageContent(result.pageId, currentRevisionId, linkedContent);
|
|
460
485
|
if (updateResult.success && updateResult.revisionId) {
|