diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30efe19 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/bower_components/ +/node_modules/ +/.pulp-cache/ +/output/ +/generated-docs/ +/.psc-package/ +/.psc* +/.purs* +/.psa* +/.spago diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..393d145 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 purescript-contrib + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..26b0e77 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# purescript-js-blob +Low-level bindings for the [`Blob` API](https://developer.mozilla.org/en-US/docs/Web/API/Blob/Blob). + +**Note:** If you are using Node.js then you will need Node.js version >= v15.7.0, v14.18.0 diff --git a/package.json b/package.json new file mode 100644 index 0000000..270c201 --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "purescript-js-blob", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "build": "spago build", + "test": "spago -x test.dhall test" + }, + "keywords": [], + "author": "", + "license": "MIT" +} diff --git a/packages.dhall b/packages.dhall new file mode 100644 index 0000000..a03e719 --- /dev/null +++ b/packages.dhall @@ -0,0 +1,105 @@ +{- +Welcome to your new Dhall package-set! + +Below are instructions for how to edit this file for most use +cases, so that you don't need to know Dhall to use it. + +## Use Cases + +Most will want to do one or both of these options: +1. Override/Patch a package's dependency +2. Add a package not already in the default package set + +This file will continue to work whether you use one or both options. +Instructions for each option are explained below. + +### Overriding/Patching a package + +Purpose: +- Change a package's dependency to a newer/older release than the + default package set's release +- Use your own modified version of some dependency that may + include new API, changed API, removed API by + using your custom git repo of the library rather than + the package set's repo + +Syntax: +where `entityName` is one of the following: +- dependencies +- repo +- version +------------------------------- +let upstream = -- +in upstream + with packageName.entityName = "new value" +------------------------------- + +Example: +------------------------------- +let upstream = -- +in upstream + with halogen.version = "master" + with halogen.repo = "https://example.com/path/to/git/repo.git" + + with halogen-vdom.version = "v4.0.0" + with halogen-vdom.dependencies = [ "extra-dependency" ] # halogen-vdom.dependencies +------------------------------- + +### Additions + +Purpose: +- Add packages that aren't already included in the default package set + +Syntax: +where `` is: +- a tag (i.e. "v4.0.0") +- a branch (i.e. "master") +- commit hash (i.e. "701f3e44aafb1a6459281714858fadf2c4c2a977") +------------------------------- +let upstream = -- +in upstream + with new-package-name = + { dependencies = + [ "dependency1" + , "dependency2" + ] + , repo = + "https://example.com/path/to/git/repo.git" + , version = + "" + } +------------------------------- + +Example: +------------------------------- +let upstream = -- +in upstream + with benchotron = + { dependencies = + [ "arrays" + , "exists" + , "profunctor" + , "strings" + , "quickcheck" + , "lcg" + , "transformers" + , "foldable-traversable" + , "exceptions" + , "node-fs" + , "node-buffer" + , "node-readline" + , "datetime" + , "now" + ] + , repo = + "https://github.com/hdgarrood/purescript-benchotron.git" + , version = + "v7.0.0" + } +------------------------------- +-} +let upstream = + https://github.com/purescript/package-sets/releases/download/psc-0.15.7-20230310/packages.dhall + sha256:c30c50d19c9eb55516b0a8a1bd368a0754bde47365be36abadb489295d86d77c + +in upstream diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..17b8a8b --- /dev/null +++ b/spago.dhall @@ -0,0 +1,17 @@ +{ name = "js-blob" +, dependencies = + [ "arrays" + , "console" + , "effect" + , "integers" + , "maybe" + , "media-types" + , "newtype" + , "nullable" + , "numbers" + , "prelude" + , "unsafe-coerce" + ] +, packages = ./packages.dhall +, sources = [ "src/**/*.purs" ] +} diff --git a/src/Js/Blob.js b/src/Js/Blob.js new file mode 100644 index 0000000..4aa1150 --- /dev/null +++ b/src/Js/Blob.js @@ -0,0 +1,65 @@ +export function typeImpl(blob) { + return blob.type; +} + +const fromSources = function (sources, options) { + if (options === null) { + return new Blob(sources); + } else { + return new Blob(sources, options); + } +}; + +export const fromStringsImpl = function (sources) { + return function (options) { + return fromSources(sources, options); + }; +}; + +export function size(blob) { + return blob.size; +} + +export function sliceImpl(contentType) { + return function (start) { + return function (end) { + return function (blob) { + if (contentType != null) { + return blob.slice(start, end, contentType); + } else { + return blob.slice(start, end); + } + }; + }; + }; +} + +export const text = function (blob) { + return function () { + return blob.text(); + }; +}; + +export const toArrayBuffer = function (blob) { + return function () { + return blob.arrayBuffer(); + }; +}; + +export const fromArrayBuffersImpl = function (sources) { + return function (options) { + return fromSources(sources, options); + }; +}; + +export const fromBlobsImpl = function (sources) { + return function (options) { + return fromSources(sources, options); + }; +}; + +export const fromDataViewImpl = function (sources) { + return function (options) { + return fromSources(sources, options); + }; +}; diff --git a/src/Js/Blob.purs b/src/Js/Blob.purs new file mode 100644 index 0000000..5c7b640 --- /dev/null +++ b/src/Js/Blob.purs @@ -0,0 +1,151 @@ +module Js.Blob + ( Blob + , BlobEnding(..) + , BlobOptions + , ByteIdx + , EndByte(..) + , StartByte(..) + , fromArrayBuffers + , fromBlobs + , fromDataView + , fromString + , fromStrings + , idxFromInt + , idxFromNumber + , size + , slice + , slice' + , text + , toArrayBuffer + , type_ + ) where + +import Control.Applicative ((<#>)) +import Data.Array.NonEmpty (NonEmptyArray) +import Data.Array.NonEmpty as NonEmptyArray +import Data.ArrayBuffer.Types (ArrayBuffer, DataView) +import Data.Int (toNumber) +import Data.Maybe (Maybe(..)) +import Data.MediaType (MediaType(..)) +import Data.Newtype (un) +import Data.Nullable (Nullable, toNullable) +import Data.Nullable as Nullable +import Data.Number (round) +import Effect (Effect) +import Prelude ((#), (==), (>>>)) +import Promise (Promise) +import Unsafe.Coerce (unsafeCoerce) + +foreign import data Blob :: Type + +data BlobEnding = Transparent | Native + +type BlobOptions = + { "type" :: MediaType + , endings :: BlobEnding + } + +type BlobOptionsImpl = + { "type" :: String + , endings :: String + } + +toBlobOptionsImpl :: BlobOptions -> BlobOptionsImpl +toBlobOptionsImpl { "type": mediaType, endings } = + { "type": un MediaType mediaType + , endings: toEndings endings + } + where + toEndings Transparent = "transparent" + toEndings Native = "native" + +foreign import fromStringsImpl :: Array String -> Nullable BlobOptionsImpl -> Blob + +-- | Creates a String with the given Mediatype +-- | For example: +-- | ``` +-- | myBlob = fromString (unsafeStringify { name: "Carl", age: 25 }) (MediaType "application/json") +-- | ``` +fromString :: String -> Maybe BlobOptions -> Blob +fromString strs opts = fromStringsImpl [ strs ] (opts <#> toBlobOptionsImpl # toNullable) + +-- | Creates a new Blob from one or more strings +fromStrings :: NonEmptyArray String -> Maybe BlobOptions -> Blob +fromStrings strs opts = fromStringsImpl (NonEmptyArray.toArray strs) (opts <#> toBlobOptionsImpl # toNullable) + +foreign import typeImpl :: Blob -> String + +-- | `MediaType` of the data contained in the `Blob`. +-- | Returns `Nothing` if the `MediaType` is unknown. +type_ :: Blob -> Maybe MediaType +type_ blob = + let + blobType = typeImpl blob + in + if blobType == "" then Nothing + else Just (MediaType blobType) + +-- | The size (in bytes) of the data contained in the `Blob`. +foreign import size :: Blob -> Int + +-- | An index into the Blob indicating the first byte to include in the new Blob. +-- | If you specify a negative value, it's treated as an offset from the end of the +-- | string toward the beginning. For example, -10 would be the 10th from last byte +-- | in the Blob. If you specify a value for start that is larger than the size +-- | of the source Blob, the returned Blob has size 0 and contains no data. +newtype StartByte = StartByte ByteIdx + +-- | An index into the Blob indicating the first byte that will *not* be included +-- | in the new Blob (i.e. the byte exactly at this index is not included). +-- | If you specify a negative value, it's treated as an offset from the end of +-- | the string toward the beginning. For example, -10 would be the 10th from +-- | last byte in the Blob. The default value is size. +newtype EndByte = EndByte ByteIdx + +foreign import data ByteIdx :: Type + +-- | Creates `ByteIdx` from `Int` value +idxFromInt :: Int -> ByteIdx +idxFromInt = toNumber >>> unsafeCoerce + +-- | Creates `ByteIdx` from `Number` value using `Math.round`. +idxFromNumber :: Number -> ByteIdx +idxFromNumber = round >>> unsafeCoerce + +-- | Creates a new `Blob` object (with specified `MediaType`), containing the +-- | data in the specified range of bytes of the source Blob, by setting . +foreign import sliceImpl ∷ Nullable MediaType -> StartByte -> EndByte -> Blob -> Blob + +-- | Creates a new `Blob` object containing the data in the specified range +-- | of bytes of the source Blob. +slice ∷ MediaType -> StartByte -> EndByte -> Blob -> Blob +slice mt = sliceImpl (Nullable.notNull mt) + +-- | Creates a new `Blob` object containing the data in the specified range +-- | of bytes of the source Blob. +slice' ∷ StartByte -> EndByte -> Blob -> Blob +slice' = sliceImpl (Nullable.null) + +-- | Returns a promise that fulfills with the contents of the Blob decoded as a UTF-8 string. +foreign import text :: Blob -> Effect (Promise String) + +-- | Copies the data in the Blob to a new JS ArrayBuffer +foreign import toArrayBuffer :: Blob -> Effect (Promise ArrayBuffer) + +foreign import fromArrayBuffersImpl :: NonEmptyArray ArrayBuffer -> Nullable BlobOptionsImpl -> Blob + +-- | Creates a new Blob from one ore more `ArrayBuffer`s +fromArrayBuffers :: NonEmptyArray ArrayBuffer -> Maybe BlobOptions -> Blob +fromArrayBuffers strs opts = fromArrayBuffersImpl strs (opts <#> toBlobOptionsImpl # toNullable) + +foreign import fromBlobsImpl :: NonEmptyArray Blob -> Nullable BlobOptionsImpl -> Blob + +-- | Creates a new Blob from one ore more `Blob`s +fromBlobs :: NonEmptyArray Blob -> Maybe BlobOptions -> Blob +fromBlobs strs opts = fromBlobsImpl strs (opts <#> toBlobOptionsImpl # toNullable) + +foreign import fromDataViewImpl :: NonEmptyArray DataView -> Nullable BlobOptionsImpl -> Blob + +-- | Creates a new Blob from one ore more `DataView`s +fromDataView :: NonEmptyArray DataView -> Maybe BlobOptions -> Blob +fromDataView strs opts = fromDataViewImpl strs (opts <#> toBlobOptionsImpl # toNullable) diff --git a/test.dhall b/test.dhall new file mode 100644 index 0000000..0642e61 --- /dev/null +++ b/test.dhall @@ -0,0 +1,6 @@ +let conf = ./spago.dhall + +in conf + // { sources = conf.sources # [ "test/**/*.purs" ] + , dependencies = conf.dependencies # [ "assert", "aff", "arrays", "js-promise", "js-promise-aff", "arraybuffer-types" ] + } diff --git a/test/Js/Blob.js b/test/Js/Blob.js new file mode 100644 index 0000000..9db1189 --- /dev/null +++ b/test/Js/Blob.js @@ -0,0 +1,5 @@ + +export const testDataView = () => { + const buffer = new ArrayBuffer(16); + return new DataView(buffer, 2, 10); +}; diff --git a/test/Js/Blob.purs b/test/Js/Blob.purs new file mode 100644 index 0000000..e5f2479 --- /dev/null +++ b/test/Js/Blob.purs @@ -0,0 +1,103 @@ +module Test.Js.Blob + ( test + ) where + +import Prelude + +import Data.Array.NonEmpty ((:)) +import Data.Array.NonEmpty as NEA +import Data.ArrayBuffer.Types (DataView) +import Data.Maybe (Maybe(..)) +import Data.MediaType (MediaType(..)) +import Data.MediaType.Common as MediaTypes +import Effect (Effect) +import Effect.Aff (launchAff_) +import Effect.Class (liftEffect) +import Effect.Class.Console (log) +import Js.Blob as Blob +import Promise.Aff as Promise +import Test.Assert (assertEqual) + +foreign import testDataView :: Effect DataView + +test :: Effect Unit +test = launchAff_ do + log "Testing fromStrings with no options" + testFromStringsNoOptions + log "Testing fromStrings with options" + testFromStringsWithOptions + log "Testing fromArrayBuffer / toArrayBuffer" + testFromBlobs + log "Testing fromDataView" + testFromDataView + log "Testing fromBlobs" + testToFromArrayBuffer + log "Testing size" + testSize + log "Testing slice" + testSlice + log "Test type" + testType + where + + testFromStringsNoOptions = do + let + input = "hello" : (NEA.singleton "world") + expected = NEA.fold1 input + blob = Blob.fromStrings input Nothing + actual <- Promise.toAffE $ Blob.text blob + liftEffect $ assertEqual { actual, expected } + + testFromStringsWithOptions = do + let + input = "{\n\"hello\":\"world\"\n}" : (NEA.singleton "{\n\"hola\":\"mundo\"\n}") + expected = NEA.fold1 input + blob = Blob.fromStrings input (Just { "type": MediaTypes.applicationJSON, endings: Blob.Transparent }) + actual <- Promise.toAffE $ Blob.text blob + liftEffect $ assertEqual { actual, expected } + + testToFromArrayBuffer = do + let + expected = "helloworld" + blob = Blob.fromStrings (NEA.singleton expected) Nothing + buffer <- Promise.toAffE $ Blob.toArrayBuffer blob + actual <- Promise.toAffE $ Blob.text $ Blob.fromArrayBuffers (NEA.singleton buffer) Nothing + liftEffect $ assertEqual { actual, expected } + + testFromBlobs = do + let + expected = "helloworld" + blob1 = Blob.fromStrings (NEA.singleton "hello") Nothing + blob2 = Blob.fromStrings (NEA.singleton "world") Nothing + blob = Blob.fromBlobs (blob1 : NEA.singleton blob2) Nothing + actual <- Promise.toAffE $ Blob.text $ blob + liftEffect $ assertEqual { actual, expected } + + testFromDataView = do + typedArray <- liftEffect testDataView + let + expected = 10 + blob = Blob.fromDataView (NEA.singleton typedArray) Nothing + actual = Blob.size $ blob + liftEffect $ assertEqual { actual, expected } + + testSize = do + let + expected = 11 + blob = Blob.fromStrings (NEA.singleton "hello world") Nothing + actual = Blob.size blob + liftEffect $ assertEqual { actual, expected } + + testSlice = do + let + expected = "ello wor" + blob = Blob.fromStrings (NEA.singleton "hello world") Nothing + actual <- Promise.toAffE $ Blob.text $ Blob.slice' (Blob.StartByte $ Blob.idxFromInt 1) (Blob.EndByte $ Blob.idxFromInt 9) blob + liftEffect $ assertEqual { actual, expected } + + testType = do + let + expected = Just $ MediaType "text/plain" + blob = Blob.fromStrings (NEA.singleton "hello world") (Just { "type": MediaTypes.textPlain, endings: Blob.Transparent }) + actual = Blob.type_ blob + liftEffect $ assertEqual { actual, expected } diff --git a/test/Main.purs b/test/Main.purs new file mode 100644 index 0000000..d7c38f6 --- /dev/null +++ b/test/Main.purs @@ -0,0 +1,9 @@ +module Test.Main where + +import Prelude +import Effect (Effect) +import Test.Js.Blob as Blob + +main :: Effect Unit +main = Blob.test +