diff --git a/README.md b/README.md index 7255ef5..01fa047 100644 --- a/README.md +++ b/README.md @@ -325,7 +325,7 @@ Smartly paste for Markdown. } ] ``` - + You can also use **regex capture groups** in your `match` patterns. Each captured group can be referenced in `targetPath` and `linkPattern` as `$1`, `$2`, etc. This allows you to dynamically create folders or links based on parts of the Markdown filename or path. **Example:** diff --git a/package.json b/package.json index 5f712d8..fc948ef 100644 --- a/package.json +++ b/package.json @@ -120,6 +120,11 @@ "default": "${fileDirname}", "description": "The destination to save image file." }, + "MarkdownPaste.basePath": { + "type": "string", + "default": "", + "description": "[Link Text Control] Sets the reference point for calculating the image URL. 1. If set (e.g., '${fileWorkspaceFolder}'), the link will be transformed into a site-root relative path (e.g., '/asset/img.png'). 2. If left empty, it defaults to a relative path from the current editing file. Note: This only affects the text inserted into Markdown and does not change the physical storage location of the image file." + }, "MarkdownPaste.nameBase": { "type": "string", "scope": "resource", diff --git a/src/paster.ts b/src/paster.ts index c667658..f4d98f6 100644 --- a/src/paster.ts +++ b/src/paster.ts @@ -300,12 +300,34 @@ class Paster { if (!editor) return; const fileUri = editor.document.uri; if (!fileUri) return; - const basePath = path.dirname(fileUri.fsPath); - // Compute the image file path relative to the current file. - let imageFilePath = Paster.encodePath( - path.relative(basePath, pasteImgContext.targetFile.fsPath) + let basePath: string; + + const config = vscode.workspace.getConfiguration("MarkdownPaste"); + const customBasePath = config.get("basePath"); + + if (customBasePath && customBasePath.trim() !== "") { + basePath = path.resolve(Predefine.replacePredefinedVars(customBasePath)); + } else { + // Original behavior + basePath = path.dirname(fileUri.fsPath); + } + + let imageFilePath = path.relative( + basePath, + pasteImgContext.targetFile.fsPath ); + imageFilePath = imageFilePath.replace(/\\/g, "/"); + + if ( + customBasePath && + !path.isAbsolute(imageFilePath) && + !imageFilePath.startsWith("/") + ) { + imageFilePath = "/" + imageFilePath; + } + + imageFilePath = Paster.encodePath(imageFilePath); // Apply any language rules (if configured). const parse_result = Paster.parse_rules(imageFilePath); diff --git a/src/predefine.ts b/src/predefine.ts index 76e8a9d..d428cc8 100644 --- a/src/predefine.ts +++ b/src/predefine.ts @@ -12,18 +12,22 @@ class Predefine { _fileBasenameNoExtension: string; _fileDirname: string; - constructor() { + constructor(fileUri?: vscode.Uri, workspaceFolderUri?: vscode.Uri) { + // prioritize using the passed‑in fileUri; if none is provided, then fall back to the currently active editor (the original logic). let editor = vscode.window.activeTextEditor; - let fileUri = editor && editor.document.uri; - let fileWorkspaceFolderUri = - fileUri && vscode.workspace.getWorkspaceFolder(fileUri); + const targetUri = fileUri ?? editor?.document.uri; + + const targetFolderUri = + workspaceFolderUri ?? + (targetUri + ? vscode.workspace.getWorkspaceFolder(targetUri)?.uri + : undefined) ?? + vscode.workspace.workspaceFolders?.[0]?.uri; + this._workspaceRoot = - (vscode.workspace.workspaceFolders && - vscode.workspace.workspaceFolders[0].uri.fsPath) || - ""; - this._filePath = fileUri && fileUri.fsPath; - this._fileWorkspaceFolder = - (fileWorkspaceFolderUri && fileWorkspaceFolderUri.uri.fsPath) || ""; + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || ""; + this._filePath = targetUri?.fsPath || ""; + this._fileWorkspaceFolder = targetFolderUri?.fsPath || ""; if (this._filePath) { this._fileExtname = path.extname(this._filePath); @@ -33,6 +37,11 @@ class Predefine { ); this._fileBasename = path.basename(this._filePath); this._fileDirname = path.dirname(this._filePath); + } else { + this._fileExtname = ""; + this._fileBasenameNoExtension = ""; + this._fileBasename = ""; + this._fileDirname = ""; } } @@ -52,12 +61,18 @@ class Predefine { return this.workspaceRoot(); } - public filePath() { - return this._filePath; + public filePath(param?: string): string { + if (!param) { + return this._filePath; + } + return this.getSlicedPath(this._filePath, param); } - public fileWorkspaceFolder() { - return this._fileWorkspaceFolder; + public fileWorkspaceFolder(param?: string): string { + if (!param) { + return this._fileWorkspaceFolder; + } + return this.getSlicedPath(this._fileWorkspaceFolder, param); } public fileBasename(): string { @@ -74,11 +89,58 @@ class Predefine { public fileDirname(): string { return this._fileDirname; } + private getSlicedPath(inputPath: string, param: string): string { + const sep = path.sep; + const { root } = path.parse(inputPath); + const body = inputPath.substring(root.length); + const parts = root + ? [root, ...body.split(sep).filter(Boolean)] + : body.split(sep).filter(Boolean); + const slicedParts = this.getArraySlice(parts, param); + return path.join(...slicedParts); + } + /** - * the current opened file's dirname relative to `$fileWorkspaceFolder` + * Support for Python-style slicing, without step slicing. + * Syntax examples: + * + * - ${relativeFileDirname} -> "src/assets/images" + * - ${relativeFileDirname|-1} -> "images" + * - ${relativeFileDirname|0:2} -> "src/assets" + * - ${relativeFileDirname|:-1} -> "src/assets" */ - public relativeFileDirname(): string { - return path.relative(this.fileWorkspaceFolder(), this.fileDirname()); + private getArraySlice(items: T[], param: string): T[] { + if (!param || items.length === 0) return items; + + const cleanParam = param.trim(); + if (!cleanParam) return items; + + if (cleanParam.includes(":")) { + // "start:end" + const [s, e] = cleanParam.split(":").map((p) => p.trim()); + const start = s ? parseInt(s) : 0; + const end = e ? parseInt(e) : undefined; + return items.slice(start, end); + } + + // "index" + const index = parseInt(cleanParam); + if (isNaN(index)) return items; + + return items.slice(index, index === -1 ? undefined : index + 1); + } + + public relativeFileDirname(param?: string): string { + const wsFolder = this.fileWorkspaceFolder(); + const fileDir = this.fileDirname(); + if (!wsFolder || !fileDir) return ""; + + let rawRelative = path.relative(wsFolder, fileDir); + if (rawRelative === ".") rawRelative = ""; + if (rawRelative === "") return ""; + if (!param) return rawRelative; + + return this.getSlicedPath(rawRelative, param); } /** diff --git a/test/suite/extension.test.ts b/test/suite/extension.test.ts index 0a44558..8426a3d 100644 --- a/test/suite/extension.test.ts +++ b/test/suite/extension.test.ts @@ -181,4 +181,120 @@ suite("Extension Tests", () => { ret = Predefine.replaceRegPredefinedVars(str, predefine); assert.strictEqual(ret, ret_expect); }); + + test("Infrastructure: should correctly initialize paths based on platform", () => { + const isWin = process.platform === "win32"; + + const mockFileStr = isWin + ? "C:\\user\\project\\test\\file.ts" + : "/user/project/test/file.ts"; + const mockWsStr = isWin ? "C:\\user\\project" : "/user/project"; + + const predefine = new Predefine( + vscode.Uri.file(mockFileStr), + vscode.Uri.file(mockWsStr) + ); + + console.log(`[Test 1] Platform detected: ${process.platform}`); + + const actualBasename = predefine.fileBasename(); + const actualDirname = predefine.fileDirname(); + + // Validate Basename parsing + if (actualBasename !== "file.ts") { + throw new Error( + `[Infrastructure Error] Basename mismatch! Expected "file.ts", but got "${actualBasename}". ` + + `Check if your mock path format matches the current OS (${process.platform}).` + ); + } + + // Validate Directory Separator consistency + if (isWin && !actualDirname.includes("\\")) { + throw new Error( + `[Infrastructure Error] Win32 validation failed: Dirname should contain backslashes.` + ); + } + + if (!isWin && actualDirname.includes("\\")) { + throw new Error( + `[Infrastructure Error] POSIX validation failed: Dirname should not contain backslashes.` + ); + } + + console.log("Environment path initialization passed!"); + }); + + test("Predefined variables slicing test", () => { + const isWindows = process.platform === "win32"; + const filePathStr = isWindows + ? "C:\\user\\project\\markdown-paste\\test\\suite\\extension.test.ts" + : "/user/project/markdown-paste/test/suite/extension.test.ts"; + const wsPathStr = isWindows + ? "C:\\user\\project\\markdown-paste" + : "/user/project/markdown-paste"; + + const mockFile = vscode.Uri.file(filePathStr); + const mockWs = vscode.Uri.file(wsPathStr); + console.log(`Debug - mockFile: ${mockFile}`); + console.log(`Debug - mockWs: ${mockWs}`); + const predefine = new Predefine(mockFile, mockWs); + let ret_expect = ""; + let str = ""; + let ret = ""; + + // Case 1: Testing negative index to get the last component (the filename) + str = "${filePath|-1}"; + ret = Predefine.replaceRegPredefinedVars(str, predefine); + ret_expect = "extension.test.ts"; + console.log(`Debug - Case 1 filePath[-1]: ${ret}`); + assert.strictEqual( + ret, + ret_expect, + "Case 1: Should return the last part of the path" + ); + + // Case 2: Testing positive index to get the first component + // Note: On Windows, index 0 is usually C:\ + str = "${filePath|0:2}"; + ret = Predefine.replaceRegPredefinedVars(str, predefine); + ret_expect = isWindows ? "c:/user" : "/user"; + console.log(`Debug - Case 2 filePath[0:2]: ${ret}`); + assert.strictEqual( + ret, + ret_expect, + "Should return the first and second part of the path" + ); + + // Case 3: Testing range slicing without step (Python style [:-1]) + // This should return the path components except the last one + str = "${filePath|:-1}"; + ret = Predefine.replaceRegPredefinedVars(str, predefine); + console.log(`Debug - Case 3 filePath[:-1]: ${ret}`); + assert.strictEqual( + ret.includes("extension.test.ts"), + false, + "Should not include the filename" + ); + + // Case 4: Testing full range with specific indices + str = "${filePath|-3:-1}"; + ret = Predefine.replaceRegPredefinedVars(str, predefine); + // .replaceRegPredefinedVars() returns "/" uniformly + ret_expect = isWindows ? "test/suite" : "test/suite"; + console.log(`Debug - Case 4 filePath[-3:-1]: ${ret}`); + assert.strictEqual(ret, ret_expect); + + // Case 5: Folder test + str = "${fileWorkspaceFolder|-1}"; + ret = Predefine.replaceRegPredefinedVars(str, predefine); + ret_expect = "markdown-paste"; + console.log(`Debug - Case 5 fileWorkspaceFolder[-1]: ${ret}`); + assert.strictEqual(ret, ret_expect); + + str = "${relativeFileDirname|-2}"; + ret = Predefine.replaceRegPredefinedVars(str, predefine); + ret_expect = "test"; + console.log(`Debug - Case 5 relativeFileDirname[-2]: ${ret}`); + assert.strictEqual(ret, ret_expect); + }); });