@flourish/layout
Advanced tools
+3
-3
| { | ||
| "name": "@flourish/layout", | ||
| "type": "module", | ||
| "version": "8.6.0", | ||
| "version": "8.7.0", | ||
| "private": false, | ||
@@ -51,4 +51,4 @@ "description": "Flourish module to control layouts", | ||
| "@flourish/pocket-knife": "^2.3.0", | ||
| "@flourish/utils-color": "^2.0.0", | ||
| "@flourish/number-formatter": "^2.0.0" | ||
| "@flourish/number-formatter": "^2.0.0", | ||
| "@flourish/utils-color": "^2.0.0" | ||
| }, | ||
@@ -55,0 +55,0 @@ "exports": "./src/index.js", |
+3
-0
@@ -0,1 +1,4 @@ | ||
| # 8.7.0 | ||
| * Use Visualization title for csv download filename | ||
| # 8.6.0 | ||
@@ -2,0 +5,0 @@ * Update @flourish/interpreter to v9.2.0 to improve date interpretations |
@@ -197,2 +197,32 @@ import { zipSync, strToU8 } from "fflate"; | ||
| function sanitizeFilename(filename) { | ||
| return ( | ||
| filename | ||
| // Remove path traversal | ||
| .replace(/^\.+/, "") | ||
| // eslint-disable-next-line no-useless-escape | ||
| .replace(/[\/\\]/g, "_") | ||
| // Remove invalid characters | ||
| // eslint-disable-next-line no-control-regex | ||
| .replace(/[<>:"|?*\x00-\x1f]/g, "_") | ||
| // Handle Windows reserved names | ||
| .replace(/^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/i, "_$1") | ||
| // Remove leading/trailing spaces and dots | ||
| .trim() | ||
| .replace(/[\s.]+$/, "") | ||
| // Limit length (preserve extension) | ||
| .slice(0, 255) | ||
| ); | ||
| } | ||
| function generateCSVFilename(extension = "csv") { | ||
| const visualization_title = window.template?.metadata?.visualization_title; | ||
| const sanitized_title = sanitizeFilename( | ||
| visualization_title || "Untitled_Visualization", | ||
| ); | ||
| const filename = `${sanitized_title}.${extension}`; | ||
| return filename; | ||
| } | ||
| async function getData() { | ||
@@ -210,3 +240,3 @@ const zip_files = {}; | ||
| const dataset_name = datasets_to_download[0]; | ||
| const file_name = `Flourish-data_${dataset_name}.csv`; | ||
| const file_name = generateCSVFilename("csv"); | ||
| const csv_string = generateCSVString(formatted_datasets, dataset_name); | ||
@@ -223,3 +253,3 @@ // Add BOM to CSV string before creating the blob | ||
| const file_name = `Flourish-data.zip`; | ||
| const file_name = generateCSVFilename("zip"); | ||
| const zip_data = zipSync(zip_files); | ||
@@ -240,2 +270,3 @@ const blob = new Blob([zip_data], { type: "application/zip" }); | ||
| formatData, | ||
| sanitizeFilename, | ||
| }; |
@@ -7,2 +7,3 @@ import { expect } from "chai"; | ||
| formatData, | ||
| sanitizeFilename, | ||
| } from "./data-download.js"; | ||
@@ -433,1 +434,77 @@ import { | ||
| }); | ||
| describe("sanitizeFilename", () => { | ||
| it("should replace invalid characters with underscores", () => { | ||
| expect(sanitizeFilename("My<File>Name:Test")).to.equal("My_File_Name_Test"); | ||
| expect(sanitizeFilename("File|With?Invalid*Chars")).to.equal( | ||
| "File_With_Invalid_Chars", | ||
| ); | ||
| expect(sanitizeFilename('File"With"Quotes')).to.equal("File_With_Quotes"); | ||
| }); | ||
| it("should replace forward and back slashes with underscores", () => { | ||
| expect(sanitizeFilename("path/to/file")).to.equal("path_to_file"); | ||
| expect(sanitizeFilename("path\\to\\file")).to.equal("path_to_file"); | ||
| expect(sanitizeFilename("C:\\Users\\data")).to.equal("C__Users_data"); | ||
| }); | ||
| it("should remove leading dots to prevent hidden files", () => { | ||
| expect(sanitizeFilename(".hidden")).to.equal("hidden"); | ||
| expect(sanitizeFilename("..hidden")).to.equal("hidden"); | ||
| expect(sanitizeFilename("...file")).to.equal("file"); | ||
| }); | ||
| it("should handle Windows reserved names", () => { | ||
| expect(sanitizeFilename("con")).to.equal("_con"); | ||
| expect(sanitizeFilename("prn")).to.equal("_prn"); | ||
| expect(sanitizeFilename("aux")).to.equal("_aux"); | ||
| expect(sanitizeFilename("nul")).to.equal("_nul"); | ||
| expect(sanitizeFilename("com1")).to.equal("_com1"); | ||
| expect(sanitizeFilename("lpt1")).to.equal("_lpt1"); | ||
| expect(sanitizeFilename("CON")).to.equal("_CON"); | ||
| }); | ||
| it("should trim leading and trailing spaces", () => { | ||
| expect(sanitizeFilename(" title ")).to.equal("title"); | ||
| expect(sanitizeFilename(" file name ")).to.equal("file name"); | ||
| }); | ||
| it("should remove trailing dots and spaces to prevent issues when extension is added", () => { | ||
| expect(sanitizeFilename("title...")).to.equal("title"); | ||
| expect(sanitizeFilename("title ")).to.equal("title"); | ||
| expect(sanitizeFilename("title. . .")).to.equal("title"); | ||
| }); | ||
| it("should limit title length to 255 characters", () => { | ||
| const longTitle = "a".repeat(300); | ||
| expect(sanitizeFilename(longTitle)).to.have.lengthOf(255); | ||
| }); | ||
| it("should handle control characters", () => { | ||
| expect(sanitizeFilename("file\x00name")).to.equal("file_name"); | ||
| expect(sanitizeFilename("file\x1fname")).to.equal("file_name"); | ||
| }); | ||
| it("should handle normal titles without changes", () => { | ||
| expect(sanitizeFilename("normal_title")).to.equal("normal_title"); | ||
| expect(sanitizeFilename("title-name-123")).to.equal("title-name-123"); | ||
| expect(sanitizeFilename("MyVisualization2024")).to.equal( | ||
| "MyVisualization2024", | ||
| ); | ||
| }); | ||
| it("should handle titles that happen to contain dots", () => { | ||
| expect(sanitizeFilename("Q1.2024 Report")).to.equal("Q1.2024 Report"); | ||
| expect(sanitizeFilename("Data v2.0")).to.equal("Data v2.0"); | ||
| }); | ||
| it("should handle complex visualization titles", () => { | ||
| expect(sanitizeFilename(" <Report>: Q1/2024 | Final* ")).to.equal( | ||
| "_Report__ Q1_2024 _ Final_", | ||
| ); | ||
| expect(sanitizeFilename("..Sales\\Data|2024?")).to.equal( | ||
| "Sales_Data_2024_", | ||
| ); | ||
| expect(sanitizeFilename('My "Best" Viz!')).to.equal("My _Best_ Viz!"); | ||
| }); | ||
| }); |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
429125
1.22%8160
1.51%0
-100%