@onozaty/growi-uploader 1.2.0 → 1.4.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.
Files changed (3) hide show
  1. package/README.md +16 -0
  2. package/dist/index.mjs +48 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # growi-uploader
2
2
 
3
+ [![Test](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml/badge.svg)](https://github.com/onozaty/growi-uploader/actions/workflows/test.yaml)
4
+ [![codecov](https://codecov.io/gh/onozaty/growi-uploader/graph/badge.svg?token=X0YN1OP5PB)](https://codecov.io/gh/onozaty/growi-uploader)
3
5
  [![npm version](https://badge.fury.io/js/@onozaty%2Fgrowi-uploader.svg)](https://www.npmjs.com/package/@onozaty/growi-uploader)
4
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
7
 
@@ -178,10 +180,24 @@ images/
178
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.
179
181
 
180
182
  **Path resolution:**
183
+ - Markdown escape sequences are unescaped (`\(` → `(`)
184
+ - URL encoding (percent-encoding) is decoded (`%20` → space, `%E7%94%BB%E5%83%8F` → `画像`)
181
185
  - Relative paths (`./`, `../`, or no prefix): Resolved from the Markdown file's directory
182
186
  - Absolute paths (starting with `/`): Resolved from the source directory root
183
187
  - Example: `/assets/banner.png` → `<source-dir>/assets/banner.png`
184
188
 
189
+ **Supported link formats:**
190
+ ```markdown
191
+ ![Logo](./images/logo.png) # Standard relative path
192
+ ![Logo](images/logo.png) # Relative path without ./
193
+ ![Image](./images/%E7%94%BB%E5%83%8F.png) # 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
+ ![Image](./path/file\\(1\\).png) # Special chars with escaping
197
+ <img src="./images/logo.png" alt="Logo"> # HTML img tag (double quotes)
198
+ <img src='./images/logo.png' alt='Logo'> # HTML img tag (single quotes)
199
+ ```
200
+
185
201
  **Excluded from detection:**
186
202
  - `.md` files (treated as page links)
187
203
  - External URLs (`http://`, `https://`)
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.2.0";
11
+ var version = "1.4.0";
12
12
 
13
13
  //#endregion
14
14
  //#region src/config.ts
@@ -215,6 +215,37 @@ const uploadAttachment = async (attachment, pageId, sourceDir) => {
215
215
  //#endregion
216
216
  //#region src/scanner.ts
217
217
  /**
218
+ * Process a link path and convert it to an AttachmentFile if it exists
219
+ *
220
+ * @param linkPath The link path from markdown
221
+ * @param originalLinkPath The original format of the link (for replacement)
222
+ * @param markdownFilePath Path to the markdown file (relative to sourceDir)
223
+ * @param sourceDir Source directory (absolute path)
224
+ * @returns AttachmentFile if the path resolves to an existing file, null otherwise
225
+ */
226
+ const processLinkPath = (linkPath, originalLinkPath, markdownFilePath, sourceDir) => {
227
+ if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) return null;
228
+ if (linkPath.endsWith(".md")) return null;
229
+ const unescapedPath = linkPath.replace(/\\([\\`*_{}[\]()#+\-.!])/g, "$1");
230
+ let decodedPath;
231
+ try {
232
+ decodedPath = decodeURIComponent(unescapedPath);
233
+ } catch {
234
+ decodedPath = unescapedPath;
235
+ }
236
+ let absolutePath;
237
+ if (linkPath.startsWith("/")) absolutePath = resolve(sourceDir, decodedPath.slice(1));
238
+ else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), decodedPath);
239
+ if (!existsSync(absolutePath)) return null;
240
+ const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
241
+ return {
242
+ localPath: normalizedPath,
243
+ fileName: basename(normalizedPath),
244
+ detectionPattern: "link",
245
+ originalLinkPaths: [originalLinkPath]
246
+ };
247
+ };
248
+ /**
218
249
  * Extract attachment files from markdown links
219
250
  *
220
251
  * Scans markdown content for image and link references, resolves their paths,
@@ -243,21 +274,15 @@ const extractLinkedAttachments = (content, markdownFilePath, sourceDir) => {
243
274
  const { anglePath, regularPath } = match.groups;
244
275
  const linkPath = anglePath || regularPath;
245
276
  if (!linkPath) continue;
246
- const originalLinkPath = anglePath ? `<${anglePath}>` : regularPath;
247
- if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) continue;
248
- if (linkPath.endsWith(".md")) continue;
249
- const unescapedPath = linkPath.replace(/\\([\\`*_{}[\]()#+\-.!])/g, "$1");
250
- let absolutePath;
251
- if (linkPath.startsWith("/")) absolutePath = resolve(sourceDir, unescapedPath.slice(1));
252
- else absolutePath = resolve(dirname(join(sourceDir, markdownFilePath)), unescapedPath);
253
- if (!existsSync(absolutePath)) continue;
254
- const normalizedPath = relative(sourceDir, absolutePath).replace(/\\/g, "/");
255
- attachments.push({
256
- localPath: normalizedPath,
257
- fileName: basename(normalizedPath),
258
- detectionPattern: "link",
259
- originalLinkPaths: [originalLinkPath]
260
- });
277
+ const attachment = processLinkPath(linkPath, anglePath ? `<${anglePath}>` : regularPath, markdownFilePath, sourceDir);
278
+ if (attachment) attachments.push(attachment);
279
+ }
280
+ const imgTagRegex = /<img\s+[^>]*src=(?<quote>["'])(?<src>.*?)\k<quote>[^>]*>/gi;
281
+ while ((match = imgTagRegex.exec(content)) !== null) {
282
+ const { src } = match.groups;
283
+ if (!src) continue;
284
+ const attachment = processLinkPath(src, src, markdownFilePath, sourceDir);
285
+ if (attachment) attachments.push(attachment);
261
286
  }
262
287
  return attachments;
263
288
  };
@@ -343,13 +368,13 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
343
368
  const escapedFileName = escapeRegex(`${pageName}_attachment_${attachment.fileName}`);
344
369
  patterns.push(escapedFileName, `\\./${escapedFileName}`);
345
370
  }
346
- if (attachment.detectionPattern === "link" && attachment.originalLinkPaths) for (const linkPath of attachment.originalLinkPaths) {
371
+ if (attachment.originalLinkPaths) for (const linkPath of attachment.originalLinkPaths) {
347
372
  const escapedPath = escapeRegex(linkPath);
348
373
  patterns.push(escapedPath);
349
374
  if (linkPath.startsWith("./")) {
350
375
  const escapedWithoutDot = escapeRegex(linkPath.substring(2));
351
376
  patterns.push(escapedWithoutDot);
352
- } else if (!linkPath.startsWith("../")) patterns.push(`\\./${escapedPath}`);
377
+ } else if (!linkPath.startsWith("../") && !linkPath.startsWith("<")) patterns.push(`\\./${escapedPath}`);
353
378
  }
354
379
  for (const pattern of patterns) {
355
380
  const imgRegex = new RegExp(`!\\[([^\\]]*)\\]\\(${pattern}\\)`, "g");
@@ -362,6 +387,11 @@ const replaceAttachmentLinks = (markdown, attachments, pageName) => {
362
387
  replaced = true;
363
388
  result = result.replace(linkRegex, `[$1](${growiPath})`);
364
389
  }
390
+ const imgTagRegex = new RegExp(`(<img\\s+[^>]*src=)(["'])${pattern}\\2([^>]*>)`, "gi");
391
+ if (imgTagRegex.test(result)) {
392
+ replaced = true;
393
+ result = result.replace(imgTagRegex, `$1$2${growiPath}$2$3`);
394
+ }
365
395
  }
366
396
  }
367
397
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onozaty/growi-uploader",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "A content uploader for GROWI",
5
5
  "type": "module",
6
6
  "bin": {