Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
30 changes: 26 additions & 4 deletions src/paster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("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);
Expand Down
96 changes: 79 additions & 17 deletions src/predefine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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 = "";
}
}

Expand All @@ -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 {
Expand All @@ -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<T>(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);
}

/**
Expand Down
116 changes: 116 additions & 0 deletions test/suite/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
Loading