From c74477864fba9a7fa3b3afe305bf50ce83bd5e5c Mon Sep 17 00:00:00 2001 From: "John A. De Goes" Date: Tue, 28 Apr 2015 21:32:13 -0600 Subject: [PATCH] first commit --- .gitignore | 7 + Gruntfile.js | 52 ++ LICENSE | 202 +++++ MODULES.md | 474 +++++++++++ README.md | 93 +++ bower.json | 36 + examples/src/Examples.purs | 41 + output/examples.html | 8 + output/examples.js | 1559 ++++++++++++++++++++++++++++++++++++ package.json | 9 + src/Data/Path/Pathy.purs | 424 ++++++++++ 11 files changed, 2905 insertions(+) create mode 100644 .gitignore create mode 100644 Gruntfile.js create mode 100644 LICENSE create mode 100644 MODULES.md create mode 100644 README.md create mode 100644 bower.json create mode 100644 examples/src/Examples.purs create mode 100644 output/examples.html create mode 100644 output/examples.js create mode 100644 package.json create mode 100644 src/Data/Path/Pathy.purs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06ec60b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.* +!/.gitignore +/output/ +/node_modules/ +/bower_components/ +/tmp/ +/node_modules/ \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..62783c9 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,52 @@ +module.exports = function(grunt) { + "use strict"; + + grunt.initConfig({ + psc: { + options: { + main: "Examples", + modules: ["Examples"] + }, + example: { + src: ["<%=libFiles%>", "examples/Prelude.purs"], + dest: "output/examples.js" + } + }, + libFiles: [ + "src/**/*.purs", + "examples/**/*.purs", + "bower_components/purescript-*/src/**/*.purs", + ], + + clean: ["output"], + + pscMake: ["<%=libFiles%>"], + dotPsci: ["<%=libFiles%>"], + pscDocs: { + readme: { + src: "src/**/*.purs", + dest: "MODULES.md" + } + }, + jsvalidate: { + options:{ + globals: {}, + esprimaOptions: {}, + verbose: false + }, + targetName:{ + files:{ + src:['output/**/*.js'] + } + } + } + }); + + grunt.loadNpmTasks("grunt-contrib-clean"); + grunt.loadNpmTasks("grunt-purescript"); + grunt.loadNpmTasks('grunt-jsvalidate'); + + grunt.registerTask("make", ["pscMake", "jsvalidate", "dotPsci", "pscDocs"]); + grunt.registerTask("test", ["jsvalidate", "psc", "pscDocs"]); + grunt.registerTask("default", ["make"]); +}; \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e06d208 --- /dev/null +++ b/LICENSE @@ -0,0 +1,202 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + diff --git a/MODULES.md b/MODULES.md new file mode 100644 index 0000000..36b3909 --- /dev/null +++ b/MODULES.md @@ -0,0 +1,474 @@ +# Module Documentation + +## Module Data.Path.Pathy + +#### `Rel` + +``` purescript +data Rel :: * +``` + +The (phantom) type of relative paths. + +#### `Abs` + +``` purescript +data Abs :: * +``` + +The (phantom) type of absolute paths. + +#### `File` + +``` purescript +data File :: * +``` + +The (phantom) type of files. + +#### `Dir` + +``` purescript +data Dir :: * +``` + +The (phantom) type of directories. + +#### `Unsandboxed` + +``` purescript +data Unsandboxed :: * +``` + +The (phantom) type of unsandboxed paths. + +#### `Sandboxed` + +``` purescript +data Sandboxed :: * +``` + +The (phantom) type of sandboxed paths. + +#### `FileName` + +``` purescript +newtype FileName + = FileName String +``` + +A newtype around a file name. + +#### `DirName` + +``` purescript +newtype DirName + = DirName String +``` + +A newtype around a directory name. + +#### `Path` + +``` purescript +data Path a b s +``` + +A type that describes a Path. All flavors of paths are described by this +type, whether they are absolute or relative paths, whether they +refer to files or directories, whether they are sandboxed or not. + +* The type parameter `a` describes whether the path is `Rel` or `Abs`. +* The type parameter `b` describes whether the path is `File` or `Dir`. +* The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + +To ensure type safety, there is no way for users to create a value of +this type directly. Instead, helpers should be used, such as `rootDir`, +`currentDir`, `file`, `dir`, `()`, and `parsePath`. + +This ADT allows invalid paths (e.g. paths inside files), but there is no +possible way for such paths to be constructed by user-land code. The only +"invalid path" that may be constructed is using the `parentDir'` function, e.g. +`parentDir' rootDir`, or by parsing an equivalent string such as `/../`, +but such paths are marked as unsandboxed, and may not be rendered to strings +until they are first sandboxed to some directory. + +#### `RelFile` + +``` purescript +type RelFile s = Path Rel File s +``` + +A type describing a file whose location is given relative to some other, +unspecified directory (referred to as the "current directory"). + +#### `AbsFile` + +``` purescript +type AbsFile s = Path Abs File s +``` + +A type describing a file whose location is absolutely specified. + +#### `RelDir` + +``` purescript +type RelDir s = Path Rel Dir s +``` + +A type describing a directory whose location is given relative to some +other, unspecified directory (referred to as the "current directory"). + +#### `AbsDir` + +``` purescript +type AbsDir s = Path Abs Dir s +``` + +A type describing a directory whose location is absolutely specified. + +#### `Escaper` + +``` purescript +newtype Escaper + = Escaper (String -> String) +``` + +Escapers encode segments or characters which have reserved meaning. + +#### `runEscaper` + +``` purescript +runEscaper :: Escaper -> String -> String +``` + +Given an escaper and a segment to encode, returns the encoded segment. + +#### `posixEscaper` + +``` purescript +posixEscaper :: Escaper +``` + +An escaper that removes all slashes, converts ".." into "$dot$dot", and +converts "." into "$dot". + +#### `file` + +``` purescript +file :: forall s. String -> Path Rel File s +``` + +Creates a path which points to a relative file of the specified name. + +#### `file'` + +``` purescript +file' :: forall s. FileName -> Path Rel File s +``` + +Creates a path which points to a relative file of the specified name. + +#### `fileName` + +``` purescript +fileName :: forall a s. Path a File s -> FileName +``` + +Retrieves the name of a file path. + +#### `extension` + +``` purescript +extension :: FileName -> String +``` + +Retrieves the extension of a file name. + +#### `dropExtension` + +``` purescript +dropExtension :: FileName -> FileName +``` + +Drops the extension on a file name. + +#### `changeExtension` + +``` purescript +changeExtension :: forall a s. (String -> String) -> FileName -> FileName +``` + +Changes the extension on a file name. + +#### `dirName` + +``` purescript +dirName :: forall a s. Path a Dir s -> Maybe DirName +``` + +Retrieves the name of a directory path. Not all paths have such a name, +for example, the root or current directory. + +#### `dir` + +``` purescript +dir :: String -> Path Rel Dir Sandboxed +``` + +Creates a path which points to a relative directory of the specified name. + +#### `dir'` + +``` purescript +dir' :: forall s. DirName -> Path Rel Dir s +``` + +Creates a path which points to a relative directory of the specified name. + +#### `()` + +``` purescript +() :: forall a b s. Path a Dir s -> Path Rel b s -> Path a b s +``` + +Given a directory path, appends either a file or directory to the path. + +#### `(<.>)` + +``` purescript +(<.>) :: forall a s. Path a File s -> String -> Path a File s +``` + +Sets the extension of the file to the specified extension. + +```purescript +file "image" <.> "png" +``` + +#### `isAbsolute` + +``` purescript +isAbsolute :: forall a b s. Path a b s -> Boolean +``` + +Determines if this path is absolutely located. + +#### `isRelative` + +``` purescript +isRelative :: forall a b s. Path a b s -> Boolean +``` + +Determines if this path is relatively located. + +#### `peel` + +``` purescript +peel :: forall a b s. Path a b s -> Maybe (Tuple (Path a Dir s) (Either DirName FileName)) +``` + +Peels off the last directory and the terminal file or directory name +from the path. Returns `Nothing` if there is no such pair (for example, +if the last path segment is root directory, current directory, or parent +directory). + +#### `parentDir` + +``` purescript +parentDir :: forall a b s. Path a b s -> Maybe (Path a Dir s) +``` + +Attempts to extract out the parent directory of the specified path. If the +function would have to use a relative path in the return value, the function will +instead return `Nothing`. + +#### `unsandbox` + +``` purescript +unsandbox :: forall a b s. Path a b s -> Path a b Unsandboxed +``` + +Unsandboxes any path (whether sandboxed or not). + +#### `parentDir'` + +``` purescript +parentDir' :: forall a b s. Path a b s -> Path a Dir Unsandboxed +``` + +Extracts out the parent directory of the specified path. Will use the +parent path segment (..) if strictly necessary and therefore can escape +a sandboxed path. + +#### `currentDir` + +``` purescript +currentDir :: Path Rel Dir Sandboxed +``` + +The "current directory", which can be used to define relatively-located resources. + +#### `rootDir` + +``` purescript +rootDir :: Path Abs Dir Sandboxed +``` + +The root directory, which can be used to define absolutely-located resources. + +#### `renameFile` + +``` purescript +renameFile :: forall a s. (FileName -> FileName) -> Path a File s -> Path a File s +``` + +Renames a file path. + +#### `renameDir` + +``` purescript +renameDir :: forall a s. (DirName -> DirName) -> Path a Dir s -> Path a Dir s +``` + +Renames a directory path. Note: This is a simple rename of the terminal +directory name, not a "move". + +#### `canonicalize` + +``` purescript +canonicalize :: forall a b s. Path a b s -> Path a b s +``` + +Canonicalizes a path, by reducing things in the form `/x/../` to just `/x/`. + +#### `unsafePrintPath'` + +``` purescript +unsafePrintPath' :: forall a b s. Escaper -> Path a b s -> String +``` + + +#### `unsafePrintPath` + +``` purescript +unsafePrintPath :: forall a b s. Path a b s -> String +``` + + +#### `printPath` + +``` purescript +printPath :: forall a b. Path a b Sandboxed -> String +``` + +Prints a `Path` into its canonical `String` representation. For security +reasons, the path must be sandboxed before it can be rendered to a string. + +#### `printPath'` + +``` purescript +printPath' :: forall a b. Escaper -> Path a b Sandboxed -> String +``` + +Prints a `Path` into its canonical `String` representation, using the +specified escaper to escape special characters in path segments. For +security reasons, the path must be sandboxed before rendering to string. + +#### `identicalPath` + +``` purescript +identicalPath :: forall a a' b b' s s'. Path a b s -> Path a' b' s' -> Boolean +``` + +Determines if two paths have the exact same representation. Note that +two paths may represent the same path even if they have different +representations! + +#### `relativeTo` + +``` purescript +relativeTo :: forall a b s s'. Path a b s -> Path a Dir s' -> Maybe (Path Rel b s') +``` + +Makes one path relative to another reference path, if possible, otherwise +returns `Nothing`. The returned path inherits the sandbox settings of the +reference path. + +Note there are some cases this function cannot handle. + +#### `sandbox` + +``` purescript +sandbox :: forall a b s. Path a Dir Sandboxed -> Path a b s -> Maybe (Path Rel b Sandboxed) +``` + +Attempts to sandbox a path relative to some directory. If successful, the sandboxed +directory will be returned relative to the sandbox directory (although this can easily +be converted into an absolute path using ``). + +This combinator can be used to ensure that paths which originate from user-code +cannot access data outside a given directory. + +#### `refine` + +``` purescript +refine :: forall a b s. (FileName -> FileName) -> (DirName -> DirName) -> Path a b s -> Path a b s +``` + +Refines path segments but does not change anything else. + +#### `parsePath` + +``` purescript +parsePath :: forall z. (RelFile Unsandboxed -> z) -> (AbsFile Unsandboxed -> z) -> (RelDir Unsandboxed -> z) -> (AbsDir Unsandboxed -> z) -> String -> z +``` + +Parses a canonical `String` representation of a path into a `Path` value. +Note that in order to be unambiguous, trailing directories should be +marked with a trailing slash character (`'/'`). + +#### `parseRelFile` + +``` purescript +parseRelFile :: String -> Maybe (RelFile Unsandboxed) +``` + +Attempts to parse a relative file from a string. + +#### `parseAbsFile` + +``` purescript +parseAbsFile :: String -> Maybe (AbsFile Unsandboxed) +``` + +Attempts to parse an absolute file from a string. + +#### `parseRelDir` + +``` purescript +parseRelDir :: String -> Maybe (RelDir Unsandboxed) +``` + +Attempts to parse a relative directory from a string. + +#### `parseAbsDir` + +``` purescript +parseAbsDir :: String -> Maybe (AbsDir Unsandboxed) +``` + +Attempts to parse an absolute directory from a string. + +#### `showPath` + +``` purescript +instance showPath :: Show (Path a b s) +``` + + +#### `eqPath` + +``` purescript +instance eqPath :: Eq (Path a b s) +``` \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c365b6 --- /dev/null +++ b/README.md @@ -0,0 +1,93 @@ +# purescript-pathy + +A type-safe abstraction for platform-independent file system paths. + +# Example + +```purescript +fullPath = rootDir dir "baz" file "foo.png" +``` + +# Getting Started + +## Installation + +```bash +bower install purescript-pathy +``` + +```purescript +import Data.Path.Pathy +``` + +## Introduction + +Applications often have to refer to file system paths in a platform-independent way. + +Many path libraries provide a single abstraction to deal with file system paths. This allows easy composition of different kinds of paths, but comes at the expense of the following distinctions: + + * The distinction between relative and absolute paths. + * The distinction between paths denoting file resources and paths denoting directories. + * The distinction between paths that are secure (sandboxed to some location in the file system) and those that are insecure. + +*Pathy* also uses a single abstraction for file system paths, called `Path`, but uses *phantom types* to keep track of the distinctions above. + +This approach lets you write code that performs type-safe composition of relative, absolute, file, and directory paths, and makes sure you never use paths in an unsafe fashion. Bogus and insecure operations simply aren't allowed by the type system! + +Many paths come from user-input or configuration data. Pathy can parse such string paths and allow you to safely resolve them to expected types. + +### Paths Literals + +Building path liberals is easy. You will typically build path literals from the following components: + + * `rootDir` — The root directory of an absolute path. + * `currentDir` — The current directory (AKA the "working directory"), useful for describing relative paths. + * `file` — A file (in the current directory). + * `dir` — A directory (in the current directory). + * `()` — Combines two paths into one, if the composition makes sense! + * `(<.>)` — Sets the extension of a file path. + + For example: + +```purescript +let + path1 = rootDir dir "foo" dir "bar" file "baz.boo" + path2 = currentDir dir "foo" +in do + trace $ show $ printPath path1 + trace $ show $ printPath path2 +``` + +Pathy doesn't let you create combinators that don't make sense, such as: + +```purescript +rootDir rootDir +currentDir rootDir +file "foo" file "bar" +file "foo" dir "bar" +``` + +All these combinations will be disallowed at compile time! + +Pathy also carries information on whether a path is a file or directory, and whether it's been sandboxed to some known directory. + +### Paths from Strings + +`parsePath` + +### Paths to Strings + +`printPath` + +### Basic Path Manipulation + +`parentDir` + +`parentDir'` + +`sandbox` + + +# API Docs + +[MODULES.md](MODULES.md) diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..60f1556 --- /dev/null +++ b/bower.json @@ -0,0 +1,36 @@ + +{ + "name": "purescript-pathy", + "homepage": "https://github.com/slamdata/purescript-pathy", + "description": "A type-safe abstraction for platform-independent file paths", + "keywords": [ + "purescript" + ], + "license": "Apache 2", + "ignore": [ + "**/.*", + "bower_components", + "node_modules", + "output", + "tests", + "tmp", + "bower.json", + "Gruntfile.js", + "package.json" + ], + "dependencies": { + "purescript-tuples": "~0.3.0", + "purescript-either": "~0.1.4", + "purescript-monoid": "~0.2.0", + "purescript-exceptions": "~0.2.2", + "purescript-control": "~0.2.2", + "purescript-maybe": "~0.2.1", + "purescript-strings": "~0.4.5", + "purescript-monad-eff": "~0.1.0", + "purescript-lists": "~0.6.0", + "purescript-profunctor": "~0.2.1", + "purescript-control": "~0.2.6", + "purescript-foldable-traversable": "~0.3.1", + "purescript-transformers": "~0.5.1" + } +} \ No newline at end of file diff --git a/examples/src/Examples.purs b/examples/src/Examples.purs new file mode 100644 index 0000000..3fc9853 --- /dev/null +++ b/examples/src/Examples.purs @@ -0,0 +1,41 @@ +module Examples where + import Debug.Trace(Trace(), trace) + import Control.Monad.Eff(Eff()) + import Data.Maybe.Unsafe(fromJust) + import Data.Path.Pathy + + test :: forall a. (Show a, Eq a) => String -> a -> a -> Eff (trace :: Trace) Unit + test name expected actual = do + trace $ "Test: " ++ name + if expected == actual then trace $ "Passed: " ++ (show expected) else trace $ "Failed: Expected " ++ (show expected) ++ " but found " ++ (show actual) + + test' :: forall a b s. String -> Path a b s -> String -> Eff (trace :: Trace) Unit + test' n p s = test n (unsafePrintPath p) s + + main = do + trace "NEW TEST" + + -- Should not compile: + -- test "() - file in dir" (printPath (file "image.png" dir "foo")) "./image.png/foo" + + -- Should not compile: + -- test "() - absolute dir in absolute dir" (printPath (rootDir rootDir)) "/" + + -- Should not compile: + -- test "() - absolute dir in relative dir" (printPath (currentDir rootDir)) "/" + + test' "() - two directories" (dir "foo" dir "bar") "./foo/bar/" + + test' "() - file with two parents" (dir "foo" dir "bar" file "image.png") "./foo/bar/image.png" + + test' "(<.>) - file without extension" (file "image" <.> "png") "./image.png" + + test' "(<.>) - file with extension" (file "image.jpg" <.> "png") "./image.png" + + test' "canonicalize - 1 down, 1 up" (canonicalize $ parentDir' $ dir "foo") "./" + + test' "canonicalize - 2 down, 2 up" (canonicalize (parentDir' (parentDir' (dir "foo" dir "bar")))) "./" + + test' "renameFile - single level deep" (renameFile dropExtension (file "image.png")) "./image" + + test' "sandbox - sandbox absolute dir to one level higher" (fromJust $ sandbox (rootDir dir "foo") (rootDir dir "foo" dir "bar")) "./bar/" diff --git a/output/examples.html b/output/examples.html new file mode 100644 index 0000000..43e5623 --- /dev/null +++ b/output/examples.html @@ -0,0 +1,8 @@ + + + + + +

Examples

+ + \ No newline at end of file diff --git a/output/examples.js b/output/examples.js new file mode 100644 index 0000000..1f6865c --- /dev/null +++ b/output/examples.js @@ -0,0 +1,1559 @@ +// Generated by psc version 0.6.8 +var PS = PS || {}; +PS.Prelude = (function () { + "use strict"; + + function showStringImpl(s) { + return JSON.stringify(s); + } + ; + + function numAdd(n1) { + return function(n2) { + return n1 + n2; + }; + } + ; + + function numSub(n1) { + return function(n2) { + return n1 - n2; + }; + } + ; + + function numMul(n1) { + return function(n2) { + return n1 * n2; + }; + } + ; + + function refEq(r1) { + return function(r2) { + return r1 === r2; + }; + } + ; + + function refIneq(r1) { + return function(r2) { + return r1 !== r2; + }; + } + ; + + function concatString(s1) { + return function(s2) { + return s1 + s2; + }; + } + ; + var Semigroupoid = function ($less$less$less) { + this["<<<"] = $less$less$less; + }; + var Show = function (show) { + this.show = show; + }; + var Functor = function ($less$dollar$greater) { + this["<$>"] = $less$dollar$greater; + }; + var Apply = function ($less$times$greater, __superclass_Prelude$dotFunctor_0) { + this["<*>"] = $less$times$greater; + this["__superclass_Prelude.Functor_0"] = __superclass_Prelude$dotFunctor_0; + }; + var Applicative = function (__superclass_Prelude$dotApply_0, pure) { + this["__superclass_Prelude.Apply_0"] = __superclass_Prelude$dotApply_0; + this.pure = pure; + }; + var Bind = function ($greater$greater$eq, __superclass_Prelude$dotApply_0) { + this[">>="] = $greater$greater$eq; + this["__superclass_Prelude.Apply_0"] = __superclass_Prelude$dotApply_0; + }; + var Monad = function (__superclass_Prelude$dotApplicative_0, __superclass_Prelude$dotBind_1) { + this["__superclass_Prelude.Applicative_0"] = __superclass_Prelude$dotApplicative_0; + this["__superclass_Prelude.Bind_1"] = __superclass_Prelude$dotBind_1; + }; + + /** + * | Addition and multiplication + */ + var Semiring = function ($times, $plus, one, zero) { + this["*"] = $times; + this["+"] = $plus; + this.one = one; + this.zero = zero; + }; + + /** + * | Addition, multiplication, and subtraction + */ + var Ring = function ($minus, __superclass_Prelude$dotSemiring_0) { + this["-"] = $minus; + this["__superclass_Prelude.Semiring_0"] = __superclass_Prelude$dotSemiring_0; + }; + var Eq = function ($div$eq, $eq$eq) { + this["/="] = $div$eq; + this["=="] = $eq$eq; + }; + var Semigroup = function ($less$greater) { + this["<>"] = $less$greater; + }; + var $greater$greater$eq = function (dict) { + return dict[">>="]; + }; + var $eq$eq = function (dict) { + return dict["=="]; + }; + var $less$greater = function (dict) { + return dict["<>"]; + }; + var $less$less$less = function (dict) { + return dict["<<<"]; + }; + var $greater$greater$greater = function (__dict_Semigroupoid_0) { + return function (f) { + return function (g) { + return $less$less$less(__dict_Semigroupoid_0)(g)(f); + }; + }; + }; + var $less$times$greater = function (dict) { + return dict["<*>"]; + }; + var $less$dollar$greater = function (dict) { + return dict["<$>"]; + }; + var $div$eq = function (dict) { + return dict["/="]; + }; + + /** + * | Addition, multiplication, and subtraction + */ + var $minus = function (dict) { + return dict["-"]; + }; + var $plus$plus = function (__dict_Semigroup_2) { + return $less$greater(__dict_Semigroup_2); + }; + + /** + * | Addition and multiplication + */ + var $plus = function (dict) { + return dict["+"]; + }; + var $dollar = function (f) { + return function (x) { + return f(x); + }; + }; + + /** + * | Addition and multiplication + */ + var zero = function (dict) { + return dict.zero; + }; + var showString = new Show(showStringImpl); + var show = function (dict) { + return dict.show; + }; + var semiringNumber = new Semiring(numMul, numAdd, 1, 0); + var semigroupoidArr = new Semigroupoid(function (f) { + return function (g) { + return function (x) { + return f(g(x)); + }; + }; + }); + var semigroupString = new Semigroup(concatString); + var ringNumber = new Ring(numSub, function () { + return semiringNumber; + }); + var pure = function (dict) { + return dict.pure; + }; + var $$return = function (__dict_Monad_5) { + return pure(__dict_Monad_5["__superclass_Prelude.Applicative_0"]()); + }; + var negate = function (__dict_Ring_6) { + return function (a) { + return $minus(__dict_Ring_6)(zero(__dict_Ring_6["__superclass_Prelude.Semiring_0"]()))(a); + }; + }; + var liftA1 = function (__dict_Applicative_8) { + return function (f) { + return function (a) { + return $less$times$greater(__dict_Applicative_8["__superclass_Prelude.Apply_0"]())(pure(__dict_Applicative_8)(f))(a); + }; + }; + }; + + /** + * | Flips the order of the arguments to a function of two arguments. + */ + var flip = function (f) { + return function (b) { + return function (a) { + return f(a)(b); + }; + }; + }; + var eqString = new Eq(refIneq, refEq); + var eqNumber = new Eq(refIneq, refEq); + + /** + * | Returns its first argument and ignores its second. + */ + var $$const = function (_35) { + return function (_36) { + return _35; + }; + }; + var ap = function (__dict_Monad_16) { + return function (f) { + return function (a) { + return $greater$greater$eq(__dict_Monad_16["__superclass_Prelude.Bind_1"]())(f)(function (_2) { + return $greater$greater$eq(__dict_Monad_16["__superclass_Prelude.Bind_1"]())(a)(function (_1) { + return $$return(__dict_Monad_16)(_2(_1)); + }); + }); + }; + }; + }; + return { + Semigroup: Semigroup, + Eq: Eq, + Ring: Ring, + Semiring: Semiring, + Monad: Monad, + Bind: Bind, + Applicative: Applicative, + Apply: Apply, + Functor: Functor, + Show: Show, + Semigroupoid: Semigroupoid, + "++": $plus$plus, + "<>": $less$greater, + refIneq: refIneq, + refEq: refEq, + "/=": $div$eq, + "==": $eq$eq, + negate: negate, + "-": $minus, + zero: zero, + "+": $plus, + ap: ap, + "return": $$return, + ">>=": $greater$greater$eq, + liftA1: liftA1, + pure: pure, + "<*>": $less$times$greater, + "<$>": $less$dollar$greater, + show: show, + "$": $dollar, + ">>>": $greater$greater$greater, + "<<<": $less$less$less, + "const": $$const, + flip: flip, + semigroupoidArr: semigroupoidArr, + showString: showString, + semiringNumber: semiringNumber, + ringNumber: ringNumber, + eqString: eqString, + eqNumber: eqNumber, + semigroupString: semigroupString + }; +})(); +var PS = PS || {}; +PS.Data_Profunctor = (function () { + "use strict"; + var Prelude = PS.Prelude; + + /** + * | A `Profunctor` is a `Functor` from the pair category `(Type^op, Type)` + * | to `Type`. + * | + * | In other words, a `Profunctor` is a type constructor of two type + * | arguments, which is contravariant in its first argument and covariant + * | in its second argument. + * | + * | The `dimap` function can be used to map functions over both arguments + * | simultaneously. + * | + * | A straightforward example of a profunctor is the function arrow `(->)`. + * | + * | Laws: + * | + * | - Identity: `dimap id id = id` + * | - Composition: `dimap f1 g1 <<< dimap f2 g2 = dimap (f1 >>> f2) (g1 <<< g2)` + */ + var Profunctor = function (dimap) { + this.dimap = dimap; + }; + var profunctorArr = new Profunctor(function (a2b) { + return function (c2d) { + return function (b2c) { + return Prelude[">>>"](Prelude.semigroupoidArr)(a2b)(Prelude[">>>"](Prelude.semigroupoidArr)(b2c)(c2d)); + }; + }; + }); + return { + Profunctor: Profunctor, + profunctorArr: profunctorArr + }; +})(); +var PS = PS || {}; +PS.Control_Monad_Eff = (function () { + "use strict"; + var Prelude = PS.Prelude; + + function returnE(a) { + return function() { + return a; + }; + } + ; + + function bindE(a) { + return function(f) { + return function() { + return f(a())(); + }; + }; + } + ; + var monadEff = new Prelude.Monad(function () { + return applicativeEff; + }, function () { + return bindEff; + }); + var bindEff = new Prelude.Bind(bindE, function () { + return applyEff; + }); + var applyEff = new Prelude.Apply(Prelude.ap(monadEff), function () { + return functorEff; + }); + var applicativeEff = new Prelude.Applicative(function () { + return applyEff; + }, returnE); + var functorEff = new Prelude.Functor(Prelude.liftA1(applicativeEff)); + return { + bindE: bindE, + returnE: returnE, + functorEff: functorEff, + applyEff: applyEff, + applicativeEff: applicativeEff, + bindEff: bindEff, + monadEff: monadEff + }; +})(); +var PS = PS || {}; +PS.Debug_Trace = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Control_Monad_Eff = PS.Control_Monad_Eff; + + function trace(s) { + return function() { + console.log(s); + return {}; + }; + } + ; + return { + trace: trace + }; +})(); +var PS = PS || {}; +PS.Data_Either = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Control_Alt = PS.Control_Alt; + var Control_Extend = PS.Control_Extend; + + /** + * | The `Either` type is used to represent a choice between two types of value. + * | + * | A common use case for `Either` is error handling, where `Left` is used to + * | carry an error value and `Right` is used to carry a success value. + */ + var Left = (function () { + function Left(value0) { + this.value0 = value0; + }; + Left.create = function (value0) { + return new Left(value0); + }; + return Left; + })(); + + /** + * | The `Either` type is used to represent a choice between two types of value. + * | + * | A common use case for `Either` is error handling, where `Left` is used to + * | carry an error value and `Right` is used to carry a success value. + */ + var Right = (function () { + function Right(value0) { + this.value0 = value0; + }; + Right.create = function (value0) { + return new Right(value0); + }; + return Right; + })(); + + /** + * | Takes two functions and an `Either` value, if the value is a `Left` the + * | inner value is applied to the first function, if the value is a `Right` + * | the inner value is applied to the second function. + * | + * | ``` purescript + * | either f g (Left x) == f x + * | either f g (Right y) == g y + * | ``` + */ + var either = function (_91) { + return function (_92) { + return function (_93) { + if (_93 instanceof Left) { + return _91(_93.value0); + }; + if (_93 instanceof Right) { + return _92(_93.value0); + }; + throw new Error("Failed pattern match"); + }; + }; + }; + return { + Left: Left, + Right: Right, + either: either + }; +})(); +var PS = PS || {}; +PS.Data_Maybe = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Control_Alt = PS.Control_Alt; + var Control_Alternative = PS.Control_Alternative; + var Control_Extend = PS.Control_Extend; + var Control_MonadPlus = PS.Control_MonadPlus; + var Control_Plus = PS.Control_Plus; + + /** + * | The `Maybe` type is used to represent optional values and can be seen as + * | something like a type-safe `null`, where `Nothing` is `null` and `Just x` + * | is the non-null value `x`. + */ + var Nothing = (function () { + function Nothing() { + + }; + Nothing.value = new Nothing(); + return Nothing; + })(); + + /** + * | The `Maybe` type is used to represent optional values and can be seen as + * | something like a type-safe `null`, where `Nothing` is `null` and `Just x` + * | is the non-null value `x`. + */ + var Just = (function () { + function Just(value0) { + this.value0 = value0; + }; + Just.create = function (value0) { + return new Just(value0); + }; + return Just; + })(); + + /** + * | Takes a default value, a function, and a `Maybe` value. If the `Maybe` + * | value is `Nothing` the default value is returned, otherwise the function + * | is applied to the value inside the `Just` and the result is returned. + * | + * | ``` purescript + * | maybe x f Nothing == x + * | maybe x f (Just y) == f y + * | ``` + */ + var maybe = function (_111) { + return function (_112) { + return function (_113) { + if (_113 instanceof Nothing) { + return _111; + }; + if (_113 instanceof Just) { + return _112(_113.value0); + }; + throw new Error("Failed pattern match"); + }; + }; + }; + + /** + * | The `Functor` instance allows functions to transform the contents of a + * | `Just` with the `<$>` operator: + * | + * | ``` purescript + * | f <$> Just x == Just (f x) + * | ``` + * | + * | `Nothing` values are left untouched: + * | + * | ``` purescript + * | f <$> Nothing == Nothing + * | ``` + */ + var functorMaybe = new Prelude.Functor(function (_114) { + return function (_115) { + if (_115 instanceof Just) { + return new Just(_114(_115.value0)); + }; + return Nothing.value; + }; + }); + + /** + * | The `Apply` instance allows functions contained within a `Just` to + * | transform a value contained within a `Just` using the `(<*>)` operator: + * | + * | ``` purescript + * | Just f <*> Just x == Just (f x) + * | ``` + * | + * | `Nothing` values are left untouched: + * | + * | ``` purescript + * | Just f <*> Nothing == Nothing + * | Nothing <*> Just x == Nothing + * | ``` + * | + * | Combining `Functor`'s `<$>` with `Apply`'s `<*>` can be used transform a + * | pure function to take `Maybe`-typed arguments so `f :: a -> b -> c` + * | becomes `f :: Maybe a -> Maybe b -> Maybe c`: + * | + * | ``` purescript + * | f <$> Just x <*> Just y == Just (f x y) + * | ``` + * | + * | The `Nothing`-preserving behaviour of both operators means the result of + * | an expression like the above but where any one of the values is `Nothing` + * | means the whole result becomes `Nothing` also: + * | + * | ``` purescript + * | f <$> Nothing <*> Just y == Nothing + * | f <$> Just x <*> Nothing == Nothing + * | f <$> Nothing <*> Nothing == Nothing + * | ``` + */ + var applyMaybe = new Prelude.Apply(function (_116) { + return function (_117) { + if (_116 instanceof Just) { + return Prelude["<$>"](functorMaybe)(_116.value0)(_117); + }; + if (_116 instanceof Nothing) { + return Nothing.value; + }; + throw new Error("Failed pattern match"); + }; + }, function () { + return functorMaybe; + }); + + /** + * | The `Bind` instance allows sequencing of `Maybe` values and functions that + * | return a `Maybe` by using the `>>=` operator: + * | + * | ``` purescript + * | Just x >>= f = f x + * | Nothing >>= f = Nothing + * | ``` + */ + var bindMaybe = new Prelude.Bind(function (_120) { + return function (_121) { + if (_120 instanceof Just) { + return _121(_120.value0); + }; + if (_120 instanceof Nothing) { + return Nothing.value; + }; + throw new Error("Failed pattern match"); + }; + }, function () { + return applyMaybe; + }); + return { + Nothing: Nothing, + Just: Just, + maybe: maybe, + functorMaybe: functorMaybe, + applyMaybe: applyMaybe, + bindMaybe: bindMaybe + }; +})(); +var PS = PS || {}; +PS.Data_Array = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Data_Maybe = PS.Data_Maybe; + var Control_Alt = PS.Control_Alt; + var Control_Plus = PS.Control_Plus; + var Control_Alternative = PS.Control_Alternative; + var Control_MonadPlus = PS.Control_MonadPlus; + var Prelude_Unsafe = PS.Prelude_Unsafe; + function filter (f) { return function (arr) { var n = 0; var result = []; for (var i = 0, l = arr.length; i < l; i++) { if (f(arr[i])) { result[n++] = arr[i]; } } return result; };}; + return { + filter: filter + }; +})(); +var PS = PS || {}; +PS.Data_Maybe_Unsafe = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Data_Maybe = PS.Data_Maybe; + + /** + * | A partial function that extracts the value from the `Just` data + * | constructor. Passing `Nothing` to `fromJust` will throw an error at + * | runtime. + */ + var fromJust = function (_150) { + if (_150 instanceof Data_Maybe.Just) { + return _150.value0; + }; + throw new Error("Failed pattern match"); + }; + return { + fromJust: fromJust + }; +})(); +var PS = PS || {}; +PS.Data_Tuple = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Data_Monoid = PS.Data_Monoid; + var Control_Lazy = PS.Control_Lazy; + var Data_Array = PS.Data_Array; + var Control_Comonad = PS.Control_Comonad; + var Control_Extend = PS.Control_Extend; + + /** + * | A simple product type for wrapping a pair of component values. + */ + var Tuple = (function () { + function Tuple(value0, value1) { + this.value0 = value0; + this.value1 = value1; + }; + Tuple.create = function (value0) { + return function (value1) { + return new Tuple(value0, value1); + }; + }; + return Tuple; + })(); + + /** + * | Returns the second component of a tuple. + */ + var snd = function (_235) { + return _235.value1; + }; + + /** + * | The `Functor` instance allows functions to transform the contents of a + * | `Tuple` with the `<$>` operator, applying the function to the second + * | component, so: + * | ```purescript + * | f <$> (Tuple x y) = Tuple x (f y) + * | ```` + */ + var functorTuple = new Prelude.Functor(function (_249) { + return function (_250) { + return new Tuple(_250.value0, _249(_250.value1)); + }; + }); + + /** + * | Returns the first component of a tuple. + */ + var fst = function (_234) { + return _234.value0; + }; + return { + Tuple: Tuple, + snd: snd, + fst: fst, + functorTuple: functorTuple + }; +})(); +var PS = PS || {}; +PS.Data_Profunctor_Strong = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Data_Profunctor = PS.Data_Profunctor; + var Data_Tuple = PS.Data_Tuple; + + /** + * | The `Strong` class extends `Profunctor` with combinators for working with + * | product types. + * | + * | `first` and `first` lift values in a `Profunctor` to act on the first and + * | second components of a `Tuple`, respectively. + * | + */ + var Strong = function (__superclass_Data$dotProfunctor$dotProfunctor_0, first, second) { + this["__superclass_Data.Profunctor.Profunctor_0"] = __superclass_Data$dotProfunctor$dotProfunctor_0; + this.first = first; + this.second = second; + }; + var strongArr = new Strong(function () { + return Data_Profunctor.profunctorArr; + }, function (_308) { + return function (_309) { + return new Data_Tuple.Tuple(_308(_309.value0), _309.value1); + }; + }, Prelude["<$>"](Data_Tuple.functorTuple)); + + /** + * | The `Strong` class extends `Profunctor` with combinators for working with + * | product types. + * | + * | `first` and `first` lift values in a `Profunctor` to act on the first and + * | second components of a `Tuple`, respectively. + * | + */ + var first = function (dict) { + return dict.first; + }; + return { + Strong: Strong, + first: first, + strongArr: strongArr + }; +})(); +var PS = PS || {}; +PS.Data_String = (function () { + "use strict"; + var Data_Function = PS.Data_Function; + var Data_Char = PS.Data_Char; + var Prelude = PS.Prelude; + var Data_String_Unsafe = PS.Data_String_Unsafe; + var Data_Maybe = PS.Data_Maybe; + + function lastIndexOf(x) { + return function(s) { + return s.lastIndexOf(x); + }; + } + ; + + function take(n) { + return function(s) { + return s.substr(0, n); + }; + } + ; + + function drop(n) { + return function(s) { + return s.substr(n); + }; + } + ; + + function split(sep) { + return function(s) { + return s.split(sep); + }; + } + ; + + function joinWith(s) { + return function(xs) { + return xs.join(s); + }; + } + ; + return { + joinWith: joinWith, + split: split, + drop: drop, + take: take, + lastIndexOf: lastIndexOf + }; +})(); +var PS = PS || {}; +PS.Data_Path_Pathy = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Data_String = PS.Data_String; + var Data_Array = PS.Data_Array; + var Data_Tuple = PS.Data_Tuple; + var Data_Maybe = PS.Data_Maybe; + var Data_Either = PS.Data_Either; + var Data_Profunctor_Strong = PS.Data_Profunctor_Strong; + var Data_Foldable = PS.Data_Foldable; + var Control_Alt = PS.Control_Alt; + var Data_List = PS.Data_List; + + /** + * | A newtype around a file name. + */ + var FileName = function (x) { + return x; + }; + + /** + * | Escapers encode segments or characters which have reserved meaning. + */ + var Escaper = function (x) { + return x; + }; + + /** + * | A newtype around a directory name. + */ + var DirName = function (x) { + return x; + }; + + /** + * | A type that describes a Path. All flavors of paths are described by this + * | type, whether they are absolute or relative paths, whether they + * | refer to files or directories, whether they are sandboxed or not. + * | + * | * The type parameter `a` describes whether the path is `Rel` or `Abs`. + * | * The type parameter `b` describes whether the path is `File` or `Dir`. + * | * The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + * | + * | To ensure type safety, there is no way for users to create a value of + * | this type directly. Instead, helpers should be used, such as `rootDir`, + * | `currentDir`, `file`, `dir`, `()`, and `parsePath`. + * | + * | This ADT allows invalid paths (e.g. paths inside files), but there is no + * | possible way for such paths to be constructed by user-land code. The only + * | "invalid path" that may be constructed is using the `parentDir'` function, e.g. + * | `parentDir' rootDir`, or by parsing an equivalent string such as `/../`, + * | but such paths are marked as unsandboxed, and may not be rendered to strings + * | until they are first sandboxed to some directory. + */ + var Current = (function () { + function Current() { + + }; + Current.value = new Current(); + return Current; + })(); + + /** + * | A type that describes a Path. All flavors of paths are described by this + * | type, whether they are absolute or relative paths, whether they + * | refer to files or directories, whether they are sandboxed or not. + * | + * | * The type parameter `a` describes whether the path is `Rel` or `Abs`. + * | * The type parameter `b` describes whether the path is `File` or `Dir`. + * | * The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + * | + * | To ensure type safety, there is no way for users to create a value of + * | this type directly. Instead, helpers should be used, such as `rootDir`, + * | `currentDir`, `file`, `dir`, `()`, and `parsePath`. + * | + * | This ADT allows invalid paths (e.g. paths inside files), but there is no + * | possible way for such paths to be constructed by user-land code. The only + * | "invalid path" that may be constructed is using the `parentDir'` function, e.g. + * | `parentDir' rootDir`, or by parsing an equivalent string such as `/../`, + * | but such paths are marked as unsandboxed, and may not be rendered to strings + * | until they are first sandboxed to some directory. + */ + var Root = (function () { + function Root() { + + }; + Root.value = new Root(); + return Root; + })(); + + /** + * | A type that describes a Path. All flavors of paths are described by this + * | type, whether they are absolute or relative paths, whether they + * | refer to files or directories, whether they are sandboxed or not. + * | + * | * The type parameter `a` describes whether the path is `Rel` or `Abs`. + * | * The type parameter `b` describes whether the path is `File` or `Dir`. + * | * The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + * | + * | To ensure type safety, there is no way for users to create a value of + * | this type directly. Instead, helpers should be used, such as `rootDir`, + * | `currentDir`, `file`, `dir`, `()`, and `parsePath`. + * | + * | This ADT allows invalid paths (e.g. paths inside files), but there is no + * | possible way for such paths to be constructed by user-land code. The only + * | "invalid path" that may be constructed is using the `parentDir'` function, e.g. + * | `parentDir' rootDir`, or by parsing an equivalent string such as `/../`, + * | but such paths are marked as unsandboxed, and may not be rendered to strings + * | until they are first sandboxed to some directory. + */ + var ParentIn = (function () { + function ParentIn(value0) { + this.value0 = value0; + }; + ParentIn.create = function (value0) { + return new ParentIn(value0); + }; + return ParentIn; + })(); + + /** + * | A type that describes a Path. All flavors of paths are described by this + * | type, whether they are absolute or relative paths, whether they + * | refer to files or directories, whether they are sandboxed or not. + * | + * | * The type parameter `a` describes whether the path is `Rel` or `Abs`. + * | * The type parameter `b` describes whether the path is `File` or `Dir`. + * | * The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + * | + * | To ensure type safety, there is no way for users to create a value of + * | this type directly. Instead, helpers should be used, such as `rootDir`, + * | `currentDir`, `file`, `dir`, `()`, and `parsePath`. + * | + * | This ADT allows invalid paths (e.g. paths inside files), but there is no + * | possible way for such paths to be constructed by user-land code. The only + * | "invalid path" that may be constructed is using the `parentDir'` function, e.g. + * | `parentDir' rootDir`, or by parsing an equivalent string such as `/../`, + * | but such paths are marked as unsandboxed, and may not be rendered to strings + * | until they are first sandboxed to some directory. + */ + var DirIn = (function () { + function DirIn(value0, value1) { + this.value0 = value0; + this.value1 = value1; + }; + DirIn.create = function (value0) { + return function (value1) { + return new DirIn(value0, value1); + }; + }; + return DirIn; + })(); + + /** + * | A type that describes a Path. All flavors of paths are described by this + * | type, whether they are absolute or relative paths, whether they + * | refer to files or directories, whether they are sandboxed or not. + * | + * | * The type parameter `a` describes whether the path is `Rel` or `Abs`. + * | * The type parameter `b` describes whether the path is `File` or `Dir`. + * | * The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + * | + * | To ensure type safety, there is no way for users to create a value of + * | this type directly. Instead, helpers should be used, such as `rootDir`, + * | `currentDir`, `file`, `dir`, `()`, and `parsePath`. + * | + * | This ADT allows invalid paths (e.g. paths inside files), but there is no + * | possible way for such paths to be constructed by user-land code. The only + * | "invalid path" that may be constructed is using the `parentDir'` function, e.g. + * | `parentDir' rootDir`, or by parsing an equivalent string such as `/../`, + * | but such paths are marked as unsandboxed, and may not be rendered to strings + * | until they are first sandboxed to some directory. + */ + var FileIn = (function () { + function FileIn(value0, value1) { + this.value0 = value0; + this.value1 = value1; + }; + FileIn.create = function (value0) { + return function (value1) { + return new FileIn(value0, value1); + }; + }; + return FileIn; + })(); + + /** + * | Given a directory path, appends either a file or directory to the path. + */ + var $less$div$greater = function (_625) { + return function (_626) { + if (_625 instanceof Current && _626 instanceof Current) { + return Current.value; + }; + if (_625 instanceof Root && _626 instanceof Current) { + return Root.value; + }; + if (_625 instanceof ParentIn && _626 instanceof Current) { + return new ParentIn($less$div$greater(_625.value0)(Current.value)); + }; + if (_625 instanceof FileIn && _626 instanceof Current) { + return new FileIn($less$div$greater(_625.value0)(Current.value), _625.value1); + }; + if (_625 instanceof DirIn && _626 instanceof Current) { + return new DirIn($less$div$greater(_625.value0)(Current.value), _625.value1); + }; + if (_625 instanceof Current && _626 instanceof Root) { + return Current.value; + }; + if (_625 instanceof Root && _626 instanceof Root) { + return Root.value; + }; + if (_625 instanceof ParentIn && _626 instanceof Root) { + return new ParentIn($less$div$greater(_625.value0)(Current.value)); + }; + if (_625 instanceof FileIn && _626 instanceof Root) { + return new FileIn($less$div$greater(_625.value0)(Current.value), _625.value1); + }; + if (_625 instanceof DirIn && _626 instanceof Root) { + return new DirIn($less$div$greater(_625.value0)(Current.value), _625.value1); + }; + if (_626 instanceof ParentIn) { + return new ParentIn($less$div$greater(_625)(_626.value0)); + }; + if (_626 instanceof FileIn) { + return new FileIn($less$div$greater(_625)(_626.value0), _626.value1); + }; + if (_626 instanceof DirIn) { + return new DirIn($less$div$greater(_625)(_626.value0), _626.value1); + }; + throw new Error("Failed pattern match"); + }; + }; + + /** + * | Unsandboxes any path (whether sandboxed or not). + */ + var unsandbox = function (_629) { + if (_629 instanceof Current) { + return Current.value; + }; + if (_629 instanceof Root) { + return Root.value; + }; + if (_629 instanceof ParentIn) { + return new ParentIn(unsandbox(_629.value0)); + }; + if (_629 instanceof DirIn) { + return new DirIn(unsandbox(_629.value0), _629.value1); + }; + if (_629 instanceof FileIn) { + return new FileIn(unsandbox(_629.value0), _629.value1); + }; + throw new Error("Failed pattern match"); + }; + var unsafePrintPath$prime = function (r) { + return function (p) { + var go = function (_634) { + if (_634 instanceof Current) { + return "./"; + }; + if (_634 instanceof Root) { + return "/"; + }; + if (_634 instanceof ParentIn) { + return go(_634.value0) + "../"; + }; + if (_634 instanceof DirIn && _634.value0 instanceof FileIn) { + return go(_634.value0) + ("/" + (_634.value1 + "/")); + }; + if (_634 instanceof DirIn) { + return go(_634.value0) + (_634.value1 + "/"); + }; + if (_634 instanceof FileIn && _634.value0 instanceof FileIn) { + return go(_634.value0) + ("/" + _634.value1); + }; + if (_634 instanceof FileIn) { + return go(_634.value0) + _634.value1; + }; + throw new Error("Failed pattern match"); + }; + return go(p); + }; + }; + var showPath = new Prelude.Show(function (_638) { + if (_638 instanceof Current) { + return "currentDir"; + }; + if (_638 instanceof Root) { + return "rootDir"; + }; + if (_638 instanceof ParentIn) { + return "(parentDir' " + (Prelude.show(showPath)(_638.value0) + ")"); + }; + if (_638 instanceof FileIn) { + return Prelude.show(showPath)(_638.value0) + ("(file " + (_638.value1 + ")")); + }; + if (_638 instanceof DirIn) { + return Prelude.show(showPath)(_638.value0) + ("(dir " + (_638.value1 + ")")); + }; + throw new Error("Failed pattern match"); + }); + + /** + * | Given an escaper and a segment to encode, returns the encoded segment. + */ + var runEscaper = function (_619) { + return _619; + }; + + /** + * | The root directory, which can be used to define absolutely-located resources. + */ + var rootDir = Root.value; + + /** + * | Renames a file path. + */ + var renameFile = function (f) { + var go = function (_632) { + if (_632 instanceof FileIn) { + return new FileIn(_632.value0, f(_632.value1)); + }; + return _632; + }; + return go; + }; + + /** + * | An escaper that does nothing except remove slashes (the bare minimum of + * | what must be done). + */ + var nonEscaper = Escaper(function (s) { + return Data_String.joinWith("")(Data_Array.filter(Prelude["/="](Prelude.eqString)("/"))(Data_String.split("")(s))); + }); + + /** + * | An escaper that removes all slashes, converts ".." into "$dot$dot", and + * | converts "." into "$dot". + */ + var posixEscaper = Escaper(Prelude[">>>"](Prelude.semigroupoidArr)(runEscaper(nonEscaper))(function (s) { + var _724 = s === ".."; + if (_724) { + return "$dot$dot"; + }; + if (!_724) { + var _725 = s === "."; + if (_725) { + return "$dot"; + }; + if (!_725) { + return s; + }; + throw new Error("Failed pattern match"); + }; + throw new Error("Failed pattern match"); + })); + var unsafePrintPath = unsafePrintPath$prime(posixEscaper); + + /** + * | Determines if this path is absolutely located. + */ + var isAbsolute = function (__copy__627) { + var _627 = __copy__627; + tco: while (true) { + if (_627 instanceof Current) { + return false; + }; + if (_627 instanceof Root) { + return true; + }; + if (_627 instanceof ParentIn) { + var __tco__627 = _627.value0; + _627 = __tco__627; + continue tco; + }; + if (_627 instanceof FileIn) { + var __tco__627 = _627.value0; + _627 = __tco__627; + continue tco; + }; + if (_627 instanceof DirIn) { + var __tco__627 = _627.value0; + _627 = __tco__627; + continue tco; + }; + throw new Error("Failed pattern match"); + }; + }; + + /** + * | Peels off the last directory and the terminal file or directory name + * | from the path. Returns `Nothing` if there is no such pair (for example, + * | if the last path segment is root directory, current directory, or parent + * | directory). + */ + var peel = function (_628) { + if (_628 instanceof Current) { + return Data_Maybe.Nothing.value; + }; + if (_628 instanceof Root) { + return Data_Maybe.Nothing.value; + }; + if (_628 instanceof ParentIn) { + return Prelude[">>="](Data_Maybe.bindMaybe)(peel(_628.value0))(Prelude[">>>"](Prelude.semigroupoidArr)(Data_Tuple.fst)(peel)); + }; + if (_628 instanceof DirIn) { + var d$prime = new Data_Either.Left(_628.value1); + return Data_Maybe.Just.create(Data_Maybe.maybe((function () { + var _734 = isAbsolute(_628.value0); + if (_734) { + return new Data_Tuple.Tuple(Root.value, d$prime); + }; + if (!_734) { + return new Data_Tuple.Tuple(Current.value, d$prime); + }; + throw new Error("Failed pattern match"); + })())(function (_614) { + return new Data_Tuple.Tuple(Data_Either.either(DirIn.create(_614.value0))(FileIn.create(_614.value0))(_614.value1), d$prime); + })(peel(_628.value0))); + }; + if (_628 instanceof FileIn) { + var f$prime = new Data_Either.Right(_628.value1); + return Data_Maybe.Just.create(Data_Maybe.maybe((function () { + var _740 = isAbsolute(_628.value0); + if (_740) { + return new Data_Tuple.Tuple(Root.value, f$prime); + }; + if (!_740) { + return new Data_Tuple.Tuple(Current.value, f$prime); + }; + throw new Error("Failed pattern match"); + })())(function (_615) { + return new Data_Tuple.Tuple(Data_Either.either(DirIn.create(_615.value0))(FileIn.create(_615.value0))(_615.value1), f$prime); + })(peel(_628.value0))); + }; + throw new Error("Failed pattern match"); + }; + + /** + * | Extracts out the parent directory of the specified path. Will use the + * | parent path segment (..) if strictly necessary and therefore can escape + * | a sandboxed path. + */ + var parentDir$prime = function (_630) { + if (_630 instanceof Current) { + return new ParentIn(Current.value); + }; + if (_630 instanceof Root) { + return new ParentIn(Root.value); + }; + if (_630 instanceof ParentIn) { + return new ParentIn(parentDir$prime(_630.value0)); + }; + if (_630 instanceof FileIn) { + return Data_Maybe.maybe((function () { + var _748 = isAbsolute(_630.value0); + if (_748) { + return Root.value; + }; + if (!_748) { + return Current.value; + }; + throw new Error("Failed pattern match"); + })())(function (_616) { + return Data_Either.either(DirIn.create(_616.value0))(FileIn.create(_616.value0))(_616.value1); + })(Prelude["<$>"](Data_Maybe.functorMaybe)(Data_Profunctor_Strong.first(Data_Profunctor_Strong.strongArr)(unsandbox))(peel(_630.value0))); + }; + if (_630 instanceof DirIn) { + return Data_Maybe.maybe((function () { + var _754 = isAbsolute(_630.value0); + if (_754) { + return Root.value; + }; + if (!_754) { + return Current.value; + }; + throw new Error("Failed pattern match"); + })())(function (_617) { + return Data_Either.either(DirIn.create(_617.value0))(FileIn.create(_617.value0))(_617.value1); + })(Prelude["<$>"](Data_Maybe.functorMaybe)(Data_Profunctor_Strong.first(Data_Profunctor_Strong.strongArr)(unsandbox))(peel(_630.value0))); + }; + throw new Error("Failed pattern match"); + }; + + /** + * | Determines if two paths have the exact same representation. Note that + * | two paths may represent the same path even if they have different + * | representations! + */ + var identicalPath = function (p1) { + return function (p2) { + return Prelude.show(showPath)(p1) === Prelude.show(showPath)(p2); + }; + }; + + /** + * | Creates a path which points to a relative file of the specified name. + */ + var file$prime = function (f) { + return new FileIn(Current.value, f); + }; + + /** + * | Creates a path which points to a relative file of the specified name. + */ + var file = function (f) { + return file$prime(f); + }; + + /** + * | Retrieves the extension of a file name. + */ + var extension = function (_621) { + var idx = Data_String.lastIndexOf(".")(_621); + var _761 = idx === -1; + if (_761) { + return ""; + }; + if (!_761) { + return Data_String.drop(idx + 1)(_621); + }; + throw new Error("Failed pattern match"); + }; + + /** + * | Drops the extension on a file name. + */ + var dropExtension = function (_622) { + var idx = Data_String.lastIndexOf(".")(_622); + var _763 = idx === -1; + if (_763) { + return _622; + }; + if (!_763) { + return FileName(Data_String.take(idx)(_622)); + }; + throw new Error("Failed pattern match"); + }; + + /** + * | Creates a path which points to a relative directory of the specified name. + */ + var dir$prime = function (d) { + return new DirIn(Current.value, d); + }; + + /** + * | Creates a path which points to a relative directory of the specified name. + */ + var dir = function (d) { + return dir$prime(d); + }; + + /** + * | Changes the extension on a file name. + */ + var changeExtension = function (_623) { + return function (_624) { + var ext = _623(extension(_624)); + return (function (_613) { + var _767 = ext === ""; + if (_767) { + return _613; + }; + if (!_767) { + return FileName(_613 + ("." + ext)); + }; + throw new Error("Failed pattern match"); + })(dropExtension(_624)); + }; + }; + + /** + * | Sets the extension of the file to the specified extension. + * | + * | ```purescript + * | file "image" <.> "png" + * | ``` + */ + var $less$dot$greater = function (p) { + return function (ext) { + return renameFile(changeExtension(Prelude["const"](ext)))(p); + }; + }; + + /** + * | Canonicalizes a path and returns information on whether or not it actually changed. + */ + var canonicalize$prime = function (_631) { + if (_631 instanceof Current) { + return new Data_Tuple.Tuple(false, Current.value); + }; + if (_631 instanceof Root) { + return new Data_Tuple.Tuple(false, Root.value); + }; + if (_631 instanceof ParentIn && _631.value0 instanceof FileIn) { + return new Data_Tuple.Tuple(true, _631.value0.value0); + }; + if (_631 instanceof ParentIn && _631.value0 instanceof DirIn) { + return new Data_Tuple.Tuple(true, _631.value0.value0); + }; + if (_631 instanceof ParentIn) { + return (function (_618) { + var p$prime$prime = new ParentIn(_618.value1); + if (_618.value0) { + return canonicalize$prime(p$prime$prime); + }; + if (!_618.value0) { + return new Data_Tuple.Tuple(_618.value0, p$prime$prime); + }; + throw new Error("Failed pattern match"); + })(canonicalize$prime(_631.value0)); + }; + if (_631 instanceof FileIn) { + return Prelude["<$>"](Data_Tuple.functorTuple)(Prelude.flip(FileIn.create)(_631.value1))(canonicalize$prime(_631.value0)); + }; + if (_631 instanceof DirIn) { + return Prelude["<$>"](Data_Tuple.functorTuple)(Prelude.flip(DirIn.create)(_631.value1))(canonicalize$prime(_631.value0)); + }; + throw new Error("Failed pattern match"); + }; + + /** + * | Canonicalizes a path, by reducing things in the form `/x/../` to just `/x/`. + */ + var canonicalize = function (p) { + return Data_Tuple.snd(canonicalize$prime(p)); + }; + + /** + * | Makes one path relative to another reference path, if possible, otherwise + * | returns `Nothing`. The returned path inherits the sandbox settings of the + * | reference path. + * | + * | Note there are some cases this function cannot handle. + */ + var relativeTo = function (p1) { + return function (p2) { + var relativeTo$prime = function (p1_1) { + return function (p2_1) { + var _784 = identicalPath(p1_1)(p2_1); + if (_784) { + return new Data_Maybe.Just(Current.value); + }; + if (!_784) { + var _785 = peel(p1_1); + if (_785 instanceof Data_Maybe.Nothing) { + var _786 = new Data_Tuple.Tuple(p1_1, p2_1); + if (_786.value0 instanceof Root && _786.value1 instanceof Root) { + return new Data_Maybe.Just(Current.value); + }; + if (_786.value0 instanceof Current && _786.value1 instanceof Current) { + return new Data_Maybe.Just(Current.value); + }; + return Data_Maybe.Nothing.value; + }; + if (_785 instanceof Data_Maybe.Just) { + return Prelude["<$>"](Data_Maybe.functorMaybe)(Prelude.flip($less$div$greater)(Data_Either.either(DirIn.create(Current.value))(FileIn.create(Current.value))(_785.value0.value1)))(relativeTo$prime(_785.value0.value0)(p2_1)); + }; + throw new Error("Failed pattern match"); + }; + throw new Error("Failed pattern match"); + }; + }; + return relativeTo$prime(canonicalize(p1))(canonicalize(p2)); + }; + }; + + /** + * | Attempts to sandbox a path relative to some directory. If successful, the sandboxed + * | directory will be returned relative to the sandbox directory (although this can easily + * | be converted into an absolute path using ``). + * | + * | This combinator can be used to ensure that paths which originate from user-code + * | cannot access data outside a given directory. + */ + var sandbox = function (p1) { + return function (p2) { + return relativeTo(p2)(p1); + }; + }; + return { + FileName: FileName, + Escaper: Escaper, + DirName: DirName, + "unsafePrintPath'": unsafePrintPath$prime, + unsafePrintPath: unsafePrintPath, + unsandbox: unsandbox, + sandbox: sandbox, + runEscaper: runEscaper, + rootDir: rootDir, + renameFile: renameFile, + relativeTo: relativeTo, + posixEscaper: posixEscaper, + peel: peel, + "parentDir'": parentDir$prime, + isAbsolute: isAbsolute, + identicalPath: identicalPath, + "file'": file$prime, + file: file, + extension: extension, + dropExtension: dropExtension, + "dir'": dir$prime, + dir: dir, + changeExtension: changeExtension, + canonicalize: canonicalize, + "<.>": $less$dot$greater, + "": $less$div$greater, + showPath: showPath + }; +})(); +var PS = PS || {}; +PS.Examples = (function () { + "use strict"; + var Prelude = PS.Prelude; + var Debug_Trace = PS.Debug_Trace; + var Data_Path_Pathy = PS.Data_Path_Pathy; + var Data_Maybe_Unsafe = PS.Data_Maybe_Unsafe; + var Control_Monad_Eff = PS.Control_Monad_Eff; + var test = function (__dict_Show_520) { + return function (__dict_Eq_521) { + return function (name) { + return function (expected) { + return function (actual) { + return function __do() { + Debug_Trace.trace("Test: " + name)(); + var _794 = Prelude["=="](__dict_Eq_521)(expected)(actual); + if (_794) { + return Debug_Trace.trace("Passed: " + Prelude.show(__dict_Show_520)(expected))(); + }; + if (!_794) { + return Debug_Trace.trace("Failed: Expected " + (Prelude.show(__dict_Show_520)(expected) + (" but found " + Prelude.show(__dict_Show_520)(actual))))(); + }; + throw new Error("Failed pattern match"); + }; + }; + }; + }; + }; + }; + var test$prime = function (n) { + return function (p) { + return function (s) { + return test(Prelude.showString)(Prelude.eqString)(n)(Data_Path_Pathy.unsafePrintPath(p))(s); + }; + }; + }; + var main = function __do() { + Debug_Trace.trace("NEW TEST")(); + test$prime("() - two directories")(Data_Path_Pathy[""](Data_Path_Pathy.dir("foo"))(Data_Path_Pathy.dir("bar")))("./foo/bar/")(); + test$prime("() - file with two parents")(Data_Path_Pathy[""](Data_Path_Pathy[""](Data_Path_Pathy.dir("foo"))(Data_Path_Pathy.dir("bar")))(Data_Path_Pathy.file("image.png")))("./foo/bar/image.png")(); + test$prime("(<.>) - file without extension")(Data_Path_Pathy["<.>"](Data_Path_Pathy.file("image"))("png"))("./image.png")(); + test$prime("(<.>) - file with extension")(Data_Path_Pathy["<.>"](Data_Path_Pathy.file("image.jpg"))("png"))("./image.png")(); + test$prime("canonicalize - 1 down, 1 up")(Data_Path_Pathy.canonicalize(Data_Path_Pathy["parentDir'"](Data_Path_Pathy.dir("foo"))))("./")(); + test$prime("canonicalize - 2 down, 2 up")(Data_Path_Pathy.canonicalize(Data_Path_Pathy["parentDir'"](Data_Path_Pathy["parentDir'"](Data_Path_Pathy[""](Data_Path_Pathy.dir("foo"))(Data_Path_Pathy.dir("bar"))))))("./")(); + test$prime("renameFile - single level deep")(Data_Path_Pathy.renameFile(Data_Path_Pathy.dropExtension)(Data_Path_Pathy.file("image.png")))("./image")(); + return test$prime("sandbox - sandbox absolute dir to one level higher")(Data_Maybe_Unsafe.fromJust(Data_Path_Pathy.sandbox(Data_Path_Pathy[""](Data_Path_Pathy.rootDir)(Data_Path_Pathy.dir("foo")))(Data_Path_Pathy[""](Data_Path_Pathy[""](Data_Path_Pathy.rootDir)(Data_Path_Pathy.dir("foo")))(Data_Path_Pathy.dir("bar")))))("./bar/")(); + }; + return { + main: main, + "test'": test$prime, + test: test + }; +})(); +PS.Examples.main(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..fd52140 --- /dev/null +++ b/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "dependencies": { + "grunt": "~0.4.5", + "grunt-purescript": "~0.6.0", + "grunt-contrib-clean": "~0.6.0", + "grunt-jsvalidate": "~0.2.2" + } +} \ No newline at end of file diff --git a/src/Data/Path/Pathy.purs b/src/Data/Path/Pathy.purs new file mode 100644 index 0000000..ad02d41 --- /dev/null +++ b/src/Data/Path/Pathy.purs @@ -0,0 +1,424 @@ +module Data.Path.Pathy + ( Abs() + , AbsDir(..) + , AbsFile(..) + , Dir() + , DirName(..) + , Escaper(..) + , File() + , FileName(..) + , Path() + , Rel() + , RelDir(..) + , RelFile(..) + , Sandboxed() + , Unsandboxed() + , () + , (<.>) + , canonicalize + , changeExtension + , currentDir + , dir + , dir' + , dirName + , dropExtension + , extension + , file + , file' + , fileName + , identicalPath + , isAbsolute + , isRelative + , parentDir + , parentDir' + , peel + , posixEscaper + , parsePath + , parseAbsDir + , parseAbsFile + , parseRelDir + , parseRelFile + , printPath + , printPath' + , refine + , relativeTo + , renameDir + , renameFile + , rootDir + , runEscaper + , sandbox + , unsandbox + , unsafePrintPath + , unsafePrintPath' + ) + where + + import Control.Alt((<|>)) + import qualified Data.String as S + import Data.Foldable(foldr) + import Data.Array(filter, length, zipWith, range) + import Data.Tuple(Tuple(..), fst, snd) + import Data.Either(Either(..), either) + import Data.Maybe(Maybe(..), maybe, fromMaybe) + import Data.List(List(..)) + import Data.Profunctor.Strong(first) + + -- | The (phantom) type of relative paths. + foreign import data Rel :: * + + -- | The (phantom) type of absolute paths. + foreign import data Abs :: * + + -- | The (phantom) type of files. + foreign import data File :: * + + -- | The (phantom) type of directories. + foreign import data Dir :: * + + -- | The (phantom) type of unsandboxed paths. + foreign import data Unsandboxed :: * + + -- | The (phantom) type of sandboxed paths. + foreign import data Sandboxed :: * + + -- | A newtype around a file name. + newtype FileName = FileName String + + -- | A newtype around a directory name. + newtype DirName = DirName String + + -- | A type that describes a Path. All flavors of paths are described by this + -- | type, whether they are absolute or relative paths, whether they + -- | refer to files or directories, whether they are sandboxed or not. + -- | + -- | * The type parameter `a` describes whether the path is `Rel` or `Abs`. + -- | * The type parameter `b` describes whether the path is `File` or `Dir`. + -- | * The type parameter `s` describes whether the path is `Sandboxed` or `Unsandboxed`. + -- | + -- | To ensure type safety, there is no way for users to create a value of + -- | this type directly. Instead, helpers should be used, such as `rootDir`, + -- | `currentDir`, `file`, `dir`, `()`, and `parsePath`. + -- | + -- | This ADT allows invalid paths (e.g. paths inside files), but there is no + -- | possible way for such paths to be constructed by user-land code. The only + -- | "invalid path" that may be constructed is using the `parentDir'` function, e.g. + -- | `parentDir' rootDir`, or by parsing an equivalent string such as `/../`, + -- | but such paths are marked as unsandboxed, and may not be rendered to strings + -- | until they are first sandboxed to some directory. + data Path a b s = Current | Root | ParentIn (Path a b s) | DirIn (Path a b s) DirName | FileIn (Path a b s) FileName + + -- | A type describing a file whose location is given relative to some other, + -- | unspecified directory (referred to as the "current directory"). + type RelFile s = Path Rel File s + + -- | A type describing a file whose location is absolutely specified. + type AbsFile s = Path Abs File s + + -- | A type describing a directory whose location is given relative to some + -- | other, unspecified directory (referred to as the "current directory"). + type RelDir s = Path Rel Dir s + + -- | A type describing a directory whose location is absolutely specified. + type AbsDir s = Path Abs Dir s + + -- | Escapers encode segments or characters which have reserved meaning. + newtype Escaper = Escaper (String -> String) + + -- | Given an escaper and a segment to encode, returns the encoded segment. + runEscaper :: Escaper -> String -> String + runEscaper (Escaper f) = f + + -- | An escaper that does nothing except remove slashes (the bare minimum of + -- | what must be done). + nonEscaper :: Escaper + nonEscaper = Escaper $ \s -> S.joinWith "" $ filter ((/=) "/") (S.split "" s) + + -- | An escaper that removes all slashes, converts ".." into "$dot$dot", and + -- | converts "." into "$dot". + posixEscaper :: Escaper + posixEscaper = Escaper $ runEscaper nonEscaper >>> \s -> if s == ".." then "$dot$dot" else if s == "." then "$dot" else s + + -- | Creates a path which points to a relative file of the specified name. + file :: forall s. String -> Path Rel File s + file f = file' (FileName f) + + -- | Creates a path which points to a relative file of the specified name. + file' :: forall s. FileName -> Path Rel File s + file' f = FileIn Current f + + -- | Retrieves the name of a file path. + fileName :: forall a s. Path a File s -> FileName + fileName (FileIn _ f) = f + fileName _ = FileName "" + + -- | Retrieves the extension of a file name. + extension :: FileName -> String + extension (FileName f) = let idx = S.lastIndexOf "." f in if idx == -1 then "" else S.drop (idx + 1) f + + -- | Drops the extension on a file name. + dropExtension :: FileName -> FileName + dropExtension (FileName n) = + let idx = S.lastIndexOf "." n + in if idx == -1 then FileName n else FileName $ S.take idx n + + -- | Changes the extension on a file name. + changeExtension :: forall a s. (String -> String) -> FileName -> FileName + changeExtension f nm @ (FileName n) = + let + ext = f $ extension nm + in (\(FileName n) -> if ext == "" then FileName n else FileName $ n ++ "." ++ ext) (dropExtension nm) + + -- | Retrieves the name of a directory path. Not all paths have such a name, + -- | for example, the root or current directory. + dirName :: forall a s. Path a Dir s -> Maybe DirName + dirName p = case canonicalize p of + (DirIn _ d) -> Just d + _ -> Nothing + + -- | Creates a path which points to a relative directory of the specified name. + dir :: String -> Path Rel Dir Sandboxed + dir d = dir' (DirName d) + + -- | Creates a path which points to a relative directory of the specified name. + dir' :: forall s. DirName -> Path Rel Dir s + dir' d = DirIn Current d + + -- | Given a directory path, appends either a file or directory to the path. + () :: forall a b s. Path a Dir s -> Path Rel b s -> Path a b s + () (Current ) (Current ) = Current + () (Root ) (Current ) = Root + () (ParentIn p1 ) (Current ) = ParentIn (p1 Current) + () (FileIn p1 f1) (Current ) = FileIn (p1 Current) f1 + () (DirIn p1 d1) (Current ) = DirIn (p1 Current) d1 + () (Current ) (Root ) = Current -- doesn't make sense but cannot exist + () (Root ) (Root ) = Root -- doesn't make sense but cannot exist + () (ParentIn p1 ) (Root ) = ParentIn (p1 Current) -- doesn't make sense but cannot exist + () (FileIn p1 f1) (Root ) = FileIn (p1 Current) f1 -- doesn't make sense but cannot exist + () (DirIn p1 d1) (Root ) = DirIn (p1 Current) d1 -- doesn't make sense but cannot exist + () (p1 ) (ParentIn p2 ) = ParentIn (p1 p2 ) + () (p1 ) (FileIn p2 f2) = FileIn (p1 p2 ) f2 + () (p1 ) (DirIn p2 d2) = DirIn (p1 p2 ) d2 + + -- | Sets the extension of the file to the specified extension. + -- | + -- | ```purescript + -- | file "image" <.> "png" + -- | ``` + (<.>) :: forall a s. Path a File s -> String -> Path a File s + (<.>) p ext = renameFile (changeExtension $ const ext) p + + -- | Determines if this path is absolutely located. + isAbsolute :: forall a b s. Path a b s -> Boolean + isAbsolute (Current ) = false + isAbsolute (Root ) = true + isAbsolute (ParentIn p ) = isAbsolute p + isAbsolute (FileIn p _) = isAbsolute p + isAbsolute (DirIn p _) = isAbsolute p + + -- | Determines if this path is relatively located. + isRelative :: forall a b s. Path a b s -> Boolean + isRelative p = not $ isAbsolute p + + -- | Peels off the last directory and the terminal file or directory name + -- | from the path. Returns `Nothing` if there is no such pair (for example, + -- | if the last path segment is root directory, current directory, or parent + -- | directory). + peel :: forall a b s. Path a b s -> Maybe (Tuple (Path a Dir s) (Either DirName FileName)) + peel (Current ) = Nothing + peel (Root ) = Nothing + peel (ParentIn p ) = peel p >>= (fst >>> peel) + peel (DirIn p d) = Just $ maybe (if isAbsolute p then Tuple Root d' else Tuple Current d') + (\(Tuple p e) -> Tuple (either (DirIn p) (FileIn p) e) d') (peel p) + where d' = Left d + peel (FileIn p f) = Just $ maybe (if isAbsolute p then Tuple Root f' else Tuple Current f') + (\(Tuple p e) -> Tuple (either (DirIn p) (FileIn p) e) f') (peel p) + where f' = Right f + + -- | Attempts to extract out the parent directory of the specified path. If the + -- | function would have to use a relative path in the return value, the function will + -- | instead return `Nothing`. + parentDir :: forall a b s. Path a b s -> Maybe (Path a Dir s) + parentDir p = fst <$> peel p + + -- | Unsandboxes any path (whether sandboxed or not). + unsandbox :: forall a b s. Path a b s -> Path a b Unsandboxed + unsandbox (Current ) = Current + unsandbox (Root ) = Root + unsandbox (ParentIn p ) = ParentIn (unsandbox p) + unsandbox (DirIn p d) = DirIn (unsandbox p) d + unsandbox (FileIn p f) = FileIn (unsandbox p) f + + -- | Extracts out the parent directory of the specified path. Will use the + -- | parent path segment (..) if strictly necessary and therefore can escape + -- | a sandboxed path. + parentDir' :: forall a b s. Path a b s -> Path a Dir Unsandboxed + parentDir' (Current ) = ParentIn Current + parentDir' (Root ) = ParentIn Root + parentDir' (ParentIn p ) = ParentIn (parentDir' p) + parentDir' (FileIn p f) = maybe (if isAbsolute p then Root else Current) + (\(Tuple p e) -> either (DirIn p) (FileIn p) e) (first unsandbox <$> peel p) + parentDir' (DirIn p f) = maybe (if isAbsolute p then Root else Current) + (\(Tuple p e) -> either (DirIn p) (FileIn p) e) (first unsandbox <$> peel p) + + -- | The "current directory", which can be used to define relatively-located resources. + currentDir :: Path Rel Dir Sandboxed + currentDir = Current + + -- | The root directory, which can be used to define absolutely-located resources. + rootDir :: Path Abs Dir Sandboxed + rootDir = Root + + -- | Renames a file path. + renameFile :: forall a s. (FileName -> FileName) -> Path a File s -> Path a File s + renameFile f = go + where + go (FileIn p f0) = FileIn p (f f0) + go (p ) = p + + -- | Renames a directory path. Note: This is a simple rename of the terminal + -- | directory name, not a "move". + renameDir :: forall a s. (DirName -> DirName) -> Path a Dir s -> Path a Dir s + renameDir f = go + where + go (DirIn p d) = DirIn p (f d) + go (p ) = p + + -- | Canonicalizes a path, by reducing things in the form `/x/../` to just `/x/`. + canonicalize :: forall a b s. Path a b s -> Path a b s + canonicalize p = snd $ canonicalize' p + + -- | Canonicalizes a path and returns information on whether or not it actually changed. + canonicalize' :: forall a b s. Path a b s -> Tuple Boolean (Path a b s) + canonicalize' (Current ) = Tuple false Current + canonicalize' (Root ) = Tuple false Root + canonicalize' (ParentIn (FileIn p f)) = Tuple true p + canonicalize' (ParentIn (DirIn p f)) = Tuple true p + canonicalize' (ParentIn (p )) = (\(Tuple changed p') -> + let p'' = ParentIn p' in if changed then canonicalize' p'' else Tuple changed p'') $ canonicalize' p + canonicalize' (FileIn p f ) = flip FileIn f <$> canonicalize' p + canonicalize' (DirIn p d ) = flip DirIn d <$> canonicalize' p + + unsafePrintPath' :: forall a b s. Escaper -> Path a b s -> String + unsafePrintPath' r p = go p + where + go (Current) = "./" + go (Root) = "/" + go (ParentIn p) = go p ++ "../" + go (DirIn p @ (FileIn _ _ ) (DirName d)) = go p ++ "/" ++ d ++ "/" -- dir inside a file + go (DirIn p (DirName d)) = go p ++ d ++ "/" -- dir inside a dir + go (FileIn p @ (FileIn _ _) (FileName f)) = go p ++ "/" ++ f -- file inside a file + go (FileIn p (FileName f)) = go p ++ f + + unsafePrintPath :: forall a b s. Path a b s -> String + unsafePrintPath = unsafePrintPath' posixEscaper + + -- | Prints a `Path` into its canonical `String` representation. For security + -- | reasons, the path must be sandboxed before it can be rendered to a string. + printPath :: forall a b. Path a b Sandboxed -> String + printPath = unsafePrintPath + + -- | Prints a `Path` into its canonical `String` representation, using the + -- | specified escaper to escape special characters in path segments. For + -- | security reasons, the path must be sandboxed before rendering to string. + printPath' :: forall a b. Escaper -> Path a b Sandboxed -> String + printPath' = unsafePrintPath' + + -- | Determines if two paths have the exact same representation. Note that + -- | two paths may represent the same path even if they have different + -- | representations! + identicalPath :: forall a a' b b' s s'. Path a b s -> Path a' b' s' -> Boolean + identicalPath p1 p2 = show p1 == show p2 + + -- | Makes one path relative to another reference path, if possible, otherwise + -- | returns `Nothing`. The returned path inherits the sandbox settings of the + -- | reference path. + -- | + -- | Note there are some cases this function cannot handle. + relativeTo :: forall a b s s'. Path a b s -> Path a Dir s' -> Maybe (Path Rel b s') + relativeTo p1 p2 = relativeTo' (canonicalize p1) (canonicalize p2) where + relativeTo' :: forall a b s s'. Path a b s -> Path a Dir s' -> Maybe (Path Rel b s') + relativeTo' p1 p2 = + if identicalPath p1 p2 then Just Current else case peel p1 of + Nothing -> case Tuple p1 p2 of + Tuple Root Root -> Just Current + Tuple Current Current -> Just Current + _ -> Nothing + Just (Tuple p1' e) -> flip () (either (DirIn Current) (FileIn Current) e) <$> relativeTo' p1' p2 + + -- | Attempts to sandbox a path relative to some directory. If successful, the sandboxed + -- | directory will be returned relative to the sandbox directory (although this can easily + -- | be converted into an absolute path using ``). + -- | + -- | This combinator can be used to ensure that paths which originate from user-code + -- | cannot access data outside a given directory. + sandbox :: forall a b s. Path a Dir Sandboxed -> Path a b s -> Maybe (Path Rel b Sandboxed ) + sandbox p1 p2 = p2 `relativeTo` p1 + + -- | Refines path segments but does not change anything else. + refine :: forall a b s. (FileName -> FileName) -> (DirName -> DirName) -> Path a b s -> Path a b s + refine f d = go + where go (Current ) = Current + go (Root ) = Root + go (ParentIn p ) = ParentIn (go p) + go (DirIn p d0) = DirIn (go p) (d d0) + go (FileIn p f0) = FileIn (go p) (f f0) + + -- | Parses a canonical `String` representation of a path into a `Path` value. + -- | Note that in order to be unambiguous, trailing directories should be + -- | marked with a trailing slash character (`'/'`). + parsePath :: forall z. + (RelFile Unsandboxed -> z) -> + (AbsFile Unsandboxed -> z) -> + (RelDir Unsandboxed -> z) -> + (AbsDir Unsandboxed -> z) -> String -> z + parsePath rf af rd ad p = + let + segs = filter (\s -> S.length s > 0) (S.split "/" p) + filename = let i = S.lastIndexOf "/" p in if i == -1 then "" else S.drop (i + 1) p + lastIndex = length segs - 1 + tuples = zipWith Tuple segs (range 0 lastIndex) + + folder :: forall a b s. Tuple String Number -> (Path a b s -> Path a b s) -> (Path a b s -> Path a b s) + folder (Tuple seg idx) f = case idx of + idx | idx == 0 -> const $ f + (if seg == "." then Current else + if seg == ".." then ParentIn Current else + if seg == "" then Root else + if idx == lastIndex then FileIn Current (FileName seg) else + DirIn Current (DirName seg)) + idx -> if seg == "." then f else + if seg == ".." then \p -> ParentIn (f p) else + if seg == "" then f else + \p -> DirIn p (DirName seg) + in + if p == "" then rd Current + else if S.take 1 p == "/" then (if filename == "" then ad (foldr folder id tuples Root) else af (foldr folder id tuples Root)) + else if filename == "" then rd (foldr folder id tuples Root) else rf (foldr folder id tuples Root) + + -- | Attempts to parse a relative file from a string. + parseRelFile :: String -> Maybe (RelFile Unsandboxed) + parseRelFile = parsePath Just (const Nothing) (const Nothing) (const Nothing) + + -- | Attempts to parse an absolute file from a string. + parseAbsFile :: String -> Maybe (AbsFile Unsandboxed) + parseAbsFile = parsePath (const Nothing) Just (const Nothing) (const Nothing) + + -- | Attempts to parse a relative directory from a string. + parseRelDir :: String -> Maybe (RelDir Unsandboxed) + parseRelDir = parsePath (const Nothing) (const Nothing) Just (const Nothing) + + -- | Attempts to parse an absolute directory from a string. + parseAbsDir :: String -> Maybe (AbsDir Unsandboxed) + parseAbsDir = parsePath (const Nothing) (const Nothing) (const Nothing) Just + + instance showPath :: Show (Path a b s) where + show (Current ) = "currentDir" + show (Root ) = "rootDir" + show (ParentIn p ) = "(parentDir' " ++ show p ++ ")" + show (FileIn p (FileName f)) = show p ++ "(file " ++ f ++ ")" + show (DirIn p (DirName f)) = show p ++ "(dir " ++ f ++ ")" + + instance eqPath :: Eq (Path a b s) where + (==) p1 p2 = canonicalize p1 == canonicalize p2 + + (/=) p1 p2 = not (p1 == p2) \ No newline at end of file