From 17ead262eb972717f5c22b49bc32acce805d5c79 Mon Sep 17 00:00:00 2001 From: Mark Eibes Date: Mon, 20 Feb 2023 15:17:02 +0100 Subject: [PATCH] Create library --- .gitignore | 10 +++ LICENCE | 7 ++ packages.dhall | 105 +++++++++++++++++++++++++ spago.dhall | 34 ++++++++ src/WebExtension/Polyfill.js | 22 ++++++ src/WebExtension/Polyfill.purs | 113 +++++++++++++++++++++++++++ src/WebExtension/Polyfill/Types.purs | 10 +++ 7 files changed, 301 insertions(+) create mode 100644 .gitignore create mode 100644 LICENCE create mode 100644 packages.dhall create mode 100644 spago.dhall create mode 100644 src/WebExtension/Polyfill.js create mode 100644 src/WebExtension/Polyfill.purs create mode 100644 src/WebExtension/Polyfill/Types.purs 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/LICENCE b/LICENCE new file mode 100644 index 0000000..febc9cc --- /dev/null +++ b/LICENCE @@ -0,0 +1,7 @@ +Copyright 2022 Mark Eibes + +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/packages.dhall b/packages.dhall new file mode 100644 index 0000000..17a8f3a --- /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-20230219/packages.dhall + sha256:2ba900b6b1cdeb3ad8f1554d1ceaafede6a9451dc009f96ef45754476d00c900 + +in upstream diff --git a/spago.dhall b/spago.dhall new file mode 100644 index 0000000..ebe1efd --- /dev/null +++ b/spago.dhall @@ -0,0 +1,34 @@ +{- +Welcome to a Spago project! +You can edit this file as you like. + +Need help? See the following resources: +- Spago documentation: https://github.com/purescript/spago +- Dhall language tour: https://docs.dhall-lang.org/tutorials/Language-Tour.html + +When creating a new Spago project, you can use +`spago init --no-comments` or `spago init -C` +to generate this file without the comments in this block. +-} +{ name = "webextension-polyfill" +, repository = "https://github.com/rowtype-yoga/purescript-webextension-polyfill.git" +, license = "MIT" +, dependencies = + [ "aff" + , "aff-promise" + , "console" + , "effect" + , "either" + , "foldable-traversable" + , "foreign" + , "foreign-generic" + , "foreign-object" + , "maybe" + , "newtype" + , "prelude" + , "psci-support" + , "yoga-json" + ] +, packages = ../../packages.dhall +, sources = [ "src/**/*.purs", "test/**/*.purs" ] +} diff --git a/src/WebExtension/Polyfill.js b/src/WebExtension/Polyfill.js new file mode 100644 index 0000000..3b944d2 --- /dev/null +++ b/src/WebExtension/Polyfill.js @@ -0,0 +1,22 @@ +import browser, { storage, history as _history, runtime, windows } from 'webextension-polyfill' + +export const browserImpl = browser +export function loadFromLocalStorageImpl(key) { return () => storage.local.get(key)} +export function saveInLocalStorageImpl(obj) { return () => storage.local.set(obj)} +export function removeFromLocalStorageImpl(key) { return () => storage.local.remove(key)} + +export function history() { return _history} + +export function addOnVisitedListenerImpl(listener) { return history => () => history.onVisited.addListener(listener)} + +export function sendMessageViaPortImpl(message, port) { return port.postMessage(message)} +export function addOnPortMessageListenerImpl(listener, port) { return port.onMessage.addListener(listener)} + +export function onContentScriptConnectedImpl(listener) { return () => + runtime.onConnect.addListener(port => listener(port)())} + +export const connectToBackgroundScriptImpl = runtime.connect + +export function openNewWindowImpl(options) { return () => windows.create(options)} + +export const reload = runtime.reload \ No newline at end of file diff --git a/src/WebExtension/Polyfill.purs b/src/WebExtension/Polyfill.purs new file mode 100644 index 0000000..bee5a1f --- /dev/null +++ b/src/WebExtension/Polyfill.purs @@ -0,0 +1,113 @@ +module WebExtension.Polyfill where + +import Prelude +import Control.Promise (Promise, toAffE) +import Data.Either (Either(..)) +import Data.Maybe (Maybe(..)) +import Data.Semigroup.Foldable (intercalateMap) +import Effect (Effect) +import Effect.Aff (Aff) +import Effect.Class.Console as Console +import Effect.Uncurried (EffectFn1, EffectFn2, mkEffectFn1, runEffectFn1, runEffectFn2) +import Foreign (Foreign, renderForeignError) +import Foreign.Internal.Stringify (unsafeStringify) +import Foreign.Object (Object) +import Foreign.Object as Object +import Prim.Row (class Union) +import Yoga.JSON (class ReadForeign, class WriteForeign, read) +import Yoga.JSON as JSON +import WebExtension.Polyfill.Types (StorageKey(..)) + +foreign import browserImpl ∷ Type + +-- Unusal API. The keys could also be an array in which case there'd be an object +-- with keys for the array of strings passed in originally values. Maybe this is +-- for efficiency reasons when using the "sync" API which is apparently kind of +-- throttled. Until then, we store keys and values. +foreign import data LocalStorage ∷ Type + +foreign import loadFromLocalStorageImpl ∷ String -> Effect (Promise (Object Foreign)) + +foreign import saveInLocalStorageImpl ∷ Object Foreign -> Effect (Promise Unit) + +foreign import removeFromLocalStorageImpl ∷ String -> Effect (Promise Unit) + +loadFromLocalStorage ∷ ∀ a. ReadForeign a => StorageKey -> Aff (Maybe a) +loadFromLocalStorage (StorageKey key) = do + Console.info $ "Loading from Local Storage " <> key + result <- toAffE $ loadFromLocalStorageImpl key + pure case Object.lookup key result of + Nothing -> Nothing + Just raw -> JSON.read_ raw + +saveInLocalStorage ∷ ∀ a. WriteForeign a => StorageKey -> a -> Aff Unit +saveInLocalStorage (StorageKey key) value = do + Console.info $ "Saving in Local Storage " <> key <> " : " <> JSON.writeJSON value + saveInLocalStorageImpl (Object.singleton key $ JSON.write value) # toAffE + +removeFromLocalStorage ∷ StorageKey -> Aff Unit +removeFromLocalStorage (StorageKey key) = do + Console.info $ "Removing from Local Storage " <> key + removeFromLocalStorageImpl key # toAffE + +foreign import data Port ∷ Type + +foreign import sendMessageViaPortImpl ∷ EffectFn2 Foreign Port Unit + +sendMessageViaPort ∷ ∀ msg. WriteForeign msg => msg -> Port -> Effect Unit +sendMessageViaPort m = runEffectFn2 sendMessageViaPortImpl (JSON.write m) + +foreign import addOnPortMessageListenerImpl ∷ EffectFn2 (EffectFn1 Foreign Unit) Port Unit + +addOnPortMessageListener ∷ (Foreign -> Effect Unit) -> Port -> Effect Unit +addOnPortMessageListener listener = runEffectFn2 addOnPortMessageListenerImpl (mkEffectFn1 listener) + +foreign import onContentScriptConnectedImpl ∷ (Port -> Effect Unit) -> Effect Unit + +onContentScriptConnected ∷ (Port -> Effect Unit) -> Effect Unit +onContentScriptConnected = onContentScriptConnectedImpl + +type ConnectOptions = + ( name ∷ String ) + +foreign import connectToBackgroundScriptImpl ∷ ∀ opts. EffectFn1 (Record opts) Port + +connectToBackgroundScript ∷ ∀ opts missing. Union opts missing ConnectOptions => Record opts -> Effect Port +connectToBackgroundScript = runEffectFn1 connectToBackgroundScriptImpl + +foreign import openNewWindowImpl ∷ ∀ opts. Record opts -> Effect Unit + +openNewWindow ∷ ∀ opts. Record opts -> Effect Unit +openNewWindow = openNewWindowImpl + +foreign import reload ∷ Effect Unit + +foreign import data BrowserHistory ∷ Type + +foreign import history ∷ Effect BrowserHistory + +foreign import addOnVisitedListenerImpl ∷ (EffectFn1 Foreign Unit) -> BrowserHistory -> Effect Unit + +addOnVisitedListener ∷ + (HistoryItem -> Effect Unit) -> + BrowserHistory -> Effect Unit +addOnVisitedListener callback = addOnVisitedListenerImpl (mkEffectFn1 rawCallback) + where + rawCallback f = do + let parsed = read f + case parsed of + Left err -> + Console.error + $ "Could not parse history item " + <> unsafeStringify f + <> " " + <> intercalateMap "\n" renderForeignError err + Right ok -> callback ok + +type HistoryItem = + { id ∷ String + , url ∷ Maybe String + , title ∷ Maybe String + , lastVisitTime ∷ Maybe Number -- really an instant + , visitCount ∷ Maybe Int + } diff --git a/src/WebExtension/Polyfill/Types.purs b/src/WebExtension/Polyfill/Types.purs new file mode 100644 index 0000000..8ffce14 --- /dev/null +++ b/src/WebExtension/Polyfill/Types.purs @@ -0,0 +1,10 @@ +module WebExtension.Polyfill.Types where + +import Prelude +import Data.Newtype (class Newtype) + +newtype StorageKey = StorageKey String + +derive instance newtypeStorageKey ∷ Newtype StorageKey _ +derive newtype instance eqStorageKey ∷ Eq StorageKey +derive newtype instance ordStorageKey ∷ Ord StorageKey