From a23c224b9f000751397019d86ecd63ff640f449a Mon Sep 17 00:00:00 2001 From: JayBridge <12310903@mail.sustech.edu.cn> Date: Sun, 1 Feb 2026 13:40:11 +0800 Subject: [PATCH 1/4] chore: update esbuild to version 0.27.2 and bump package version to 1.1.5 --- package-lock.json | 322 ++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 198 insertions(+), 126 deletions(-) diff --git a/package-lock.json b/package-lock.json index edbfa66..6c21c1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "obsidian-csv", - "version": "1.1.4", + "version": "1.1.5", "license": "MIT", "dependencies": { "@codemirror/commands": "^6.8.1", @@ -18,7 +18,7 @@ "@types/node": "^16.11.6", "@types/papaparse": "^5.3.7", "builtin-modules": "^3.3.0", - "esbuild": "0.17.3", + "esbuild": "^0.27.2", "jest": "^29.7.0", "obsidian": "latest", "ts-jest": "^29.1.1", @@ -572,10 +572,27 @@ "w3c-keyname": "^2.2.4" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/android-arm": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.3.tgz", - "integrity": "sha512-1Mlz934GvbgdDmt26rTLmf03cAgLg5HyOgJN+ZGCeP3Q9ynYTNMn2/LQxIl7Uy+o4K6Rfi2OuLsr12JQQR8gNg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -586,13 +603,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.3.tgz", - "integrity": "sha512-XvJsYo3dO3Pi4kpalkyMvfQsjxPWHYjoX4MDiB/FUM4YMfWcXa5l4VCwFWVYI1+92yxqjuqrhNg0CZg3gSouyQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -603,13 +620,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.3.tgz", - "integrity": "sha512-nuV2CmLS07Gqh5/GrZLuqkU9Bm6H6vcCspM+zjp9TdQlxJtIe+qqEXQChmfc7nWdyr/yz3h45Utk1tUn8Cz5+A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -620,13 +637,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.3.tgz", - "integrity": "sha512-01Hxaaat6m0Xp9AXGM8mjFtqqwDjzlMP0eQq9zll9U85ttVALGCGDuEvra5Feu/NbP5AEP1MaopPwzsTcUq1cw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -637,13 +654,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.3.tgz", - "integrity": "sha512-Eo2gq0Q/er2muf8Z83X21UFoB7EU6/m3GNKvrhACJkjVThd0uA+8RfKpfNhuMCl1bKRfBzKOk6xaYKQZ4lZqvA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -654,13 +671,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.3.tgz", - "integrity": "sha512-CN62ESxaquP61n1ZjQP/jZte8CE09M6kNn3baos2SeUfdVBkWN5n6vGp2iKyb/bm/x4JQzEvJgRHLGd5F5b81w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -671,13 +688,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.3.tgz", - "integrity": "sha512-feq+K8TxIznZE+zhdVurF3WNJ/Sa35dQNYbaqM/wsCbWdzXr5lyq+AaTUSER2cUR+SXPnd/EY75EPRjf4s1SLg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -688,13 +705,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.3.tgz", - "integrity": "sha512-CLP3EgyNuPcg2cshbwkqYy5bbAgK+VhyfMU7oIYyn+x4Y67xb5C5ylxsNUjRmr8BX+MW3YhVNm6Lq6FKtRTWHQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -705,13 +722,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.3.tgz", - "integrity": "sha512-JHeZXD4auLYBnrKn6JYJ0o5nWJI9PhChA/Nt0G4MvLaMrvXuWnY93R3a7PiXeJQphpL1nYsaMcoV2QtuvRnF/g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -722,13 +739,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.3.tgz", - "integrity": "sha512-FyXlD2ZjZqTFh0sOQxFDiWG1uQUEOLbEh9gKN/7pFxck5Vw0qjWSDqbn6C10GAa1rXJpwsntHcmLqydY9ST9ZA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -739,13 +756,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.3.tgz", - "integrity": "sha512-OrDGMvDBI2g7s04J8dh8/I7eSO+/E7nMDT2Z5IruBfUO/RiigF1OF6xoH33Dn4W/OwAWSUf1s2nXamb28ZklTA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -756,13 +773,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.3.tgz", - "integrity": "sha512-DcnUpXnVCJvmv0TzuLwKBC2nsQHle8EIiAJiJ+PipEVC16wHXaPEKP0EqN8WnBe0TPvMITOUlP2aiL5YMld+CQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -773,13 +790,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.3.tgz", - "integrity": "sha512-BDYf/l1WVhWE+FHAW3FzZPtVlk9QsrwsxGzABmN4g8bTjmhazsId3h127pliDRRu5674k1Y2RWejbpN46N9ZhQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -790,13 +807,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.3.tgz", - "integrity": "sha512-WViAxWYMRIi+prTJTyV1wnqd2mS2cPqJlN85oscVhXdb/ZTFJdrpaqm/uDsZPGKHtbg5TuRX/ymKdOSk41YZow==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -807,13 +824,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.3.tgz", - "integrity": "sha512-Iw8lkNHUC4oGP1O/KhumcVy77u2s6+KUjieUqzEU3XuWJqZ+AY7uVMrrCbAiwWTkpQHkr00BuXH5RpC6Sb/7Ug==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -824,13 +841,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.3.tgz", - "integrity": "sha512-0AGkWQMzeoeAtXQRNB3s4J1/T2XbigM2/Mn2yU1tQSmQRmHIZdkGbVq2A3aDdNslPyhb9/lH0S5GMTZ4xsjBqg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -841,13 +858,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.3.tgz", - "integrity": "sha512-4+rR/WHOxIVh53UIQIICryjdoKdHsFZFD4zLSonJ9RRw7bhKzVyXbnRPsWSfwybYqw9sB7ots/SYyufL1mBpEg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -858,13 +892,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.3.tgz", - "integrity": "sha512-cVpWnkx9IYg99EjGxa5Gc0XmqumtAwK3aoz7O4Dii2vko+qXbkHoujWA68cqXjhh6TsLaQelfDO4MVnyr+ODeA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -875,13 +926,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.3.tgz", - "integrity": "sha512-RxmhKLbTCDAY2xOfrww6ieIZkZF+KBqG7S2Ako2SljKXRFi+0863PspK74QQ7JpmWwncChY25JTJSbVBYGQk2Q==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -892,13 +960,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.3.tgz", - "integrity": "sha512-0r36VeEJ4efwmofxVJRXDjVRP2jTmv877zc+i+Pc7MNsIr38NfsjkQj23AfF7l0WbB+RQ7VUb+LDiqC/KY/M/A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -909,13 +977,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.3.tgz", - "integrity": "sha512-wgO6rc7uGStH22nur4aLFcq7Wh86bE9cOFmfTr/yxN3BXvDEdCSXyKkO+U5JIt53eTOgC47v9k/C1bITWL/Teg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -926,13 +994,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.3.tgz", - "integrity": "sha512-FdVl64OIuiKjgXBjwZaJLKp0eaEckifbhn10dXWhysMJkWblg3OEEGKSIyhiD5RSgAya8WzP3DNkngtIg3Nt7g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -943,7 +1011,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@istanbuljs/load-nyc-config": { @@ -1733,9 +1801,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { @@ -2144,9 +2212,9 @@ } }, "node_modules/esbuild": { - "version": "0.17.3", - "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.3.tgz", - "integrity": "sha512-9n3AsBRe6sIyOc6kmoXg2ypCLgf3eZSraWFRpnkto+svt8cZNuKTkb1bhQcitBcvIqjNiK7K0J3KPmwGSfkA8g==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2154,31 +2222,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/android-arm": "0.17.3", - "@esbuild/android-arm64": "0.17.3", - "@esbuild/android-x64": "0.17.3", - "@esbuild/darwin-arm64": "0.17.3", - "@esbuild/darwin-x64": "0.17.3", - "@esbuild/freebsd-arm64": "0.17.3", - "@esbuild/freebsd-x64": "0.17.3", - "@esbuild/linux-arm": "0.17.3", - "@esbuild/linux-arm64": "0.17.3", - "@esbuild/linux-ia32": "0.17.3", - "@esbuild/linux-loong64": "0.17.3", - "@esbuild/linux-mips64el": "0.17.3", - "@esbuild/linux-ppc64": "0.17.3", - "@esbuild/linux-riscv64": "0.17.3", - "@esbuild/linux-s390x": "0.17.3", - "@esbuild/linux-x64": "0.17.3", - "@esbuild/netbsd-x64": "0.17.3", - "@esbuild/openbsd-x64": "0.17.3", - "@esbuild/sunos-x64": "0.17.3", - "@esbuild/win32-arm64": "0.17.3", - "@esbuild/win32-ia32": "0.17.3", - "@esbuild/win32-x64": "0.17.3" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -2293,9 +2365,9 @@ } }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3325,9 +3397,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f48a63e..ee4c5c2 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "@types/node": "^16.11.6", "@types/papaparse": "^5.3.7", "builtin-modules": "^3.3.0", - "esbuild": "0.17.3", + "esbuild": "^0.27.2", "jest": "^29.7.0", "obsidian": "latest", "ts-jest": "^29.1.1", From 672d162195190e9d66c006f965ed770b6a9c3a72 Mon Sep 17 00:00:00 2001 From: JayBridge <12310903@mail.sustech.edu.cn> Date: Sun, 1 Feb 2026 13:53:34 +0800 Subject: [PATCH 2/4] feat: add clickable URLs feature in CSV cells - Implemented automatic detection and rendering of plain-text URLs as clickable links in CSV cells. - Added functionality to toggle between display and edit modes for URL cells. - Updated styles for URL display and interaction. - Created utility functions for URL detection and parsing. - Added tests for URL detection and parsing functionality. --- README.md | 1 + README_zh.md | 1 + docs/CLICKABLE_URLS.md | 100 +++++++++++++++++++++++++++++++ src/utils/url-utils.ts | 124 +++++++++++++++++++++++++++++++++++++++ src/view/table-render.ts | 84 +++++++++++++++++++++++++- styles.css | 68 +++++++++++++++++++++ test/url-test-sample.csv | 12 ++++ test/url-utils.test.ts | 105 +++++++++++++++++++++++++++++++++ 8 files changed, 494 insertions(+), 1 deletion(-) create mode 100644 docs/CLICKABLE_URLS.md create mode 100644 src/utils/url-utils.ts create mode 100644 test/url-test-sample.csv create mode 100644 test/url-utils.test.ts diff --git a/README.md b/README.md index 70abc08..f51b75c 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ A plugin designed to view and edit `CSV files` directly within Obsidian. - **Edit** cells directly by clicking and typing. - **Manage** rows and columns (add, delete, move) with a simple right-click on the header. - **Switch Delimiter Non‑Destructively**: Auto‑detects the file delimiter (comma, semicolon, tab, etc.). Changing the delimiter in the toolbar only re-parses the view; it does NOT rewrite your file. Your original delimiter is preserved when saving edits. +- **Clickable URLs**: Plain-text URLs in cells are automatically detected and rendered as clickable links. Click a link to open it in your browser, or click the cell to edit the URL text. I have a plan to design my own database using json and csv only. If you have fancy idea about tables or csv, please feel free to issue (I will consider it in csv-lite or my new plugin) or search it in community. diff --git a/README_zh.md b/README_zh.md index e60447a..5777650 100644 --- a/README_zh.md +++ b/README_zh.md @@ -23,6 +23,7 @@ - **编辑** 直接点击并输入即可编辑单元格 - **管理** 行和列(添加、删除、移动),只需右键点击表头 - **非破坏性分隔符切换**:自动检测文件原始分隔符(逗号、分号、制表符等)。在工具栏切换分隔符只会重新解析视图,不会改写文件;保存编辑时仍使用文件原始分隔符,避免产生大规模 diff。 +- **可点击的 URL 链接**:单元格中的纯文本 URL 会自动识别并渲染为可点击的链接。点击链接在浏览器中打开,或点击单元格编辑 URL 文本。 我计划仅用 json 和 csv 设计自己的数据库。如果你有关于表格或 csv 的新想法,欢迎提 issue(我会考虑加入 csv-lite 或新插件),或在社区中搜索。 diff --git a/docs/CLICKABLE_URLS.md b/docs/CLICKABLE_URLS.md new file mode 100644 index 0000000..efc9dc1 --- /dev/null +++ b/docs/CLICKABLE_URLS.md @@ -0,0 +1,100 @@ +# Clickable URLs Feature + +## Overview + +The Clickable URLs feature automatically detects and renders plain-text URLs in CSV cells as clickable links. This makes it easy to navigate to external resources directly from your CSV data. + +## How It Works + +### URL Detection + +The plugin uses a regex pattern to detect URLs in the format: +- `http://example.com` +- `https://example.com` +- URLs with paths: `https://example.com/path/to/page` +- URLs with query parameters: `https://example.com?param=value` +- URLs with fragments: `https://example.com#section` + +### Display Modes + +Each cell with a URL has two modes: + +1. **Display Mode** (default) + - URLs are rendered as clickable links + - Links open in a new browser tab + - Click the cell (not the link) to enter edit mode + +2. **Edit Mode** + - Shows the plain text input field + - Allows editing the URL text + - Automatically switches back to display mode on blur + +### Implementation Details + +#### Files Created/Modified + +1. **`src/utils/url-utils.ts`** (new) + - `containsUrl()`: Detects if text contains URLs + - `parseTextWithUrls()`: Parses text into segments (URL and non-URL) + - `createUrlDisplay()`: Creates DOM elements with clickable links + +2. **`src/view/table-render.ts`** (modified) + - Integrated URL display layer + - Toggle between display and edit modes + - Dynamic URL detection on input changes + +3. **`styles.css`** (modified) + - Added styles for `.csv-cell-display` + - Added styles for `.csv-cell-link` with hover effects + +#### Architecture + +``` +Cell Structure: +├── + ├──
(shown when not editing) + │ ├── Plain text + │ ├── URL + │ └── More text + └── (shown when editing) +``` + +### User Experience + +1. **Viewing**: URLs appear as underlined, colored links +2. **Clicking a link**: Opens in new browser tab (Ctrl/Cmd + Click for background tab) +3. **Editing**: Click anywhere in the cell (including the display area, but not on the link itself) to enter edit mode +4. **Saving**: Changes are saved automatically when you blur the input + +### Click Behavior + +- **Click on link**: Opens URL in new tab +- **Click on cell (anywhere except link)**: Enters edit mode and focuses the input +- **Hover over cell**: Shows background highlight to indicate it's editable +- **Hover over link**: Shows link-specific hover effect + +### Styling + +The links use CSS variables for theming: +- `--link-color`: Link color +- `--link-color-hover`: Hover state color +- Transitions for smooth hover effects + +### Testing + +Unit tests are provided in `test/url-utils.test.ts`: +- URL detection accuracy +- Text parsing with single/multiple URLs +- Edge cases (URLs at start/end, with special characters) + +### Demo + +A sample CSV file with URLs is provided in `test/url-test-sample.csv` for testing the feature. + +## Future Enhancements + +Possible improvements: +- Support for other URL formats (ftp://, file://, etc.) +- URL validation and error indicators +- Custom link styling per column +- Option to disable auto-linking for specific columns diff --git a/src/utils/url-utils.ts b/src/utils/url-utils.ts new file mode 100644 index 0000000..cda969c --- /dev/null +++ b/src/utils/url-utils.ts @@ -0,0 +1,124 @@ +/** + * Utility functions for URL detection and rendering + */ + +// URL regex pattern that matches common URL formats +const URL_PATTERN = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi; + +/** + * Detect if text contains URLs + */ +export function containsUrl(text: string): boolean { + const urlRegex = new RegExp(URL_PATTERN); + return urlRegex.test(text); +} + +/** + * Parse text and return segments with URL information + */ +export interface TextSegment { + text: string; + isUrl: boolean; + url?: string; +} + +export function parseTextWithUrls(text: string): TextSegment[] { + const segments: TextSegment[] = []; + let lastIndex = 0; + + // Reset regex lastIndex + const urlRegex = new RegExp(URL_PATTERN); + let match; + + while ((match = urlRegex.exec(text)) !== null) { + // Add text before URL + if (match.index > lastIndex) { + segments.push({ + text: text.substring(lastIndex, match.index), + isUrl: false + }); + } + + // Add URL segment + segments.push({ + text: match[0], + isUrl: true, + url: match[0] + }); + + lastIndex = urlRegex.lastIndex; + } + + // Add remaining text + if (lastIndex < text.length) { + segments.push({ + text: text.substring(lastIndex), + isUrl: false + }); + } + + // If no URLs found, return the whole text as one segment + if (segments.length === 0) { + segments.push({ + text: text, + isUrl: false + }); + } + + return segments; +} + +/** + * Create a display element with clickable URLs + */ +export function createUrlDisplay(text: string, onClick?: () => void): HTMLElement { + const display = document.createElement('div'); + display.className = 'csv-cell-display'; + + const segments = parseTextWithUrls(text); + + for (const segment of segments) { + if (segment.isUrl && segment.url) { + const link = document.createElement('a'); + link.href = segment.url; + link.textContent = segment.text; + link.className = 'csv-cell-link'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + + // Prevent link click from triggering cell edit + link.onclick = (e) => { + e.stopPropagation(); + }; + + display.appendChild(link); + } else { + const span = document.createElement('span'); + span.textContent = segment.text; + display.appendChild(span); + } + } + + // Add an edit button for cells that are entirely URLs (no other clickable area) + if (onClick) { + const editBtn = document.createElement('span'); + editBtn.className = 'csv-cell-edit-btn'; + editBtn.textContent = '✎'; + editBtn.title = 'Click to edit'; + editBtn.onclick = (e) => { + e.stopPropagation(); + onClick(); + }; + display.appendChild(editBtn); + + // Also make display clickable (for areas that aren't links) + display.onclick = (e) => { + // Only trigger if not clicking on a link + if ((e.target as HTMLElement).tagName !== 'A') { + onClick(); + } + }; + } + + return display; +} diff --git a/src/view/table-render.ts b/src/view/table-render.ts index bdba556..b34e58e 100644 --- a/src/view/table-render.ts +++ b/src/view/table-render.ts @@ -2,6 +2,7 @@ import { TableUtils } from "../utils/table-utils"; import { CSVUtils } from "../utils/csv-utils"; import { i18n } from "../i18n"; import { setIcon } from "obsidian"; +import { containsUrl, createUrlDisplay } from "../utils/url-utils"; export interface TableRenderOptions { tableData: string[][]; @@ -270,11 +271,39 @@ export function renderTable(options: TableRenderOptions) { const td = tableRow.createEl("td", { attr: { style: `width: ${columnWidths[j] || 100}px` }, }); + const input = td.createEl("input", { cls: "csv-cell-input", attr: { value: cell }, }); + + // Create display layer for URL rendering + const hasUrl = containsUrl(cell); + let displayEl: HTMLElement | null = null; + + // Function to enter edit mode + const enterEditMode = () => { + const display = td.querySelector('.csv-cell-display') as HTMLElement; + if (display) { + display.style.display = 'none'; + } + input.style.display = 'block'; + input.focus(); + }; + + if (hasUrl) { + displayEl = createUrlDisplay(cell, enterEditMode); + td.insertBefore(displayEl, input); + } + setupAutoResize(input); + + // Hide input initially if URL display is shown + if (hasUrl && displayEl) { + input.style.display = 'none'; + displayEl.style.display = 'block'; + } + input.oninput = (ev) => { if (ev.currentTarget instanceof HTMLInputElement) { saveSnapshot(); @@ -290,10 +319,63 @@ export function renderTable(options: TableRenderOptions) { if (autoResize) { adjustInputHeight(ev.currentTarget); } + + // Update display on input change + const newHasUrl = containsUrl(ev.currentTarget.value); + const tdEl = ev.currentTarget.parentElement; + if (tdEl) { + const existingDisplay = tdEl.querySelector('.csv-cell-display'); + if (newHasUrl) { + // Create or update display + if (existingDisplay) { + existingDisplay.remove(); + } + const inputEl = ev.currentTarget; + const newDisplay = createUrlDisplay(ev.currentTarget.value, () => { + // Enter edit mode: show input first, then focus + const disp = tdEl.querySelector('.csv-cell-display') as HTMLElement; + if (disp) { + disp.style.display = 'none'; + } + inputEl.style.display = 'block'; + inputEl.focus(); + }); + tdEl.insertBefore(newDisplay, ev.currentTarget); + } else if (existingDisplay) { + // Remove display if no URLs + existingDisplay.remove(); + } + } } }; + input.onfocus = (ev) => { - setActiveCell(i, j, ev.currentTarget as HTMLInputElement); + if (ev.currentTarget instanceof HTMLInputElement) { + setActiveCell(i, j, ev.currentTarget); + // Hide display and show input when focused + const tdEl = ev.currentTarget.parentElement; + if (tdEl) { + const display = tdEl.querySelector('.csv-cell-display') as HTMLElement; + if (display) { + display.style.display = 'none'; + ev.currentTarget.style.display = 'block'; + } + } + } + }; + + input.onblur = (ev) => { + if (ev.currentTarget instanceof HTMLInputElement) { + // Show display and hide input when blurred (if contains URL) + const tdEl = ev.currentTarget.parentElement; + if (tdEl) { + const display = tdEl.querySelector('.csv-cell-display') as HTMLElement; + if (display && containsUrl(ev.currentTarget.value)) { + display.style.display = 'block'; + ev.currentTarget.style.display = 'none'; + } + } + } }; }); } diff --git a/styles.css b/styles.css index b2cdb89..cc550ee 100644 --- a/styles.css +++ b/styles.css @@ -53,6 +53,74 @@ If your plugin does not need CSS, delete this file. line-height: 1.4; } +/* URL display layer */ +.csv-cell-display { + padding: 2px 4px; + min-height: 24px; + cursor: text; + white-space: pre-wrap; + word-break: break-word; + line-height: 1.4; + user-select: text; + transition: background-color 0.15s ease; + position: relative; + padding-right: 24px; /* Space for edit button */ +} + +/* Hover effect on cell to show it's editable */ +.csv-cell-display:hover { + background-color: var(--background-modifier-hover); + border-radius: 3px; +} + +/* Edit button for URL cells */ +.csv-cell-edit-btn { + cursor: pointer; + color: var(--text-muted); + font-size: 12px; + padding: 2px 4px; + border-radius: 3px; + opacity: 0; + transition: opacity 0.15s ease, background-color 0.15s ease; + position: absolute; + right: 2px; + top: 50%; + transform: translateY(-50%); +} + +.csv-cell-display:hover .csv-cell-edit-btn { + opacity: 1; +} + +.csv-cell-edit-btn:hover { + background-color: var(--interactive-accent); + color: var(--text-on-accent); +} + +/* Clickable URL links */ +.csv-cell-link { + color: var(--link-color); + text-decoration: none; + cursor: pointer; + border-bottom: 1px solid var(--link-color); + transition: all 0.2s ease; + position: relative; + z-index: 1; +} + +.csv-cell-link:hover { + color: var(--link-color-hover); + border-bottom-color: var(--link-color-hover); + text-decoration: none; + background-color: rgba(var(--interactive-accent-rgb), 0.1); + padding: 0 2px; + border-radius: 2px; +} + +.csv-cell-link:active { + opacity: 0.7; +} + /* csv-cell-input 特有样式(宽度由 JS 控制) */ .csv-cell-input { width: 100%; diff --git a/test/url-test-sample.csv b/test/url-test-sample.csv new file mode 100644 index 0000000..ca969b1 --- /dev/null +++ b/test/url-test-sample.csv @@ -0,0 +1,12 @@ +name,website,documentation,description +GitHub,https://github.com,https://docs.github.com,Popular code hosting platform +Stack Overflow,https://stackoverflow.com,https://stackoverflow.com/help,Q&A site for developers +MDN,https://developer.mozilla.org,https://developer.mozilla.org/en-US/docs/Web,Web technology documentation +Obsidian,https://obsidian.md,https://help.obsidian.md,Note-taking and knowledge base app +TypeScript,https://www.typescriptlang.org,https://www.typescriptlang.org/docs/,JavaScript with types +React,https://react.dev,https://react.dev/learn,UI library for web +Vue,https://vuejs.org,https://vuejs.org/guide/,Progressive framework +Node.js,https://nodejs.org,https://nodejs.org/docs/latest/api/,JavaScript runtime +NPM,https://www.npmjs.com,https://docs.npmjs.com/,Package manager +Python,https://www.python.org,https://docs.python.org/3/,General-purpose programming +Mixed Content,https://example.com,Check https://docs.example.com for details,Text with inline URLs diff --git a/test/url-utils.test.ts b/test/url-utils.test.ts new file mode 100644 index 0000000..8c240d7 --- /dev/null +++ b/test/url-utils.test.ts @@ -0,0 +1,105 @@ +import { containsUrl, parseTextWithUrls, createUrlDisplay } from '../src/utils/url-utils'; + +describe('URL Utils', () => { + describe('containsUrl', () => { + it('should detect HTTP URLs', () => { + expect(containsUrl('http://example.com')).toBe(true); + expect(containsUrl('Visit http://example.com for more')).toBe(true); + }); + + it('should detect HTTPS URLs', () => { + expect(containsUrl('https://example.com')).toBe(true); + expect(containsUrl('Check out https://github.com')).toBe(true); + }); + + it('should detect URLs with paths', () => { + expect(containsUrl('https://example.com/path/to/page')).toBe(true); + expect(containsUrl('http://site.com/docs?q=test')).toBe(true); + }); + + it('should detect URLs with query parameters', () => { + expect(containsUrl('https://example.com?foo=bar&baz=qux')).toBe(true); + }); + + it('should detect URLs with fragments', () => { + expect(containsUrl('https://example.com#section')).toBe(true); + expect(containsUrl('https://example.com/page#top')).toBe(true); + }); + + it('should return false for non-URLs', () => { + expect(containsUrl('Just plain text')).toBe(false); + expect(containsUrl('example.com')).toBe(false); // No protocol + expect(containsUrl('www.example.com')).toBe(false); // No protocol + expect(containsUrl('')).toBe(false); + }); + + it('should handle multiple URLs in text', () => { + expect(containsUrl('Visit https://site1.com and https://site2.com')).toBe(true); + }); + }); + + describe('parseTextWithUrls', () => { + it('should parse text with no URLs', () => { + const result = parseTextWithUrls('Just plain text'); + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + text: 'Just plain text', + isUrl: false + }); + }); + + it('should parse text with single URL', () => { + const result = parseTextWithUrls('Visit https://example.com today'); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ + text: 'Visit ', + isUrl: false + }); + expect(result[1]).toEqual({ + text: 'https://example.com', + isUrl: true, + url: 'https://example.com' + }); + expect(result[2]).toEqual({ + text: ' today', + isUrl: false + }); + }); + + it('should parse text with multiple URLs', () => { + const result = parseTextWithUrls('Check https://site1.com and https://site2.com out'); + expect(result).toHaveLength(5); + expect(result[1].isUrl).toBe(true); + expect(result[3].isUrl).toBe(true); + }); + + it('should parse URL at start of text', () => { + const result = parseTextWithUrls('https://example.com is great'); + expect(result[0]).toEqual({ + text: 'https://example.com', + isUrl: true, + url: 'https://example.com' + }); + }); + + it('should parse URL at end of text', () => { + const result = parseTextWithUrls('Visit https://example.com'); + expect(result[result.length - 1]).toEqual({ + text: 'https://example.com', + isUrl: true, + url: 'https://example.com' + }); + }); + + it('should parse URL with complex path', () => { + const url = 'https://example.com/path/to/page?param=value&other=123#section'; + const result = parseTextWithUrls(url); + expect(result).toHaveLength(1); + expect(result[0].isUrl).toBe(true); + expect(result[0].url).toBe(url); + }); + }); + + // Note: createUrlDisplay tests are skipped as they require DOM environment + // The function is tested through integration tests in the actual plugin +}); From d43e2063df38e094b7acefa1f076302a50a93875 Mon Sep 17 00:00:00 2001 From: JayBridge <12310903@mail.sustech.edu.cn> Date: Sun, 1 Feb 2026 13:54:37 +0800 Subject: [PATCH 3/4] style: adjust padding for compact dropdown alignment - Added left and right padding adjustments to the setting item control in the compact dropdown to ensure proper alignment with buttons. --- styles.css | 2 ++ 1 file changed, 2 insertions(+) diff --git a/styles.css b/styles.css index cc550ee..fac1232 100644 --- a/styles.css +++ b/styles.css @@ -191,6 +191,8 @@ If your plugin does not need CSS, delete this file. /* Align dropdown vertically with buttons */ .csv-delimiter-compact .setting-item-control { margin: 0 !important; + padding-left: 0 !important; + padding-right: 0 !important; } .csv-delimiter-compact .setting-item-control .dropdown { height: 32px !important; From e7d2fe56ca2a2f8ab683541dd154cc680141bc25 Mon Sep 17 00:00:00 2001 From: JayBridge <12310903@mail.sustech.edu.cn> Date: Sun, 1 Feb 2026 14:01:08 +0800 Subject: [PATCH 4/4] feat: enhance clickable URLs feature to support Markdown links - Updated the clickable URLs functionality to automatically detect and render both plain-text URLs and Markdown-style links in CSV cells. - Enhanced documentation to reflect the new capabilities and provide examples for users. - Added tests to ensure proper detection and parsing of Markdown links alongside plain URLs. --- README.md | 2 +- README_zh.md | 2 +- docs/CLICKABLE_URLS.md | 40 +++++++++++++++++-- src/utils/url-utils.ts | 72 ++++++++++++++++++++++++++++------ test/markdown-links-sample.csv | 7 ++++ test/url-utils.test.ts | 52 ++++++++++++++++++++++-- 6 files changed, 155 insertions(+), 20 deletions(-) create mode 100644 test/markdown-links-sample.csv diff --git a/README.md b/README.md index f51b75c..70db03c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ A plugin designed to view and edit `CSV files` directly within Obsidian. - **Edit** cells directly by clicking and typing. - **Manage** rows and columns (add, delete, move) with a simple right-click on the header. - **Switch Delimiter Non‑Destructively**: Auto‑detects the file delimiter (comma, semicolon, tab, etc.). Changing the delimiter in the toolbar only re-parses the view; it does NOT rewrite your file. Your original delimiter is preserved when saving edits. -- **Clickable URLs**: Plain-text URLs in cells are automatically detected and rendered as clickable links. Click a link to open it in your browser, or click the cell to edit the URL text. +- **Clickable URLs**: Plain-text URLs and Markdown-style links (`[text](url)`) in cells are automatically detected and rendered as clickable links. Click a link to open it in your browser, or click the edit button (✎) to edit the cell content. I have a plan to design my own database using json and csv only. If you have fancy idea about tables or csv, please feel free to issue (I will consider it in csv-lite or my new plugin) or search it in community. diff --git a/README_zh.md b/README_zh.md index 5777650..f6d423b 100644 --- a/README_zh.md +++ b/README_zh.md @@ -23,7 +23,7 @@ - **编辑** 直接点击并输入即可编辑单元格 - **管理** 行和列(添加、删除、移动),只需右键点击表头 - **非破坏性分隔符切换**:自动检测文件原始分隔符(逗号、分号、制表符等)。在工具栏切换分隔符只会重新解析视图,不会改写文件;保存编辑时仍使用文件原始分隔符,避免产生大规模 diff。 -- **可点击的 URL 链接**:单元格中的纯文本 URL 会自动识别并渲染为可点击的链接。点击链接在浏览器中打开,或点击单元格编辑 URL 文本。 +- **可点击的 URL 链接**:单元格中的纯文本 URL 和 Markdown 风格链接(`[文本](url)`)会自动识别并渲染为可点击的链接。点击链接在浏览器中打开,或点击编辑按钮(✎)编辑单元格内容。 我计划仅用 json 和 csv 设计自己的数据库。如果你有关于表格或 csv 的新想法,欢迎提 issue(我会考虑加入 csv-lite 或新插件),或在社区中搜索。 diff --git a/docs/CLICKABLE_URLS.md b/docs/CLICKABLE_URLS.md index efc9dc1..265e708 100644 --- a/docs/CLICKABLE_URLS.md +++ b/docs/CLICKABLE_URLS.md @@ -2,19 +2,26 @@ ## Overview -The Clickable URLs feature automatically detects and renders plain-text URLs in CSV cells as clickable links. This makes it easy to navigate to external resources directly from your CSV data. +The Clickable URLs feature automatically detects and renders plain-text URLs and Markdown-style links in CSV cells as clickable links. This makes it easy to navigate to external resources directly from your CSV data while keeping your tables clean and readable. ## How It Works ### URL Detection -The plugin uses a regex pattern to detect URLs in the format: +The plugin detects two types of links: + +**1. Plain URLs:** - `http://example.com` - `https://example.com` - URLs with paths: `https://example.com/path/to/page` - URLs with query parameters: `https://example.com?param=value` - URLs with fragments: `https://example.com#section` +**2. Markdown-style Links:** +- `[GitHub](https://github.com)` - Displays as "GitHub" but links to the URL +- `[Documentation](https://docs.example.com)` - Clean, readable link text +- Mixed content: `Visit [our site](https://example.com) for more` - Text with embedded Markdown links + ### Display Modes Each cell with a URL has two modes: @@ -89,7 +96,32 @@ Unit tests are provided in `test/url-utils.test.ts`: ### Demo -A sample CSV file with URLs is provided in `test/url-test-sample.csv` for testing the feature. +Sample CSV files are provided for testing: +- `test/url-test-sample.csv` - Plain URLs +- `test/markdown-links-sample.csv` - Markdown-style links + +### Examples + +**Plain URLs:** +```csv +name,website +GitHub,https://github.com +``` +Displays: GitHub | https://github.com (as clickable link) + +**Markdown Links:** +```csv +name,website +GitHub,[Visit GitHub](https://github.com) +``` +Displays: GitHub | Visit GitHub (as clickable link, cleaner!) + +**Mixed Content:** +```csv +description +Check [our docs](https://docs.example.com) and https://example.com +``` +Both links are clickable, with Markdown link showing as "our docs" ## Future Enhancements @@ -98,3 +130,5 @@ Possible improvements: - URL validation and error indicators - Custom link styling per column - Option to disable auto-linking for specific columns +- Support for Obsidian internal links (`[[note]]`) +- Support for Wiki-style links diff --git a/src/utils/url-utils.ts b/src/utils/url-utils.ts index cda969c..6da2e55 100644 --- a/src/utils/url-utils.ts +++ b/src/utils/url-utils.ts @@ -5,12 +5,16 @@ // URL regex pattern that matches common URL formats const URL_PATTERN = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi; +// Markdown link pattern: [text](url) +const MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g; + /** - * Detect if text contains URLs + * Detect if text contains URLs or Markdown links */ export function containsUrl(text: string): boolean { const urlRegex = new RegExp(URL_PATTERN); - return urlRegex.test(text); + const markdownRegex = new RegExp(MARKDOWN_LINK_PATTERN); + return urlRegex.test(text) || markdownRegex.test(text); } /** @@ -20,18 +24,60 @@ export interface TextSegment { text: string; isUrl: boolean; url?: string; + displayText?: string; // For Markdown links } export function parseTextWithUrls(text: string): TextSegment[] { const segments: TextSegment[] = []; - let lastIndex = 0; - // Reset regex lastIndex + // Find all matches (both URLs and Markdown links) with their positions + interface Match { + index: number; + length: number; + displayText: string; + url: string; + } + + const matches: Match[] = []; + + // Find Markdown links first (they take precedence) + const markdownRegex = new RegExp(MARKDOWN_LINK_PATTERN); + let mdMatch: RegExpExecArray | null; + while ((mdMatch = markdownRegex.exec(text)) !== null) { + matches.push({ + index: mdMatch.index, + length: mdMatch[0].length, + displayText: mdMatch[1], // The text inside [...] + url: mdMatch[2] // The URL inside (...) + }); + } + + // Find plain URLs (but skip those inside Markdown links) const urlRegex = new RegExp(URL_PATTERN); - let match; + let urlMatch: RegExpExecArray | null; + while ((urlMatch = urlRegex.exec(text)) !== null) { + // Check if this URL is already part of a Markdown link + const isPartOfMarkdown = matches.some(m => + urlMatch!.index >= m.index && urlMatch!.index < m.index + m.length + ); + + if (!isPartOfMarkdown) { + matches.push({ + index: urlMatch.index, + length: urlMatch[0].length, + displayText: urlMatch[0], + url: urlMatch[0] + }); + } + } + + // Sort matches by position + matches.sort((a, b) => a.index - b.index); - while ((match = urlRegex.exec(text)) !== null) { - // Add text before URL + // Build segments + let lastIndex = 0; + for (const match of matches) { + // Add text before this match if (match.index > lastIndex) { segments.push({ text: text.substring(lastIndex, match.index), @@ -39,14 +85,15 @@ export function parseTextWithUrls(text: string): TextSegment[] { }); } - // Add URL segment + // Add URL/link segment segments.push({ - text: match[0], + text: match.displayText, isUrl: true, - url: match[0] + url: match.url, + displayText: match.displayText }); - lastIndex = urlRegex.lastIndex; + lastIndex = match.index + match.length; } // Add remaining text @@ -81,7 +128,8 @@ export function createUrlDisplay(text: string, onClick?: () => void): HTMLElemen if (segment.isUrl && segment.url) { const link = document.createElement('a'); link.href = segment.url; - link.textContent = segment.text; + // Use displayText if available (for Markdown links), otherwise use text + link.textContent = segment.displayText || segment.text; link.className = 'csv-cell-link'; link.target = '_blank'; link.rel = 'noopener noreferrer'; diff --git a/test/markdown-links-sample.csv b/test/markdown-links-sample.csv new file mode 100644 index 0000000..df19883 --- /dev/null +++ b/test/markdown-links-sample.csv @@ -0,0 +1,7 @@ +name,website,documentation,description +GitHub,[Visit GitHub](https://github.com),[Docs](https://docs.github.com),Code hosting with Markdown links +Stack Overflow,[SO Community](https://stackoverflow.com),[Help Center](https://stackoverflow.com/help),Q&A for developers - compact display +MDN,[MDN Docs](https://developer.mozilla.org),https://developer.mozilla.org/en-US/docs/Web,Mixed: Markdown and plain URL +Obsidian,https://obsidian.md,[Help](https://help.obsidian.md),Mixed: Plain URL and Markdown +TypeScript,[TS Lang](https://www.typescriptlang.org),[Documentation](https://www.typescriptlang.org/docs/),"Cleaner tables with [TS](https://www.typescriptlang.org)" +Multiple Links,[GitHub](https://github.com) and [GitLab](https://gitlab.com),[Docs1](https://docs.github.com) | [Docs2](https://docs.gitlab.com),Multiple Markdown links in one cell diff --git a/test/url-utils.test.ts b/test/url-utils.test.ts index 8c240d7..a628cd0 100644 --- a/test/url-utils.test.ts +++ b/test/url-utils.test.ts @@ -36,6 +36,15 @@ describe('URL Utils', () => { it('should handle multiple URLs in text', () => { expect(containsUrl('Visit https://site1.com and https://site2.com')).toBe(true); }); + + it('should detect Markdown-style links', () => { + expect(containsUrl('[GitHub](https://github.com)')).toBe(true); + expect(containsUrl('Check [this link](https://example.com) out')).toBe(true); + }); + + it('should detect mixed URLs and Markdown links', () => { + expect(containsUrl('[GitHub](https://github.com) and https://example.com')).toBe(true); + }); }); describe('parseTextWithUrls', () => { @@ -58,7 +67,8 @@ describe('URL Utils', () => { expect(result[1]).toEqual({ text: 'https://example.com', isUrl: true, - url: 'https://example.com' + url: 'https://example.com', + displayText: 'https://example.com' }); expect(result[2]).toEqual({ text: ' today', @@ -78,7 +88,8 @@ describe('URL Utils', () => { expect(result[0]).toEqual({ text: 'https://example.com', isUrl: true, - url: 'https://example.com' + url: 'https://example.com', + displayText: 'https://example.com' }); }); @@ -87,7 +98,8 @@ describe('URL Utils', () => { expect(result[result.length - 1]).toEqual({ text: 'https://example.com', isUrl: true, - url: 'https://example.com' + url: 'https://example.com', + displayText: 'https://example.com' }); }); @@ -98,6 +110,40 @@ describe('URL Utils', () => { expect(result[0].isUrl).toBe(true); expect(result[0].url).toBe(url); }); + + it('should parse Markdown-style link', () => { + const result = parseTextWithUrls('[GitHub](https://github.com)'); + expect(result).toHaveLength(1); + expect(result[0].isUrl).toBe(true); + expect(result[0].url).toBe('https://github.com'); + expect(result[0].displayText).toBe('GitHub'); + }); + + it('should parse text with Markdown link and plain text', () => { + const result = parseTextWithUrls('Visit [GitHub](https://github.com) now'); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ + text: 'Visit ', + isUrl: false + }); + expect(result[1].isUrl).toBe(true); + expect(result[1].url).toBe('https://github.com'); + expect(result[1].displayText).toBe('GitHub'); + expect(result[2]).toEqual({ + text: ' now', + isUrl: false + }); + }); + + it('should parse mixed Markdown links and plain URLs', () => { + const result = parseTextWithUrls('[GitHub](https://github.com) and https://example.com'); + expect(result).toHaveLength(3); + expect(result[0].isUrl).toBe(true); + expect(result[0].displayText).toBe('GitHub'); + expect(result[1].text).toBe(' and '); + expect(result[2].isUrl).toBe(true); + expect(result[2].url).toBe('https://example.com'); + }); }); // Note: createUrlDisplay tests are skipped as they require DOM environment