@onozaty/growi-uploader 1.1.0 → 1.3.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 +23 -1
- package/dist/index.mjs +53 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# growi-uploader
|
|
2
2
|
|
|
3
|
+
[](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
|
|
4
|
+
[](https://codecov.io/gh/onozaty/growi-uploader)
|
|
3
5
|
[](https://www.npmjs.com/package/@onozaty/growi-uploader)
|
|
4
6
|
[](https://opensource.org/licenses/MIT)
|
|
5
7
|
|
|
@@ -161,6 +163,8 @@ Files referenced in markdown links are automatically detected as attachments:
|
|
|
161
163
|
**Local Directory:**
|
|
162
164
|
```
|
|
163
165
|
guide.md
|
|
166
|
+
assets/
|
|
167
|
+
banner.png
|
|
164
168
|
images/
|
|
165
169
|
logo.png
|
|
166
170
|
screenshot.png
|
|
@@ -170,9 +174,27 @@ images/
|
|
|
170
174
|
```markdown
|
|
171
175
|

|
|
172
176
|

|
|
177
|
+

|
|
173
178
|
```
|
|
174
179
|
|
|
175
|
-
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
**Path resolution:**
|
|
183
|
+
- Markdown escape sequences are unescaped (`\(` → `(`)
|
|
184
|
+
- URL encoding (percent-encoding) is decoded (`%20` → space, `%E7%94%BB%E5%83%8F` → `画像`)
|
|
185
|
+
- Relative paths (`./`, `../`, or no prefix): Resolved from the Markdown file's directory
|
|
186
|
+
- Absolute paths (starting with `/`): Resolved from the source directory root
|
|
187
|
+
- Example: `/assets/banner.png` → `<source-dir>/assets/banner.png`
|
|
188
|
+
|
|
189
|
+
**Supported link formats:**
|
|
190
|
+
```markdown
|
|
191
|
+
 # Standard relative path
|
|
192
|
+
 # Relative path without ./
|
|
193
|
+
 # URL-encoded Japanese filename
|
|
194
|
+
[File](./docs/my%20file.pdf) # URL-encoded space
|
|
195
|
+
[File](<./path/file (1).png>) # Special chars with angle brackets
|
|
196
|
+
.png) # Special chars with escaping
|
|
197
|
+
```
|
|
176
198
|
|
|
177
199
|
**Excluded from detection:**
|
|
178
200
|
- `.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.3.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,35 @@ 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 decodedPath;
|
|
251
|
+
try {
|
|
252
|
+
decodedPath = decodeURIComponent(unescapedPath);
|
|
253
|
+
} catch {
|
|
254
|
+
decodedPath = unescapedPath;
|
|
255
|
+
}
|
|
256
|
+
let absolutePath;
|
|
257
|
+
if (linkPath.startsWith("/")) absolutePath = resolve(sourceDir, decodedPath.slice(1));
|
|
258
|
+
else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), decodedPath);
|
|
242
259
|
if (!existsSync(absolutePath)) continue;
|
|
243
260
|
const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
|
|
244
261
|
attachments.push({
|
|
@@ -294,7 +311,6 @@ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
|
294
311
|
return {
|
|
295
312
|
localPath: file,
|
|
296
313
|
growiPath: growiPath.startsWith("/") ? growiPath : `/${growiPath}`,
|
|
297
|
-
content,
|
|
298
314
|
attachments
|
|
299
315
|
};
|
|
300
316
|
}));
|
|
@@ -303,6 +319,14 @@ const scanMarkdownFiles = async (sourceDir, basePath = "/") => {
|
|
|
303
319
|
//#endregion
|
|
304
320
|
//#region src/markdown.ts
|
|
305
321
|
/**
|
|
322
|
+
* Escape special regex characters for safe use in RegExp
|
|
323
|
+
* Escapes: . * + ? ^ $ { } ( ) | [ ] \ < >
|
|
324
|
+
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions#escaping
|
|
325
|
+
*/
|
|
326
|
+
const escapeRegex = (str) => {
|
|
327
|
+
return str.replace(/[.*+?^${}()|[\]\\<>]/g, "\\$&");
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
306
330
|
* Replace attachment links in Markdown content with GROWI format
|
|
307
331
|
*
|
|
308
332
|
* Supports two detection patterns:
|
|
@@ -322,16 +346,16 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
|
322
346
|
const growiPath = `/attachment/${attachment.attachmentId}`;
|
|
323
347
|
const patterns = [];
|
|
324
348
|
if (attachment.detectionPattern === "naming") {
|
|
325
|
-
const escapedFileName = `${pageName}_attachment_${attachment.fileName}
|
|
349
|
+
const escapedFileName = escapeRegex(`${pageName}_attachment_${attachment.fileName}`);
|
|
326
350
|
patterns.push(escapedFileName, `\\./${escapedFileName}`);
|
|
327
351
|
}
|
|
328
|
-
if (attachment.
|
|
329
|
-
const escapedPath = linkPath
|
|
352
|
+
if (attachment.originalLinkPaths) for (const linkPath of attachment.originalLinkPaths) {
|
|
353
|
+
const escapedPath = escapeRegex(linkPath);
|
|
330
354
|
patterns.push(escapedPath);
|
|
331
355
|
if (linkPath.startsWith("./")) {
|
|
332
|
-
const escapedWithoutDot = linkPath.substring(2)
|
|
356
|
+
const escapedWithoutDot = escapeRegex(linkPath.substring(2));
|
|
333
357
|
patterns.push(escapedWithoutDot);
|
|
334
|
-
} else if (!linkPath.startsWith("../")) patterns.push(`\\./${escapedPath}`);
|
|
358
|
+
} else if (!linkPath.startsWith("../") && !linkPath.startsWith("<")) patterns.push(`\\./${escapedPath}`);
|
|
335
359
|
}
|
|
336
360
|
for (const pattern of patterns) {
|
|
337
361
|
const imgRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${pattern}\\)`, "g");
|
|
@@ -355,18 +379,24 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
|
|
|
355
379
|
* Replace .md extension in page links with GROWI format
|
|
356
380
|
*
|
|
357
381
|
* Converts Markdown page links to GROWI-compatible format by removing .md extension.
|
|
382
|
+
* For absolute paths (starting with /), prepends basePath to the link.
|
|
358
383
|
* External URLs (http://, https://) are excluded from replacement.
|
|
359
384
|
*
|
|
360
385
|
* Supported patterns:
|
|
361
386
|
* - Relative path: [text](./page.md) → [text](./page)
|
|
362
387
|
* - Filename only: [text](page.md) → [text](page)
|
|
388
|
+
* - Absolute path: [text](/docs/page.md) → [text](/basePath/docs/page)
|
|
363
389
|
* - With anchor: [text](./page.md#section) → [text](./page#section)
|
|
364
390
|
*
|
|
365
391
|
* @param markdown Original Markdown content
|
|
392
|
+
* @param basePath Base path for GROWI pages (default: "/")
|
|
366
393
|
* @returns Object with replaced content and whether any replacement occurred
|
|
367
394
|
*/
|
|
368
|
-
const replaceMarkdownExtension = (markdown) => {
|
|
369
|
-
const result = markdown.replace(/(\[[^\]]*\]\((?!https?:\/\/)[^)]*?)\.md((?:#[^)]*)?\))/g,
|
|
395
|
+
const replaceMarkdownExtension = (markdown, basePath = "/") => {
|
|
396
|
+
const result = markdown.replace(/(\[[^\]]*\]\((?!https?:\/\/))([^)]*?)\.md((?:#[^)]*)?\))/g, (match, prefix, path, suffix) => {
|
|
397
|
+
if (path.startsWith("/") && basePath !== "/") return `${prefix}${basePath.endsWith("/") ? basePath.slice(0, -1) : basePath}${path}${suffix}`;
|
|
398
|
+
return `${prefix}${path}${suffix}`;
|
|
399
|
+
});
|
|
370
400
|
return {
|
|
371
401
|
content: result,
|
|
372
402
|
replaced: result !== markdown
|
|
@@ -401,7 +431,8 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
401
431
|
linkReplacementErrors: 0
|
|
402
432
|
};
|
|
403
433
|
for (const file of files) {
|
|
404
|
-
const
|
|
434
|
+
const content = readFileSync(join(sourceDir, file.localPath), "utf-8");
|
|
435
|
+
const result = await createOrUpdatePage(file, content, config.update);
|
|
405
436
|
if (result.action === "created") {
|
|
406
437
|
stats.pagesCreated++;
|
|
407
438
|
console.log(`[SUCCESS] ${file.localPath} → ${file.growiPath} (created)`);
|
|
@@ -416,7 +447,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
416
447
|
console.error(`[ERROR] ${file.localPath} → ${file.growiPath} (${result.errorMessage || "unknown error"})`);
|
|
417
448
|
}
|
|
418
449
|
if (result.pageId && (result.action === "created" || result.action === "updated")) {
|
|
419
|
-
let currentContent =
|
|
450
|
+
let currentContent = content;
|
|
420
451
|
let currentRevisionId = result.revisionId;
|
|
421
452
|
if (file.attachments.length > 0) {
|
|
422
453
|
let hasAttachments = false;
|
|
@@ -454,7 +485,7 @@ const uploadFiles = async (files, sourceDir, config) => {
|
|
|
454
485
|
}
|
|
455
486
|
}
|
|
456
487
|
if (currentRevisionId) {
|
|
457
|
-
const { content: linkedContent, replaced: linkReplaced } = replaceMarkdownExtension(currentContent);
|
|
488
|
+
const { content: linkedContent, replaced: linkReplaced } = replaceMarkdownExtension(currentContent, config.basePath);
|
|
458
489
|
if (linkReplaced) {
|
|
459
490
|
const updateResult = await updatePageContent(result.pageId, currentRevisionId, linkedContent);
|
|
460
491
|
if (updateResult.success && updateResult.revisionId) {
|