@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 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
  ![Logo](./images/logo.png)
172
174
  ![Screenshot](images/screenshot.png)
175
+ ![Banner](/assets/banner.png)
173
176
  ```
174
177
 
175
- Both `logo.png` and `screenshot.png` will be uploaded as attachments to the `/guide` page, even though they don't follow the `_attachment_` naming convention.
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.1.0";
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: file.content,
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: file.content
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 = /!?\[([^\]]*)\]\((?:<([^>]+)>|((?:[^)\\]+|\\.)*))\)/g;
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 angleBracketPath = match[2];
233
- const regularPath = match[3];
234
- const linkPath = angleBracketPath || regularPath;
243
+ const { anglePath, regularPath } = match.groups;
244
+ const linkPath = anglePath || regularPath;
235
245
  if (!linkPath) continue;
236
- const originalLinkPath = angleBracketPath ? `<${angleBracketPath}>` : regularPath;
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
- const absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), unescapedPath);
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}`.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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.replace(/[.*+?^${}()|[\]\\<>]/g, "\\$&");
347
+ const escapedPath = escapeRegex(linkPath);
330
348
  patterns.push(escapedPath);
331
349
  if (linkPath.startsWith("./")) {
332
- const escapedWithoutDot = linkPath.substring(2).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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, "$1$2");
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 result = await createOrUpdatePage(file, config.update);
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 = file.content;
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onozaty/growi-uploader",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A content uploader for GROWI",
5
5
  "type": "module",
6
6
  "bin": {